2014-07-11 - Fiji won't quit!
Of all the issues reported, one has stood out as the most tenacious and difficult to solve: Fiji bug #805, "Fiji not closing", colloquially known as the "Fiji won't quit!" problem. (Actually, versions of this bug existed before bug #805 was reported, but that bug is where we have been tracking the issue most recently.)
There were actually two different classes of misbehavior:
- Being unable to quit. The main window refuses to disappear, and the program becomes largely nonfunctional from that point forward and must be force closed.
- Process not terminating. ImageJ completes its shutdown routine and all windows disappear, but the Java process continues running in the background because
System.exit(0)is never called.
Being unable to quit
This class of bug was introduced because we tried to improve upon ImageJ1's behavior relating to the closing of windows and dialogs. It was previously the case that if you quit ImageJ while the Script Editor had tabs with unsaved changes, those changes would be discarded with no chance to save first. The behavior of ImageJ1 with other types of "non-sanctioned" windows (i.e., all
Window instances other than
TextWindow or non-
PlugInFrame) is to dispose them immediately without warning just prior to shutdown.
To address the problem, we added a callback hook to ImageJ1 on the
WindowManager.closeAllWindows() method, so that we could inject additional behavior. We then dispatched a windowClosing event to each remaining window to give them a chance to opt out of being closed. But it turns out that Java's standard paradigm of allowing windows to cancel their own closing conflates the ideas of confirming the close (e.g., with the user) with the process of actually doing the close/dispose on the window afterward. The approach proved too fragile and prone to deadlocks, so we ended up opting for a different approach instead. With a minor update to the Script Editor, its windows now still prompt to save changes when quitting (whoo hoo!), but without the fragility of the original approach.
Process not terminating
The second class of problem was introduced because ImageJ2 does not launch ImageJ via the
ij.ImageJ.main method. There are several reasons for this from an architectural and technical standpoint, which are outside the scope of this blog post. But suffice to say that
ij.ImageJ.main performs many actions which the ImageJ2 startup routine also needs to perform (but not in exactly the same way), such as handling command line arguments, most of which we covered—except for a call setting the
exitWhenQuitting flag to true. By default, ImageJ1 does not call
System.exit(0) when quitting, but whenever it is launched via its main method, it does. So even after we updated ImageJ2's quitting routine to lean on ImageJ1 as much as possible (which seemed to fix the problem in many of our tests), it was still not always enough since
System.exit(0) was never ultimately called. We fixed this discrepancy in the ImageJ legacy layer by always setting exitWhenQuitting to true as well.
There were other considerations too, of course. It needs to be possible to:
- Shut down ImageJ through the UI via the ImageJ1-based code path of ij.ImageJ#quit().
- Dispose of ImageJ programmatically via the ImageJ2-based code path of org.scijava.Context#dispose() which in turn disposes the
LegacyServiceand hence ImageJ1.
These two code paths need to behave very differently. In the case of (1), ImageJ1's quitting routine happens, but some extra logic is injected to handle window closing better (see "Being unable to quit" above) as well as shut down the ImageJ2 application context in addition to ImageJ1 itself. In the case of (2), ImageJ2 disposes its entire context including the
LegacyService which is responsible for managing ImageJ1, meaning that ImageJ1 gets disposed as part of ImageJ2's disposal—all of which must happen without calling
System.exit(0) and without prompting the user to save any changes.
We believe we have finally ironed out all these problems, but as the above explanation hopefully conveys, ImageJ1 is fraught with multiple code paths, and quitting the program is no exception. Searching the ImageJ1 codebase for
System.exit(0) yields five separate places where it gets called, one of which, astonishingly, is a potential code path of the
ij.IJ#getImage() method! Adding on the ImageJ legacy layer on top is a very complex endeavor, but we are proud to say that instances of ImageJ2 can be cleanly disposed of without shutting down the entire JVM, which provides a substantial boost to ImageJ's usability as a software library.
P.S. We added regression tests for all the major scenarios, even including an integration test for verifying that
System.exit(0) is called under the correct circumstances.
P.P.S. The "Fiji won't quit" T-shirt is coming soon! :-)