Clone this wiki locally
For stuff to be happening on the screen the gtk.main() function has to be running. Literally: the code control must be looping within gtk.main(), not somewhere on the stack below it. If the processing is in a stack frame below gtk.main() (i.e. in a callback), gtk.main() can not do things like redrawing background for moving windows, dynamically changing the cursor when it enters a certain region or talking to the window manager to tell it how we want the window decorations.
The problem is that there is currently always only one thread in Anaconda: so whenever we are in a callback we are choking the GUI processing.
A Simple Goal
I am modest, for the start I only aim to get rid of the ugly effects in our direct steps, beginning with the "Examining Devices" window: sometimes it pops up before its window decoration appears (should happen at the same time), other times the window content is missing/grayed out.
That's the start really. I hope the change is going to force a better UI/logic separation in the direct steps so after I am finished with the original goal I can look into how to apply similar principles everywhere (namely the iw/ directory).
Unfortunately, it is not sufficient just to fork a new processing thread from within a callback and return immediately to gtk.main. The problem is calls to the gtk library should only be done with the gtk lock and preferably from the same thread as gtk.main is running, else they just crash gtk (and Anaconda with it). The way to do gtk calls from the processing thread (as opposed to the gtk thread) is to call: gobject.idle_add(gtk.method()) which will make the main loop execute gtk.method() once it is not busy with anything else. But what our current code does instead (albeit through a chain of other calls) is do directly: gtk.method()
Mixing UI calls and other processing
We would prefer wrapping big chunks of GTK code in gobject.idle_add but at the same time we don't want to run any real processing in the GTK thread again, basically because:
- it would choke the main loop again
- if such a GTK thread runs at the same time as some other portion of e.g. the storage code it could damage integrity of the data
Thus the more the UI and the non-UI code is mixed together the harder it is to have only the UI code run in its own thread. One of the common solutions to this problem is the MVC design pattern. Applying to anaconda, I would say that:
- our models are the storage package and the anaconda object. it is somewhat separated from the rest.
- our views are the modules in iw/. They are terribly intermixed with everything else.
- our controller is the dispatch loop, the anaconda file and the Interface class and the UI callbacks under iw/, but again blended with both the remaining two domains.
The ultimate transcendental goal of the whole change is to achieve a nice MVC separation, so good that the UI could even run in a completely separate process (that would have other implications like we could have independent simplified liveinst UI). But looking at anything under iw/ it is just far too difficult at the moment.
Luckily, fixing the direct step I am after right now should be a lot more doable because it is in the storage and storage is already quite separated from the UI. And once I know how to tackle the basics and how to achieve safe and clean UI/logic interaction there, I can start thinking about doing something similar about the greater beast which is iw/.
Communication between the MVC domains
What we currently for our direct steps is: intf.waitWindow(...) which is still not optimal. It makes the model (storage) talk to the view directly, and that is not how MVC is meant to be. Also it violates the GTK threading dogma about not touching GTK from other threads. In MVC the model should just notify the view something has changed (the observer pattern) and once the view musters the energy it displays what is needed. That is also more what we want in a threaded GTK code: we are not able to synchronously get a handle for the Wait Window anyway (it is created in different thread and we absolutely must not manipulate it from other threads), we just sort of want to declare to the view that 'a wait window can be displayed any moment now'.
In this patch I introduce two concepts that implement this:
- the Status object: the model uses the status object to declare its status, like "I am busy scanning block devices" or "I am waiting for someone to give me the LUKS password"
- the View object that (asynchronously) communicates with the Status object and properly (i.e. correct thread plus the gtk lock) makes calls to the Interface object (anaconda.intf).
Eventually, I'd like all our models to only use the status interface to perform all their UI interaction. The beauty of this also is that, if written well, the Status interface will work from any thread. So the transition to the threaded UI can be much smoother (with gradual commits that always leave everything working) than I previously thought.
I have a working prototype of the Status/View. It uses python's Queue objects as the communication channel between the two (python lists are not thread safe). The interface is designed to support both asynchronous ("I am busy") and synchronous ("Give me the LUKS passwphrase") requests. Currently it resembles the old anaconda.intf's API but I believe it should be possible to simplify this into some stack-based API (because the anaconda windows stack on each other anyway): this would eliminate the need for the client to hold handlers of individual status changes requests (see this commit).
Using the interface I have the first direct step interacting with the GTK from a separate thread, that is display and hide it's "Examining Devices" message.
There's however no support yet for running only a subset of the direct steps in the special thread so the very next one freezes anaconda.
Is this going to cause new bugs?
Inevitably. However I will try to have a lot of runtime checks in the code so programming errors can be quickly spotted and safe us from many "WTF is going on here" situations. An example of that is the @gui_thread wrapper. Also the change as a whole is going to give us a cleaner UI interface and so safe us different frustrations.