1

I have a Qt application that connects to a card reader using various pcsc implementations under GNU/Linux, MacOS, and Windows. All communication with the card runs in a worker thread.

In one scenario, the user starts an operation requiring communication with the card via a card reader. The card reader has a keyboard and during the authentication procedure the user must enter their PIN on the reader's keyboard.

This operation is implemented by a call to SCardControl() (see e.g. the Microsoft documentation). As long as the user is working with the reader, the call to SCardControl() does not terminate and the worker thread is blocked by it.

At this point, the user might decide to close the application while the operation is still pending. Closing the application at this point causes the application to crash (on Linux with signal SIGABRT) because:

  1. The worker thread is blocked waiting for SCardControl() to return.
  2. The main thread cannot stop the blocked thread: neither quit() nor terminate() cause the thread to finish.
  3. When the application is exited, the QThread object for the worker thread is destroyed and, since the thread is still running state, it throws a signal to indicate an error.

I have tried several solutions.

  1. Subclass QThread and create a worker thread which calls setTerminationEnabled(true); to allow termination through QThread::terminate(). This does not work on MacOS: when QThread is destroyed, the thread is still in a running state and the signal SIGABRT is emitted.
  2. Handle signal SIGABRT on shutdown and ignore it. This did not seem to be a good idea but I wanted to try it out before discarding it. After ignoring signal SIGABRT, a signal SIGSEGV is received and the application crashes. I had adapted the approach described here.
  3. Try to unblock the thread by sending a command to the card reader from the main thread. I tried SCardCancel(), SCardDisconnect() and SCardReleaseContext() but none of these commands has any effect on the blocked thread.

I find it quite strange that it is not possible to cleanly shutdown an application when a thread is blocked on some function call, but all the solutions I have tried have not worked and I have run out of ideas. Did I overlook something? Does anybody have any useful hint?

EDIT

I looked into the Qt source code for QThread and found out that on Unix-like platforms QThread::terminate() uses pthread_cancel() internally. But apparently pthread_cancel() does not work / does nothing on Darwin, see e.g. here and here.

So, maybe I will really have to go with the option of showing a dialog to the user asking to remove the card from the reader.

giorgio-b
  • 113
  • 7

2 Answers2

0

Cleanly shutting down a thread is not possible from outside if it is blocked in a call. You can, however, prevent user from quitting the application before the operation has completed.

void MainWindow::closeEvent(QCloseEvent *closeEvent) {
     if (workerBlocked) closeEvent->ignore();
}

In addition, you can show a dialog telling the user the operation has to be completed first.

Also, if possible, you can let the window close but keep the application alive until the operation is complete by setting qApp->setQuitOnLastWindowClosed(false);

arhzu
  • 550
  • 3
  • 13
  • "Cleanly shutting down a thread is not possible from outside if it is blocked in a call.": I find it quite surprising that a thread can hijack a whole application. – giorgio-b Sep 19 '17 at 06:49
0

The problem boils down to the fact that a QThread object isn't destructible while the associated thread is running. Usually, it would a print statement like this to the debug output:

QThread: Destroyed while thread is still running

Don't agonize over trying to get SCardControl to return so that the worker thread can be quit safely (since it doesn't return as long as the user is interacting with the reader). Instead, You can follow this answer to destruct the QThread object in a safe manner with a minimum amount of changes to your current implementation.

Here is an example that shows what I mean:

#include <QtWidgets>

//a thread that can be destroyed at any time
//see http://stackoverflow.com/a/25230470
class SafeThread : public QThread{
    using QThread::run;
public:
    explicit SafeThread(QObject* parent= nullptr):QThread(parent){}
    ~SafeThread(){ quit(); wait(); }
};

//worker QObject class
class Worker : public QObject {
    Q_OBJECT
public:
    explicit Worker(QObject* parent = nullptr):QObject(parent){}
    ~Worker(){}

    Q_SLOT void doBlockingWork() {
        emit started();
        //the sleep call blocks the worker thread for 10 seconds!
        //consider it a mock call to the SCardControl function
        QThread::sleep(10);
        emit finished();
    }

    Q_SIGNAL void started();
    Q_SIGNAL void finished();
};


int main(int argc, char* argv[]) {
    QApplication a(argc, argv);

    //setup worker thread and QObject
    Worker worker;
    SafeThread thread;
    worker.moveToThread(&thread);
    thread.start();

    //setup GUI components
    QWidget w;
    QVBoxLayout layout(&w);
    QPushButton button("start working");
    QLabel status("idle");
    layout.addWidget(&button);
    layout.addWidget(&status);

    //connect signals/slots
    QObject::connect(&worker, &Worker::started, &status,
                     [&status]{ status.setText("working. . .");} );
    QObject::connect(&worker, &Worker::finished, &status,
                     [&status]{ status.setText("idle");} );
    QObject::connect(&button, &QPushButton::clicked, &worker, &Worker::doBlockingWork);

    w.show();
    return a.exec();
}

