Interesting issues with the PyQt twisted reactor

by mandel on March 21st, 2012

On Windows Ubuntu One uses the twisted reactor to run the Qt UI. The main reason for this is that the IPC protocol that is used was written in twisted. This has been giving us a number of head aches like the one we experienced today.

The following code simply shows a dialog that will as the user for his proxy credentials and will store them in the key-ring of which ever platform is being used:

def main():
    """Main method used to show the creds dialog."""
 
    if sys.platform == 'win32':
        import qt4reactor
        qt4reactor.install()
        logger.debug('Qt reactor installed.')
 
    app = QApplication(sys.argv)
    args = parse_args()
    win = ProxyCredsDialog(domain=args.domain,
                           retry=args.retry)
    return_code = win.exec_()
    if sys.platform == 'win32':
        from twisted.internet import reactor
        reactor.run()
    sys.exit(return_code)

From the dialog the most interesting part is the one in which the credentials are stored:

@defer.inlineCallbacks
def _on_save_clicked(self, *args):
    """Save the new credentials."""
    username = unicode(self.ui.username_entry.text()).encode('utf8')
    password = unicode(self.ui.password_entry.text()).encode('utf8')
    creds = dict(username=username, password=password)
    try:
        logger.debug('Save credentials as for domain %s.', self.domain)
        yield self.keyring.set_credentials(self.domain, creds)
    except Exception, e:
        logger.exception('Could not set credentials:')
        self.done(EXCEPTION_RAISED)
    logger.debug('Stored creds')
    self.done(USER_SUCCESS)

And to give even more details, the following is what is used to spawn a thread to store the credentials on windows:

def set_credentials(self, app_name, cred):
    """Set the credentials of the Ubuntu SSO item."""
    # the windows keyring can only store a pair username-password
    # so we store the data using ubuntu_sso as the user name. Then
    # the cred will be stored as the string representation of the dict.
    return deferToThread(self.keyring.set_password, app_name, USERNAME,
                             dumps(cred))

A priori there is nothing wrong with the code, or is it? Doing an IRL test you will see that the credentials are never stored, what’s even more none of the deferreds are called. But why? In theory the qt reactor should be taking care of everything which includes the deferreds, the deferToThread and the execution of the ui.. well, it is not. When we look a little closer we can see that we use the exec_ method from the QDialog and this is the root of the bug. Lets put an example, the following is possible in Qt:

import sys
 
from PyQt4.QtGui import QApplication, QDialog
 
app = QApplication(sys.argv)
dialog = QDialog()
dialog.exec_()

As you can see we are launching the dialog but we did not execute the application, but why is that? The main reason is found in the implementation of exec in the QDialog class (this time in C++, ouch!):

int QDialog::exec()
{
     Q_D(QDialog);
     if (d->eventLoop) {
         qWarning("QDialog::exec: Recursive call detected");
         return -1;
     }
 
     bool deleteOnClose = testAttribute(Qt::WA_DeleteOnClose);
     setAttribute(Qt::WA_DeleteOnClose, false);
 
     bool wasShowModal = testAttribute(Qt::WA_ShowModal);
     setAttribute(Qt::WA_ShowModal, true);
     setResult(0);
 
     show();
 
     QEventLoop eventLoop;
     d->eventLoop = &eventLoop;
     (void) eventLoop.exec();
     d->eventLoop = 0;
 
     setAttribute(Qt::WA_ShowModal, wasShowModal);
 
     int res = result();
     if (deleteOnClose)
         delete this;
     return res;
 }

As you can see the implementation uses a QEventLoop, if you read the documentation you will noticed that using exec of the event loops and the default flag all the events will me processed by this event loop, which is not the main event loop, but a child one (small brain fuck here). This means that, due to the implementation of the qtreactor, when using a dialog exec_ (or better say a QEventLoop.exec method) the reactor main loop is not processing the events which means that all your deferreds, deferToThread etc.. will not be processed :(

Nevertheless there is a way to work around this bug in the qtreactor implementation (because is not the qt fault and it is well documented) which is doing the following workaround:

win = ProxyCredsDialog(domain=args.domain, retry=args.retry)
win.show()
win.finished.connect(exit_app)

The above code ensures that the events will be handeled by the reactor main loop and not by a child QEventLoop.

In summary, qtreactor is buggy and will give you problems.. but if you really have to use it (like we do) do remember this detail, DO NOT use exec.