Brown paper bag

For those of you who ran into the bug in Music Applet 2.3.0 where the applet would seem to crash as soon as you started it, well, I finally figured out what was going on. It turns out to be a blatant brown-paper-bag bug, and to proper chastise myself for releasing Music Applet with it, I will now detail what its cause was and how I discovered it.

Since I have Debian’s music-applet package installed, during development I install the new code in a separate directory and run it from there. This leaves the problem of actually loading the applet from its nonstandard location. Normally, when you add an applet to a panel, Bonobo checks its database to see what program needs to be run. However, if the program is already running, it will connect to it instead of starting a new instance. So, when testing new code, I start the new applet code from the command line, and then add it to the panel.

This way, I don’t have to touch the system files to run the new code. This method also gives me an opportunity to augment the Python path with the directory where the new code’s modules are, since naturally they’re in a nonstandard location too. It furthermore has the advantage of printing the output on stdout and stderr to the console, whereas if Bonobo started the applet, they’d wind up in /dev/null, which is bad for debugging.

Now, several people reported the crash-on-startup-if-Amarok-support-is-enabled bug, but I was completely unable to reproduce it on my system. None of them had a stacktrace or core file or any other debugging output to offer. However, they were all using Ubuntu and Python 2.5, whereas Debian still uses Python 2.4 by default. It looked like the trouble might lie there.

After jumping through hoops to get the applet running on my system under Python 2.5 (complicated by the fact that Debian for some reason doesn’t have versions of the DCOP modules for Python 2.5), I still wasn’t able to reproduce the problem. Naturally, if I can’t reproduce the problem, and I can’t get any good details from the bug reports, I can’t figure out what’s causing the bug, let alone verify that any fix I come up with works.

I finally decided to go all-out and try to reproduce the exact circumstances of the crash on my machine, without completely wrecking the copy of the applet provided by Debian. I hacked the Bonobo .server file for the applet to point to the local copy of the applet, and then hacked the applet startup code to explicitly run under Python 2.5. Oddly, when the applet started, it would identify its version of 2.2.1 (the version provided in Debian) and not 2.3.0.bzr (the version installed locally), nor would it have any of the features added in 2.3.0, including the Amarok plugin.

After banging my head on this for a while, I realized that even though Bonobo was running the local copy of the applet’s startup code, that startup code was then loading the support modules installed in the system paths — the 2.2.1 code — instead of the local code. When starting the applet from Bonobo, there’s no opportunity to tweak the include path like you get from the command line. I then further hacked the applet to add the right paths to sys.path, and finally it started working, in that it stopped working and crashed like it was supposed to. Or, not supposed to, but you know what I mean.

Finally, I was getting somewhere. To track down the line causing the crash, I started putting lines like this in the code to trace the control flow:

os.system("xmessage 'About to start thread'")

This would pop up a crude dialog box with the message, which I relied on instead of printing to stdout or stderr since, well, those were going to /dev/null now. After playing around with things for a while, I tracked down the offending line of code:

self.__plugin._app = kdecore.KApplication (sys.argv, "music-applet")

Thinking it might be throwing an exception for some reason, I tried wrapping it in a try/except block, but no luck. That was definitely the line, but still no clue why it was failing so spectacularly. Thinking there might be something on stdout or stderr that could help, I realized I could point them at files on applet startup so I could read them after the fact:

sys.stdout = open ("/home/paul/music-applet/debug.stdout")
sys.stderr = open ("/home/paul/music-applet/debug.stderr")

Now I was seeing the normal debugging spew from the applet, but still no messages about the crash. It then hit me that this was only going to affect my Python code; everything else would still be using the “true” stdout and stderr. I’d have to literally dupe the system into redirecting the underlying file descriptors into writing into my debug files:

new_stdout = open ("/home/paul/music-applet/debug.stdout", "w")
new_stderr = open ("/home/paul/music-applet/debug.stderr", "w")
 
os.dup2(new_stdout.fileno(), sys.stdout.fileno())
os.dup2(new_stderr.fileno(), sys.stderr.fileno())

Aha! Now this showed up in the output right before the crash:

music-applet: Unknown option '--oaf-activate-iid=OAFIID:GNOME_Music_Applet_Factory'.
music-applet: Use --help to get a list of available command line options.

That option is one of the things that Bonobo puts on the command line when it starts an applet, and isn’t one of the things I was giving to the applet when launching it manually. Aha? Checking the source code of the KDE libraries for that error message turned up this:

void
KCmdLineArgs::usage(const QString &error)
{
    assert(KGlobal::_locale);
    QCString localError = error.local8Bit();
    if (localError[error.length()-1] == '\n')
  localError = localError.left(error.length()-1);
    fprintf(stderr, "%s: %s\n", argv[0], localError.data());
 
    QString tmp = i18n("Use --help to get a list of available command line options.");
    localError = tmp.local8Bit();
    fprintf(stderr, "%s: %s\n", argv[0], localError.data());
    exit(254);
}

Aha! The KApplication constructor indirectly calls this when it sees something on the command line it doesn’t understand. It prints a message to stderr, and them immediately exits the program! The applet wasn’t crashing; the KDE libraries were terminating it because they didn’t understand the command-line arguments Bonobo uses!

Here’s where that brown paper bag comes in: this bug would affect anyone relying on Bonobo to start the applet. In other words, everyone who isn’t me. By always starting the applet from the command line first, I never tested the applet in the same runtime configuration that everyone else on the planet would be using, and so never encountered a bug that everyone else would suffer. The whole Python version thing was a red herring.

Now that I finally knew what was happening, the fix was trivial: strip out any command-line arguments before calling the KApplication constructor:

fake_argv = sys.argv[0:1]
self.__plugin._app = kdecore.KApplication (fake_argv, "music-applet")

However, I still maintain that if there is a hell, there is a special place in it reserved for people who write libraries that call exit(), and thus do not give the user of the library an opportunity to try to recover from an error.

Needless to say, this bug is fixed in 2.3.1.

One Response

  1. I think this also warrants a Kulinibox.

Comments are closed.