#include "main.moc"

Notice that the SafeThread's destructor makes sure to wait() until the associated thread has finished execution. And only afterwards, the main thread can proceed to call QThread's destructor.

Mike
  • 8,055
  • 1
  • 30
  • 44
  • "Notice that the SafeThread's destructor makes sure to wait() until the associated thread has finished execution. And only afterwards, the main thread can proceed to call QThread's destructor.": If I understand correctly, in this way the application won't terminate until the user performs some action, such as removing the card from the reader, that unblocks the thread. Suppose that instead I have another call to an external library function and that does not terminate. How can I force the thread to terminate and then shut down the application cleanly? – giorgio-b Sep 19 '17 at 06:47
  • I wouldn't call any way that uses `QThread::terminate()` a clean way. I would rather check on close event if the reader is connected, and show the user a prompt to disconnect it (as shown in arhzu's answer). – Mike Sep 19 '17 at 07:03
  • but anyway, you can replace the `quit()` call of the destructor with a `terminate()` call and see how things go for you. But if you are going this route, make sure there is no cleaner way to get the `SCardControl` call to return other than forcing an error by removing the reader. – Mike Sep 19 '17 at 07:05
  • But in another scenario I could have another external library that blocks a thread and does not timeout, e.g. a function that tries to connect to some remote services, or starts a computation that takes way too long. So asking the user to do something is not an options. Regarding `QThread::terminate()`: even if that was an option, it does not work under `MacOS`: the `SIGABRT` is still received and the application crashes. I am honestly a bit surprised that a thread can hijack a whole process in this way. – giorgio-b Sep 19 '17 at 07:06
  • @giorgio-b , if it is your code that you are running in the other thread (e.g. a heavy computation/connecting to remote services). You are responsible to implement a way to signal that code that it is time for it to exit/return (e.g. you can use something like [`requestInterruption()`](https://doc.qt.io/qt-5/qthread.html#requestInterruption) for this). – Mike Sep 19 '17 at 07:11
  • @giorgio-b , As written in the documentation, `terminate()` may terminate the thread in any point along its path, what if it was holding some locks that it hasn't released yet? would you be relying on the operating system to free them up for you? I am not sure it is always possible for the operating system to perform this kind of clean-up... – Mike Sep 19 '17 at 07:12
  • @giorgio-b , As written in the documentation, `terminate()` may terminate the thread in any point along its path, what if it was holding some locks that it hasn't released yet? would you be relying on the operating system to free them up for you? I am not sure it is always possible for the operating system to perform this kind of clean-up... – Mike Sep 19 '17 at 07:12
  • As I said above, it is not my code, it is an external library I do not control. Since all I want to do is to terminate the whole process (and that will release all the resources held by the process) I do not see why this should cause a crash, a core dump and, under MacOS and Windows, a dialog with an error message. – giorgio-b Sep 19 '17 at 07:12
  • @giorgio-b , I was answering your question: *"But in another scenario I could have another external library that blocks a thread and does not timeout, e.g. a function that tries to connect to some remote services, or starts a computation that takes way too long. So asking the user to do something is not an options."* – Mike Sep 19 '17 at 07:14
  • @giorgio-b , `terminate()` may work for your case. I told you to replace `quit()` with `terminate()`, you need to `wait()` after calling `terminate()` to make sure that the thread is no longer in the running state. Have you tried that? – Mike Sep 19 '17 at 07:15
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/154756/discussion-between-giorgio-b-and-mike). – giorgio-b Sep 19 '17 at 07:16
  • @giorgio-b , but again, before deciding to go through `terminate()` route, double check that there is no other clean way to get `SCardControl` to terminate. Sorry, I haven't used this part of the WinAPI before. But maybe connecting to the reader using `SCARD_SHARE_SHARED` is an option for you. and this way you don't have to worry about your application exiting before another instance gets started... – Mike Sep 19 '17 at 07:19
  • 1
    @giorgio-b Terminating the process does not necessarily release all the resources. For example, [POSIX shared memory](https://stackoverflow.com/questions/15088525/what-happens-to-interprocess-memory-if-one-of-the-processes-dies-unexpectedly) needs to be explicitly unlinked, otherwise it will remain until system shutdown. – arhzu Sep 20 '17 at 08:31