Как разблокировать QThread при выполнении вызова pcsc?
У меня есть приложение Qt, которое подключается к кард-ридеру, используя различные pcsc
реализации под GNU/Linux, MacOS и Windows. Все общение с картой происходит в рабочем потоке.
В одном сценарии пользователь запускает операцию, требующую связи с картой через кард-ридер. Считыватель карт имеет клавиатуру, и во время процедуры аутентификации пользователь должен ввести свой PIN-код на клавиатуре считывателя.
Эта операция реализуется путем вызова SCardControl()
(см., например, документацию Microsoft). Пока пользователь работает с читателем, вызов SCardControl()
не завершается и рабочий поток блокируется им.
На этом этапе пользователь может решить закрыть приложение, пока операция еще не завершена. Закрытие приложения в этот момент вызывает сбой приложения (в Linux с сигналом SIGABRT
) так как:
- Рабочий поток заблокирован в ожидании
SCardControl()
возвращать. - Основной поток не может остановить заблокированный поток: ни
quit()
ниterminate()
заставить нить закончить. - Когда приложение закрывается,
QThread
объект для рабочего потока уничтожается и, поскольку поток все еще находится в рабочем состоянии, он выдает сигнал, указывающий на ошибку.
Я пробовал несколько решений.
- Подкласс
QThread
и создать рабочий поток, который вызываетsetTerminationEnabled(true);
разрешить прекращение черезQThread::terminate()
, Это не работает наMacOS
: когдаQThread
уничтожен, поток все еще находится в рабочем состоянии и сигналSIGABRT
испускается - Обрабатывать сигнал
SIGABRT
при выключении и игнорируй это. Кажется, это не очень хорошая идея, но я хотел попробовать ее, прежде чем выбросить. После игнорирования сигналаSIGABRT
сигналSIGSEGV
получено и приложение вылетает. Я адаптировал подход, описанный здесь. - Попробуйте разблокировать поток, отправив команду на устройство чтения карт из основного потока. Я старался
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);