From 08d7920b8090e394209fdc81e9fa20a849a96ba3 Mon Sep 17 00:00:00 2001 From: Mart Somermaa Date: Fri, 8 May 2026 13:28:25 +0300 Subject: [PATCH] WIP: Experiment with Controller refactor Signed-off-by: Mart Somermaa Assisted-by: gpt-5.5 xhigh fast --- src/controller/CMakeLists.txt | 6 + src/controller/commandsession.cpp | 234 +++++++++++++++++++ src/controller/commandsession.hpp | 96 ++++++++ src/controller/controller.cpp | 266 +++++++--------------- src/controller/controller.hpp | 38 +--- src/controller/nativemessagingsession.cpp | 44 ++++ src/controller/nativemessagingsession.hpp | 33 +++ src/controller/responsesink.cpp | 30 +++ src/controller/responsesink.hpp | 39 ++++ 9 files changed, 580 insertions(+), 206 deletions(-) create mode 100644 src/controller/commandsession.cpp create mode 100644 src/controller/commandsession.hpp create mode 100644 src/controller/nativemessagingsession.cpp create mode 100644 src/controller/nativemessagingsession.hpp create mode 100644 src/controller/responsesink.cpp create mode 100644 src/controller/responsesink.hpp diff --git a/src/controller/CMakeLists.txt b/src/controller/CMakeLists.txt index 6a30eae8..1072857e 100644 --- a/src/controller/CMakeLists.txt +++ b/src/controller/CMakeLists.txt @@ -13,6 +13,8 @@ add_library(controller STATIC command-handlers/signauthutils.cpp command-handlers/signauthutils.hpp commandhandler.hpp + commandsession.cpp + commandsession.hpp commands.cpp commands.hpp controller.cpp @@ -21,7 +23,11 @@ add_library(controller STATIC inputoutputmode.hpp logging.cpp logging.hpp + nativemessagingsession.cpp + nativemessagingsession.hpp qeid.hpp + responsesink.cpp + responsesink.hpp retriableerror.hpp threads/cardeventmonitorthread.hpp threads/commandhandlerconfirmthread.hpp diff --git a/src/controller/commandsession.cpp b/src/controller/commandsession.cpp new file mode 100644 index 00000000..92957fd3 --- /dev/null +++ b/src/controller/commandsession.cpp @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2020-2024 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "commandsession.hpp" + +#include "threads/cardeventmonitorthread.hpp" +#include "threads/commandhandlerconfirmthread.hpp" +#include "threads/commandhandlerrunthread.hpp" +#include "threads/waitforcardthread.hpp" + +#include "utils/utils.hpp" + +using namespace pcsc_cpp; +using namespace electronic_id; + +CommandSession::CommandSession(CommandHandler::ptr handler, observer_ptr w) : + commandHandler(std::move(handler)), window(w) +{ + REQUIRE_NON_NULL(commandHandler) + REQUIRE_NON_NULL(window) + + connect(window, &WebEidUI::retry, this, &CommandSession::requestRestart); + connect(window, &WebEidUI::accepted, this, &CommandSession::confirm); + connect(window, &WebEidUI::rejected, this, &CommandSession::cancel); + connect(window, &WebEidUI::failure, this, &CommandSession::fail); + connect(window, &WebEidUI::waitingForPinPad, this, &CommandSession::confirm); +} + +CommandSession::~CommandSession() +{ + stop(); +} + +void CommandSession::start() noexcept +try { + startCardWait(); +} catch (const std::exception& error) { + fail(error.what()); +} + +void CommandSession::stop() noexcept +{ + stopChildThreads(); + if (commandHandler) { + commandHandler->disconnect(); + } +} + +void CommandSession::startCardWait() +{ + REQUIRE_NON_NULL(commandHandler) + + setState(State::WaitingForCard); + + auto* waitForCardThread = new WaitForCardThread(this); + trackThread(waitForCardThread); + connect(this, &CommandSession::stopCardEventMonitorThread, waitForCardThread, + &WaitForCardThread::requestInterruption); + connect(waitForCardThread, &WaitForCardThread::statusUpdate, this, + &CommandSession::statusUpdate); + connect(waitForCardThread, &WaitForCardThread::cardsAvailable, this, + &CommandSession::onCardsAvailable); + connectChildThreadSignals(waitForCardThread); + + // When the command handler retrieves certificates successfully, start card event monitoring + // while the user enters the PIN. + connect(commandHandler.get(), &CommandHandler::singleCertificateReady, this, + &CommandSession::onCertificatesLoaded); + connect(commandHandler.get(), &CommandHandler::multipleCertificatesReady, this, + &CommandSession::onCertificatesLoaded); + connect(commandHandler.get(), &CommandHandler::verifyPinFailed, this, + &CommandSession::onCertificatesLoaded); + + waitForCardThread->start(); +} + +void CommandSession::onCardsAvailable( + const std::vector& availableEids) noexcept +try { + REQUIRE_NON_NULL(commandHandler) + REQUIRE_NON_NULL(window) + REQUIRE_NOT_EMPTY_CONTAINS_NON_NULL_PTRS(availableEids) + + setState(State::ReadingCertificates); + + for (const auto& card : availableEids) { + const auto protocol = + card->smartcard().protocol() == SmartCard::Protocol::T0 ? "T=0" : "T=1"; + qInfo() << "Card" << card->name() << "in reader" << card->smartcard().readerName() + << "using protocol" << protocol; + } + + window->showWaitingForCardPage(commandHandler->commandType()); + + commandHandler->connectSignals(window); + + auto* commandHandlerRunThread = + new CommandHandlerRunThread(this, *commandHandler, availableEids); + trackThread(commandHandlerRunThread); + connectChildThreadSignals(commandHandlerRunThread); + + commandHandlerRunThread->start(); + +} catch (const std::exception& error) { + fail(error.what()); +} + +void CommandSession::onCertificatesLoaded() noexcept +try { + setState(State::MonitoringCardChanges); + + auto* cardEventMonitorThread = new CardEventMonitorThread(this, commandType()); + trackThread(cardEventMonitorThread); + connect(this, &CommandSession::stopCardEventMonitorThread, cardEventMonitorThread, + &CardEventMonitorThread::requestInterruption); + connect(cardEventMonitorThread, &CardEventMonitorThread::cardEvent, this, + &CommandSession::requestRestart); + connectChildThreadSignals(cardEventMonitorThread); + cardEventMonitorThread->start(); +} catch (const std::exception& error) { + fail(error.what()); +} + +void CommandSession::confirm(const EidCertificateAndPinInfo& certAndPinInfo) noexcept +try { + REQUIRE_NON_NULL(commandHandler) + + setState(State::Confirming); + emit stopCardEventMonitorThread(); + + auto* commandHandlerConfirmThread = + new CommandHandlerConfirmThread(this, *commandHandler, window, certAndPinInfo); + trackThread(commandHandlerConfirmThread); + connect(commandHandlerConfirmThread, &CommandHandlerConfirmThread::completed, this, + &CommandSession::onCommandHandlerConfirmCompleted); + connectChildThreadSignals(commandHandlerConfirmThread); + + commandHandlerConfirmThread->start(); + +} catch (const std::exception& error) { + fail(error.what()); +} + +void CommandSession::onCommandHandlerConfirmCompleted(const QVariantMap& result) noexcept +{ + qDebug() << "Command completed"; + setState(State::Completed); + stopChildThreads(); + emit completed(result); +} + +void CommandSession::cancel() noexcept +{ + qDebug() << "User cancelled"; + setState(State::Cancelled); + stopChildThreads(); + emit cancelled(); +} + +void CommandSession::fail(const QString& error) noexcept +{ + if (currentState == State::Failed) { + return; + } + + setState(State::Failed); + stopChildThreads(); + emit failed(error); +} + +void CommandSession::requestRestart() noexcept +{ + setState(State::Retrying); + stopChildThreads(); + emit restartRequested(); +} + +void CommandSession::connectChildThreadSignals(ControllerChildThread* childThread) +{ + REQUIRE_NON_NULL(childThread) + connect(childThread, &ControllerChildThread::failure, this, &CommandSession::fail); + connect(childThread, &ControllerChildThread::retry, this, &CommandSession::retry); + connect(childThread, &ControllerChildThread::cancel, this, &CommandSession::cancel); +} + +void CommandSession::trackThread(QThread* thread) +{ + REQUIRE_NON_NULL(thread) + childThreads.push_back(thread); +} + +void CommandSession::stopChildThreads() noexcept +{ + emit stopCardEventMonitorThread(); + + for (const auto& thread : childThreads) { + if (!thread) { + continue; + } + qDebug() << "Interrupting thread" << uintptr_t(thread.data()); + disconnect(this, nullptr, thread, nullptr); + thread->disconnect(this); + thread->requestInterruption(); + } + + ControllerChildThread::waitForControllerNotify.wakeAll(); + + for (const auto& thread : childThreads) { + if (thread) { + thread->wait(); + } + } + + childThreads.clear(); +} diff --git a/src/controller/commandsession.hpp b/src/controller/commandsession.hpp new file mode 100644 index 00000000..71eab069 --- /dev/null +++ b/src/controller/commandsession.hpp @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2020-2024 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "commandhandler.hpp" + +#include "utils/observer_ptr.hpp" + +#include +#include + +#include + +class ControllerChildThread; + +class CommandSession : public QObject +{ + Q_OBJECT + +public: + enum class State : quint8 { + Idle, + WaitingForCard, + ReadingCertificates, + WaitingForUserConfirmation, + Confirming, + MonitoringCardChanges, + Retrying, + Completed, + Cancelled, + Failed, + }; + Q_ENUM(State) + + CommandSession(CommandHandler::ptr handler, observer_ptr window); + ~CommandSession() override; + + CommandType commandType() const { return commandHandler->commandType(); } + State state() const noexcept { return currentState; } + + void start() noexcept; + void stop() noexcept; + +signals: + void statusUpdate(RetriableError status); + void retry(RetriableError error); + void completed(const QVariantMap& result); + void cancelled(); + void failed(const QString& error); + void restartRequested(); + +private slots: + void + onCardsAvailable(const std::vector& availableEids) noexcept; + void onCertificatesLoaded() noexcept; + void confirm(const EidCertificateAndPinInfo& certAndPinInfo) noexcept; + void cancel() noexcept; + void fail(const QString& error) noexcept; + void requestRestart() noexcept; + void onCommandHandlerConfirmCompleted(const QVariantMap& result) noexcept; + +private: + void startCardWait(); + void connectChildThreadSignals(ControllerChildThread* childThread); + void trackThread(QThread* thread); + void stopChildThreads() noexcept; + void setState(State state) noexcept { currentState = state; } + + CommandHandler::ptr commandHandler; + observer_ptr window = nullptr; + std::vector> childThreads; + State currentState = State::Idle; + +signals: + void stopCardEventMonitorThread(); +}; diff --git a/src/controller/controller.cpp b/src/controller/controller.cpp index 80a3f0ec..a89001cc 100644 --- a/src/controller/controller.cpp +++ b/src/controller/controller.cpp @@ -22,19 +22,10 @@ #include "controller.hpp" -#include "threads/cardeventmonitorthread.hpp" -#include "threads/commandhandlerconfirmthread.hpp" -#include "threads/commandhandlerrunthread.hpp" -#include "threads/waitforcardthread.hpp" - -#include "utils/utils.hpp" - #include "application.hpp" -#include "inputoutputmode.hpp" -#include "writeresponse.hpp" +#include "nativemessagingsession.hpp" -using namespace pcsc_cpp; -using namespace electronic_id; +#include "utils/utils.hpp" namespace { @@ -55,23 +46,18 @@ QVariantMap makeErrorObject(const QString& errorCode, const QString& errorMessag void Controller::run() noexcept try { - // If a command is passed, the application is in command-line mode, else in stdin/stdout mode. - const bool isInCommandLineMode = bool(command); - isInStdinMode = !isInCommandLineMode; + initializeResponseSink(); qInfo() << QCoreApplication::applicationName() << "app" << QCoreApplication::applicationVersion() << "running in" - << (isInStdinMode ? "stdin/stdout" : "command-line") << "mode"; + << (command ? "command-line" : "stdin/stdout") << "mode"; - // TODO: cut out stdin mode separate class to avoid bugs in safari mode - if (isInStdinMode) { - // In stdin/stdout mode we first output the version as required by the WebExtension - // and then wait for the actual command. - writeResponseToStdOut(isInStdinMode, - {{QStringLiteral("version"), QCoreApplication::applicationVersion()}}, - "get-version"); + if (!command) { + auto* nativeMessagingSession = dynamic_cast(responseSink.get()); + REQUIRE_NON_NULL(nativeMessagingSession) - command = readCommandFromStdin(); + nativeMessagingSession->writeHandshake(); + command = nativeMessagingSession->readCommand(); } REQUIRE_NON_NULL(command) @@ -80,205 +66,107 @@ try { WebEidUI::showAboutPage(); return; case CommandType::QUIT: - // If quit is requested, respond with empty JSON object and quit immediately. qInfo() << "Quit requested, exiting"; - writeResponseToStdOut(true, {}, "quit"); + writeResult({}, CommandType::QUIT); emit quit(); return; default: break; } - commandHandler = getCommandHandler(*command); - startCommandExecution(); } catch (const std::exception& error) { onCriticalFailure(error.what()); } -void Controller::startCommandExecution() +void Controller::initializeResponseSink() { - REQUIRE_NON_NULL(commandHandler) - - // Reader monitor thread setup. - auto* waitForCardThread = new WaitForCardThread(this); - connect(this, &Controller::stopCardEventMonitorThread, waitForCardThread, - &WaitForCardThread::requestInterruption); - connect(waitForCardThread, &ControllerChildThread::failure, this, - &Controller::onCriticalFailure); - connect(waitForCardThread, &WaitForCardThread::statusUpdate, this, &Controller::statusUpdate); - connect(waitForCardThread, &WaitForCardThread::cardsAvailable, this, - &Controller::onCardsAvailable); - // When the command handler run thread retrieves certificates successfully, call - // onCertificatesLoaded() that starts card event monitoring while user enters the PIN. - connect(commandHandler.get(), &CommandHandler::singleCertificateReady, this, - &Controller::onCertificatesLoaded); - connect(commandHandler.get(), &CommandHandler::multipleCertificatesReady, this, - &Controller::onCertificatesLoaded); - connect(commandHandler.get(), &CommandHandler::verifyPinFailed, this, - &Controller::onCertificatesLoaded); - - // UI setup. - createWindow(); - - // Finally, start the thread to wait for card insertion after everything is wired up. - waitForCardThread->start(); -} - -void Controller::createWindow() -{ - window = WebEidUI::createAndShowDialog(commandHandler->commandType()); - connect(this, &Controller::statusUpdate, window, &WebEidUI::onSmartCardStatusUpdate); - connect(this, &Controller::retry, window, &WebEidUI::onRetry); - connect(window, &WebEidUI::retry, this, &Controller::onRetry); - connect(window, &WebEidUI::accepted, this, &Controller::onDialogOK); - connect(window, &WebEidUI::rejected, this, &Controller::onDialogCancel); - connect(window, &WebEidUI::failure, this, &Controller::onCriticalFailure); - connect(window, &WebEidUI::waitingForPinPad, this, &Controller::onConfirmCommandHandler); - connect(window, &WebEidUI::destroyed, this, [this] { window = nullptr; }); -} - -void Controller::onCardsAvailable( - const std::vector& availableEids) noexcept -try { - REQUIRE_NON_NULL(commandHandler) - REQUIRE_NON_NULL(window) - REQUIRE_NOT_EMPTY_CONTAINS_NON_NULL_PTRS(availableEids) - - for (const auto& card : availableEids) { - const auto protocol = - card->smartcard().protocol() == SmartCard::Protocol::T0 ? "T=0" : "T=1"; - qInfo() << "Card" << card->name() << "in reader" << card->smartcard().readerName() - << "using protocol" << protocol; + if (responseSink) { + return; } - window->showWaitingForCardPage(commandHandler->commandType()); + // QUIT is a native-messaging-only command, but tests can inject it directly. + if (!command || command->first == CommandType::QUIT) { + responseSink = std::make_unique(); + } else { + responseSink = std::make_unique(); + } +} - commandHandler->connectSignals(window); +void Controller::startCommandExecution() +{ + REQUIRE_NON_NULL(command) - auto* commandHandlerRunThread = - new CommandHandlerRunThread(this, *commandHandler, availableEids); - connectRetry(commandHandlerRunThread); + auto commandHandler = getCommandHandler(*command); + const auto type = commandHandler->commandType(); - commandHandlerRunThread->start(); + createWindow(type); -} catch (const std::exception& error) { - onCriticalFailure(error.what()); -} + commandSession = std::make_unique(std::move(commandHandler), window); + connect(commandSession.get(), &CommandSession::statusUpdate, this, &Controller::statusUpdate); + connect(commandSession.get(), &CommandSession::retry, this, &Controller::retry); + connect(commandSession.get(), &CommandSession::completed, this, + &Controller::onCommandSessionCompleted); + connect(commandSession.get(), &CommandSession::cancelled, this, + &Controller::onCommandSessionCancelled); + connect(commandSession.get(), &CommandSession::failed, this, &Controller::onCriticalFailure); + connect(commandSession.get(), &CommandSession::restartRequested, this, &Controller::onRetry); -void Controller::onCertificatesLoaded() noexcept -try { - auto* cardEventMonitorThread = new CardEventMonitorThread(this, commandType()); - connect(this, &Controller::stopCardEventMonitorThread, cardEventMonitorThread, - &CardEventMonitorThread::requestInterruption); - connect(cardEventMonitorThread, &ControllerChildThread::failure, this, - &Controller::onCriticalFailure); - connect(cardEventMonitorThread, &CardEventMonitorThread::cardEvent, this, &Controller::onRetry); - cardEventMonitorThread->start(); -} catch (const std::exception& error) { - onCriticalFailure(error.what()); + commandSession->start(); } -void Controller::disposeUI() noexcept +void Controller::createWindow(CommandType commandType) { - if (window) { - window->disconnect(); - window->forceClose(); - window->deleteLater(); - window = nullptr; - } + window = WebEidUI::createAndShowDialog(commandType); + connect(this, &Controller::statusUpdate, window, &WebEidUI::onSmartCardStatusUpdate); + connect(this, &Controller::retry, window, &WebEidUI::onRetry); + connect(window, &WebEidUI::destroyed, this, [this] { window = nullptr; }); } -void Controller::onConfirmCommandHandler(const EidCertificateAndPinInfo& certAndPinInfo) noexcept +void Controller::onCommandSessionCompleted(const QVariantMap& res) noexcept try { - emit stopCardEventMonitorThread(); - - auto* commandHandlerConfirmThread = - new CommandHandlerConfirmThread(this, *commandHandler, window, certAndPinInfo); - connect(commandHandlerConfirmThread, &CommandHandlerConfirmThread::completed, this, - &Controller::onCommandHandlerConfirmCompleted); - connectRetry(commandHandlerConfirmThread); - - commandHandlerConfirmThread->start(); - + _result = res; + writeResult(res, commandType()); + exit(); } catch (const std::exception& error) { onCriticalFailure(error.what()); } -void Controller::onCommandHandlerConfirmCompleted(const QVariantMap& res) noexcept +void Controller::onCommandSessionCancelled() noexcept try { - qDebug() << "Command completed"; - - _result = res; - writeResponseToStdOut(isInStdinMode, res, commandHandler->commandType()); - + _result = makeErrorObject(RESP_USER_CANCEL, QStringLiteral("User cancelled")); + writeResult(_result, commandType()); exit(); -} catch (const std::exception& error) { - onCriticalFailure(error.what()); +} catch (const std::exception& e) { + onCriticalFailure(e.what()); } void Controller::onRetry() noexcept try { - // Dispose the UI, it will be re-created during next execution. + disposeCommandSession(); disposeUI(); - // Command handler signals are still connected, disconnect them so that they can be - // reconnected during next execution. - commandHandler->disconnect(); - // Before restarting, wait until child threads finish. - waitForChildThreads(); - startCommandExecution(); } catch (const std::exception& error) { onCriticalFailure(error.what()); } -void Controller::connectRetry(const ControllerChildThread* childThread) const -{ - REQUIRE_NON_NULL(childThread) - connect(childThread, &ControllerChildThread::failure, this, &Controller::onCriticalFailure); - connect(childThread, &ControllerChildThread::retry, this, &Controller::retry); - // This connection handles cancel events from PIN pad. - connect(childThread, &ControllerChildThread::cancel, this, &Controller::onDialogCancel); -} - -void Controller::onDialogOK(const EidCertificateAndPinInfo& certAndPinInfo) noexcept -{ - if (commandHandler) { - onConfirmCommandHandler(certAndPinInfo); - } else { - // This should not happen, and when it does, OK should be equivalent to cancel. - onDialogCancel(); - } -} - -void Controller::onDialogCancel() noexcept -try { - qDebug() << "User cancelled"; - _result = makeErrorObject(RESP_USER_CANCEL, QStringLiteral("User cancelled")); - writeResponseToStdOut(isInStdinMode, _result, commandType()); - exit(); -} catch (const std::exception& e) { - onCriticalFailure(e.what()); -} - void Controller::onCriticalFailure(const QString& error) noexcept try { - emit stopCardEventMonitorThread(); qCritical() << "Exiting due to command" << commandType() << "fatal error:" << error; _result = makeErrorObject(RESP_TECH_ERROR, QStringLiteral("Technical error, see application logs")); + disposeCommandSession(); disposeUI(); - if (qApp->isSafariExtensionContainingApp()) { - writeResponseToStdOut(isInStdinMode, _result, commandType()); + if (responseSink && qApp->isSafariExtensionContainingApp()) { + writeResult(_result, commandType()); } WebEidUI::showFatalError(); - if (!qApp->isSafariExtensionContainingApp()) { - // Write the error response to stdout after showing the fatal error dialog. Chrome will - // close the application immediately after this, so the UI dialog may not be visible to the - // user. - writeResponseToStdOut(isInStdinMode, _result, commandType()); + if (responseSink && !qApp->isSafariExtensionContainingApp()) { + // Write the error response after showing the fatal error dialog. Chrome closes the + // application immediately after this, so the dialog may not otherwise be visible. + writeResult(_result, commandType()); } exit(); } catch (const std::exception& e) { @@ -286,25 +174,43 @@ try { exit(); } +void Controller::disposeCommandSession() noexcept +{ + if (commandSession) { + commandSession->disconnect(); + commandSession->stop(); + auto* session = commandSession.release(); + session->deleteLater(); + } +} + +void Controller::disposeUI() noexcept +{ + if (window) { + window->disconnect(); + window->forceClose(); + window->deleteLater(); + window = nullptr; + } +} + void Controller::exit() noexcept { + disposeCommandSession(); disposeUI(); - waitForChildThreads(); emit quit(); } -void Controller::waitForChildThreads() noexcept +CommandType Controller::commandType() const noexcept { - for (auto* thread : findChildren()) { - qDebug() << "Interrupting thread" << uintptr_t(thread); - thread->disconnect(); - thread->requestInterruption(); - ControllerChildThread::waitForControllerNotify.wakeAll(); - thread->wait(); + if (commandSession) { + return commandSession->commandType(); } + return command ? command->first : CommandType(CommandType::INSERT_CARD); } -CommandType Controller::commandType() const noexcept +void Controller::writeResult(const QVariantMap& result, CommandType resultCommandType) { - return commandHandler ? commandHandler->commandType() : CommandType(CommandType::INSERT_CARD); + REQUIRE_NON_NULL(responseSink) + responseSink->writeResult(result, resultCommandType); } diff --git a/src/controller/controller.hpp b/src/controller/controller.hpp index 0464fe6a..1cae8b8f 100644 --- a/src/controller/controller.hpp +++ b/src/controller/controller.hpp @@ -22,9 +22,10 @@ #pragma once -#include "commandhandler.hpp" +#include "commandsession.hpp" +#include "responsesink.hpp" -class ControllerChildThread; +#include /** Controller coordinates the execution flow and interaction between all other components. */ class Controller : public QObject @@ -40,47 +41,32 @@ class Controller : public QObject void quit(); void retry(const RetriableError error); void statusUpdate(RetriableError status); - void stopCardEventMonitorThread(); public: // slots void run() noexcept; - // Called either directly from run() or from the monitor thread when cards are available. - void - onCardsAvailable(const std::vector& availableEids) noexcept; - - // Called when CommandHandlerRunThread finishes execution. - void onCertificatesLoaded() noexcept; - - // Called either directly from onDialogOK(). - void onConfirmCommandHandler(const EidCertificateAndPinInfo& certAndPinInfo) noexcept; - - // Called from CommandHandlerConfirm thread. - void onCommandHandlerConfirmCompleted(const QVariantMap& result) noexcept; - - // Called from the dialog when user chooses to retry errors that have occured in child threads. + // Called when CommandSession needs a fresh UI/session for the same command. void onRetry() noexcept; - // User events from the dialog. - void onDialogOK(const EidCertificateAndPinInfo& certAndPinInfo) noexcept; - void onDialogCancel() noexcept; - // Failure handler, reports the error and quits the application. void onCriticalFailure(const QString& error) noexcept; private: + void initializeResponseSink(); void startCommandExecution(); - void connectRetry(const ControllerChildThread* childThread) const; - void createWindow(); + void createWindow(CommandType commandType); + void onCommandSessionCompleted(const QVariantMap& result) noexcept; + void onCommandSessionCancelled() noexcept; + void disposeCommandSession() noexcept; void disposeUI() noexcept; void exit() noexcept; - void waitForChildThreads() noexcept; CommandType commandType() const noexcept; + void writeResult(const QVariantMap& result, CommandType resultCommandType); CommandWithArgumentsPtr command; - CommandHandler::ptr commandHandler; + std::unique_ptr responseSink; + std::unique_ptr commandSession; // As the Qt::WA_DeleteOnClose flag is set, the dialog is deleted automatically. observer_ptr window = nullptr; QVariantMap _result; - bool isInStdinMode = true; }; diff --git a/src/controller/nativemessagingsession.cpp b/src/controller/nativemessagingsession.cpp new file mode 100644 index 00000000..1138a507 --- /dev/null +++ b/src/controller/nativemessagingsession.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020-2024 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "nativemessagingsession.hpp" + +#include "inputoutputmode.hpp" +#include "writeresponse.hpp" + +#include + +void NativeMessagingSession::writeHandshake() +{ + writeResponseToStdOut( + true, {{QStringLiteral("version"), QCoreApplication::applicationVersion()}}, "get-version"); +} + +CommandWithArgumentsPtr NativeMessagingSession::readCommand() const +{ + return readCommandFromStdin(); +} + +void NativeMessagingSession::writeResult(const QVariantMap& result, CommandType commandType) +{ + writeResponseToStdOut(true, result, commandType); +} diff --git a/src/controller/nativemessagingsession.hpp b/src/controller/nativemessagingsession.hpp new file mode 100644 index 00000000..6ce6f637 --- /dev/null +++ b/src/controller/nativemessagingsession.hpp @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020-2024 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "responsesink.hpp" + +class NativeMessagingSession final : public ResponseSink +{ +public: + void writeHandshake(); + CommandWithArgumentsPtr readCommand() const; + void writeResult(const QVariantMap& result, CommandType commandType) override; +}; diff --git a/src/controller/responsesink.cpp b/src/controller/responsesink.cpp new file mode 100644 index 00000000..2203f1f1 --- /dev/null +++ b/src/controller/responsesink.cpp @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020-2024 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "responsesink.hpp" + +#include "writeresponse.hpp" + +void CommandLineResponseSink::writeResult(const QVariantMap& result, CommandType commandType) +{ + writeResponseToStdOut(false, result, commandType); +} diff --git a/src/controller/responsesink.hpp b/src/controller/responsesink.hpp new file mode 100644 index 00000000..d88ba0df --- /dev/null +++ b/src/controller/responsesink.hpp @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020-2024 Estonian Information System Authority + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "commands.hpp" + +class ResponseSink +{ +public: + virtual ~ResponseSink() = default; + + virtual void writeResult(const QVariantMap& result, CommandType commandType) = 0; +}; + +class CommandLineResponseSink final : public ResponseSink +{ +public: + void writeResult(const QVariantMap& result, CommandType commandType) override; +};