Как разблокировать QThread при выполнении вызова pcsc?

У меня есть приложение Qt, которое подключается к кард-ридеру, используя различные pcsc реализации под GNU/Linux, MacOS и Windows. Все общение с картой происходит в рабочем потоке.

В одном сценарии пользователь запускает операцию, требующую связи с картой через кард-ридер. Считыватель карт имеет клавиатуру, и во время процедуры аутентификации пользователь должен ввести свой PIN-код на клавиатуре считывателя.

Эта операция реализуется путем вызова SCardControl() (см., например, документацию Microsoft). Пока пользователь работает с читателем, вызов SCardControl() не завершается и рабочий поток блокируется им.

На этом этапе пользователь может решить закрыть приложение, пока операция еще не завершена. Закрытие приложения в этот момент вызывает сбой приложения (в Linux с сигналом SIGABRT) так как:

  1. Рабочий поток заблокирован в ожидании SCardControl() возвращать.
  2. Основной поток не может остановить заблокированный поток: ни quit() ни terminate() заставить нить закончить.
  3. Когда приложение закрывается, QThread объект для рабочего потока уничтожается и, поскольку поток все еще находится в рабочем состоянии, он выдает сигнал, указывающий на ошибку.

Я пробовал несколько решений.

  1. Подкласс QThread и создать рабочий поток, который вызывает setTerminationEnabled(true); разрешить прекращение через QThread::terminate(), Это не работает на MacOS: когда QThread уничтожен, поток все еще находится в рабочем состоянии и сигнал SIGABRT испускается
  2. Обрабатывать сигнал SIGABRT при выключении и игнорируй это. Кажется, это не очень хорошая идея, но я хотел попробовать ее, прежде чем выбросить. После игнорирования сигнала SIGABRTсигнал SIGSEGV получено и приложение вылетает. Я адаптировал подход, описанный здесь.
  3. Попробуйте разблокировать поток, отправив команду на устройство чтения карт из основного потока. Я старался SCardCancel(), SCardDisconnect() а также SCardReleaseContext() но ни одна из этих команд не влияет на заблокированный поток.

Я нахожу довольно странным, что невозможно просто закрыть приложение, когда поток заблокирован при вызове некоторой функции, но все решения, которые я попробовал, не сработали, и у меня закончились идеи. Я что-то упустил? У кого-нибудь есть полезный совет?

РЕДАКТИРОВАТЬ

Я посмотрел в исходный код Qt для QThread и обнаружил, что на Unix-подобных платформах QThread::terminate() использования pthread_cancel() внутренне. Но видимо pthread_cancel() не работает / ничего не делает на Darwinсм. например здесь и здесь.

Так что, возможно, мне действительно придется пойти с возможностью показывать пользователю диалог с просьбой вынуть карту из ридера.

2 ответа

Проблема сводится к тому, что QThread объект не может быть уничтожен во время работы связанного потока. Обычно для вывода отладки это будет оператор print, подобный следующему:

QThread: уничтожено во время работы потока

Не мучайся, пытаясь получить SCardControl вернуться, чтобы рабочий поток можно было безопасно завершить (поскольку он не возвращается, пока пользователь взаимодействует с читателем). Вместо этого Вы можете следовать этому ответу, чтобы уничтожить QThread Безопасный объект с минимальным количеством изменений в вашей текущей реализации.

Вот пример, который показывает, что я имею в виду:

#include <QtWidgets>

//a thread that can be destroyed at any time
//see http://stackru.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"

Обратите внимание, что SafeThreadдеструктор обязательно wait() пока связанный поток не закончил выполнение. И только после этого основной поток может приступить к вызову QThreadдеструктор.

Чистое отключение потока снаружи невозможно, если он заблокирован во время вызова. Однако вы можете запретить пользователю выходить из приложения до завершения операции.

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

Кроме того, вы можете показать диалоговое окно, сообщающее пользователю, что операция должна быть завершена в первую очередь.

Также, если возможно, вы можете позволить окну закрыться, но оставить приложение живым до завершения операции, установив qApp->setQuitOnLastWindowClosed(false);

Другие вопросы по тегам