diff --git a/CMakeLists.txt b/CMakeLists.txt index 18fe66f..5c50cd4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ project(CLIFp # Get helper scripts include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchOBCMake.cmake) -fetch_ob_cmake("v0.3.3") +fetch_ob_cmake("928cbafc2036f97cfaeb50cd892209b6e1037b6d") # Initialize project according to standard rules include(OB/Project) @@ -72,7 +72,7 @@ endif() include(OB/FetchQx) ob_fetch_qx( - REF "2bbf83e59b0aadc3891440193892be1a2a19c00e" + REF "fdcb2e56532c74588a6774371b9910c00cf8db06" COMPONENTS ${CLIFP_QX_COMPONENTS} ) diff --git a/README.md b/README.md index eb67ab7..9b32315 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ If the application needs to use files from a Data Pack that pack will need to be The applications and arguments that are used for each game/animation can be found within the Flashpoint database ([FP Install Dir]\Data\flashpoint.sqlite) ### Flashpoint Protocol + CLIFp supports the "flashpoint" protocol, which means it can launch titles through URL with a custom scheme, followed by a title's UUID, like this: flashpoint://37e5c215-9c39-4a3d-9912-b4343a17027e @@ -135,6 +136,12 @@ If for whatever reason the service through which you wish to share a link does n > [!IMPORTANT] > You will want to disable the "Register As Protocol Handler" option in the default launcher or else it will replace CLIFp as the "flashpoint" protocol handler every time it's started. +### Companion Mode + +It is recommended to only use CLIFp when the regular launcher isn't running as it allows fully independent operation since it can start and stop required services on its own; however, CLIFp can be started while the standard launcher is running, in which case it will run in "Companion Mode" and utilize the launcher's services instead. + +The catch with this mode is that CLIFp will be required to shutdown if at any point the standard launcher is closed. + ## All Commands/Options Most options have short and long forms, which are interchangeable. For options that take a value, a space or **=** can be used between the option and its value, i.e. diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 14cb167..2bd230b 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -57,9 +57,6 @@ set(CLIFP_SOURCE tools/mounter_qmp.cpp tools/mounter_router.h tools/mounter_router.cpp - tools/processbider_p.h - tools/processbider.h - tools/processbider.cpp frontend/message.h frontend/statusrelay.h frontend/statusrelay.cpp @@ -93,7 +90,6 @@ if(CMAKE_SYSTEM_NAME STREQUAL Windows) task/t-exec_win.cpp task/t-bideprocess.h task/t-bideprocess.cpp - tools/processbider_p_win.cpp ) list(APPEND CLIFP_LINKS @@ -108,7 +104,6 @@ if(CMAKE_SYSTEM_NAME STREQUAL Linux) task/t-awaitdocker.h task/t-awaitdocker.cpp task/t-exec_linux.cpp - tools/processbider_p_linux.cpp ) list(APPEND CLIFP_LINKS PRIVATE diff --git a/app/src/command/c-play.cpp b/app/src/command/c-play.cpp index 3e1879b..583cb11 100644 --- a/app/src/command/c-play.cpp +++ b/app/src/command/c-play.cpp @@ -330,3 +330,6 @@ Qx::Error CPlay::perform() // Handle entry return std::visit([this](auto arg) { return this->handleEntry(arg); }, entry); } + +//Public: +bool CPlay::requiresServices() const { return true; } diff --git a/app/src/command/c-play.h b/app/src/command/c-play.h index 49f5d54..b50813d 100644 --- a/app/src/command/c-play.h +++ b/app/src/command/c-play.h @@ -103,6 +103,9 @@ class CPlay : public TitleCommand QList options() override; QString name() override; Qx::Error perform() override; + +public: + bool requiresServices() const override; }; REGISTER_COMMAND(CPlay::NAME, CPlay, CPlay::DESCRIPTION); diff --git a/app/src/command/c-prepare.cpp b/app/src/command/c-prepare.cpp index 22df190..4993cd8 100644 --- a/app/src/command/c-prepare.cpp +++ b/app/src/command/c-prepare.cpp @@ -45,3 +45,6 @@ Qx::Error CPrepare::perform() // Return success return Qx::Error(); } + +//Public: +bool CPrepare::requiresServices() const { return true; } diff --git a/app/src/command/c-prepare.h b/app/src/command/c-prepare.h index 75e62f8..bd28800 100644 --- a/app/src/command/c-prepare.h +++ b/app/src/command/c-prepare.h @@ -31,6 +31,9 @@ class CPrepare : public TitleCommand QList options() override; QString name() override; Qx::Error perform() override; + +public: + bool requiresServices() const override; }; REGISTER_COMMAND(CPrepare::NAME, CPrepare, CPrepare::DESCRIPTION); diff --git a/app/src/command/c-run.cpp b/app/src/command/c-run.cpp index 139d09f..b31e083 100644 --- a/app/src/command/c-run.cpp +++ b/app/src/command/c-run.cpp @@ -71,3 +71,6 @@ Qx::Error CRun::perform() // Return success return CRunError(); } + +//Public: +bool CRun::requiresServices() const { return true; } diff --git a/app/src/command/c-run.h b/app/src/command/c-run.h index 4cfba5c..5deded8 100644 --- a/app/src/command/c-run.h +++ b/app/src/command/c-run.h @@ -82,6 +82,9 @@ class CRun : public Command QSet requiredOptions() override; QString name() override; Qx::Error perform() override; + +public: + bool requiresServices() const override; }; REGISTER_COMMAND(CRun::NAME, CRun, CRun::DESCRIPTION); diff --git a/app/src/command/command.cpp b/app/src/command/command.cpp index 010fd9a..e66b58c 100644 --- a/app/src/command/command.cpp +++ b/app/src/command/command.cpp @@ -177,6 +177,7 @@ QSet Command::requiredOptions() { return {}; } //Public: bool Command::requiresFlashpoint() const { return true; } +bool Command::requiresServices() const { return false; } bool Command::autoBlockNewInstances() const { return true; } Qx::Error Command::process(const QStringList& commandLine) diff --git a/app/src/command/command.h b/app/src/command/command.h index 79a9169..d539acc 100644 --- a/app/src/command/command.h +++ b/app/src/command/command.h @@ -163,6 +163,7 @@ class Command public: virtual bool requiresFlashpoint() const; + virtual bool requiresServices() const; virtual bool autoBlockNewInstances() const; Qx::Error process(const QStringList& commandLine); }; diff --git a/app/src/kernel/core.cpp b/app/src/kernel/core.cpp index 89b0fe6..0ceef65 100644 --- a/app/src/kernel/core.cpp +++ b/app/src/kernel/core.cpp @@ -61,7 +61,8 @@ Core::Core(QObject* parent) : QObject(parent), mCriticalErrorOccurred(false), mStatusHeading(u"Initializing"_s), - mStatusMessage(u"..."_s) + mStatusMessage(u"..."_s), + mServicesMode(ServicesMode::Standalone) { establishCanonCore(*this); // Ignore return value as there should never be more than one Core with current design } @@ -342,50 +343,78 @@ Qx::Error Core::initialize(QStringList& commandLine) logEvent(NAME, LOG_EVENT_GLOBAL_OPT.arg(globalOptions)); // Check for valid arguments - if(validArgs) + if(!validArgs) { - // Handle each global option - mNotificationVerbosity = clParser.isSet(CL_OPTION_SILENT) ? NotificationVerbosity::Silent : + commandLine.clear(); // Clear remaining options since they are now irrelevant + showHelp(); + + CoreError err(CoreError::InvalidOptions, clParser.errorText()); + postError(NAME, err); + return err; + } + + // Handle each global option + mNotificationVerbosity = clParser.isSet(CL_OPTION_SILENT) ? NotificationVerbosity::Silent : clParser.isSet(CL_OPTION_QUIET) ? NotificationVerbosity::Quiet : NotificationVerbosity::Full; - logEvent(NAME, LOG_EVENT_NOTIFCATION_LEVEL.arg(ENUM_NAME(mNotificationVerbosity))); + logEvent(NAME, LOG_EVENT_NOTIFCATION_LEVEL.arg(ENUM_NAME(mNotificationVerbosity))); - if(clParser.isSet(CL_OPTION_VERSION)) - { - showVersion(); - commandLine.clear(); // Clear args so application terminates after Core setup - logEvent(NAME, LOG_EVENT_VER_SHOWN); - } - else if(clParser.isSet(CL_OPTION_HELP) || (!isActionableOptionSet(clParser) && clParser.positionalArguments().count() == 0)) // Also when no parameters + if(clParser.isSet(CL_OPTION_VERSION)) + { + showVersion(); + commandLine.clear(); // Clear args so application terminates after Core setup + logEvent(NAME, LOG_EVENT_VER_SHOWN); + } + else if(clParser.isSet(CL_OPTION_HELP) || (!isActionableOptionSet(clParser) && clParser.positionalArguments().count() == 0)) // Also when no parameters + { + showHelp(); + commandLine.clear(); // Clear args so application terminates after Core setup + logEvent(NAME, LOG_EVENT_G_HELP_SHOWN); + } + else + { + QStringList pArgs = clParser.positionalArguments(); + if(pArgs.count() == 1 && pArgs.front().startsWith(FLASHPOINT_PROTOCOL_SCHEME)) { - showHelp(); - commandLine.clear(); // Clear args so application terminates after Core setup - logEvent(NAME, LOG_EVENT_G_HELP_SHOWN); + logEvent(NAME, LOG_EVENT_PROTOCOL_FORWARD); + commandLine = {"play", "-u", pArgs.front()}; } else - { - QStringList pArgs = clParser.positionalArguments(); - if(pArgs.count() == 1 && pArgs.front().startsWith(FLASHPOINT_PROTOCOL_SCHEME)) - { - logEvent(NAME, LOG_EVENT_PROTOCOL_FORWARD); - commandLine = {"play", "-u", pArgs.front()}; - } - else - commandLine = pArgs; // Remove core options from command line list - } - - // Return success - return CoreError(); + commandLine = pArgs; // Remove core options from command line list } - else - { - commandLine.clear(); // Clear remaining options since they are now irrelevant - showHelp(); - CoreError err(CoreError::InvalidOptions, clParser.errorText()); + // Return success + return CoreError(); +} + +void Core::setServicesMode(ServicesMode mode) +{ + logEvent(NAME, LOG_EVENT_MODE_SET.arg(ENUM_NAME(mode))); + mServicesMode = mode; + + if(mode == ServicesMode::Companion) + watchLauncher(); +} + +void Core::watchLauncher() +{ + logEvent(NAME, LOG_EVENT_LAUNCHER_WATCH); + + using namespace std::chrono_literals; + mLauncherWatcher.setProcessName(Fp::Install::LAUNCHER_NAME); +#ifdef __linux__ + mLauncherWatcher.setPollRate(1s); // Generous rate since while we need to know quickly, we don't THAT quickly +#endif + connect(&mLauncherWatcher, &Qx::ProcessBider::errorOccurred, this, [this](Qx::ProcessBiderError err){ + logError(NAME, err); + }); + connect(&mLauncherWatcher, &Qx::ProcessBider::finished, this, [this]{ + // Launcher closed (or can't be hooked), need to bail + CoreError err(CoreError::CompanionModeLauncherClose); postError(NAME, err); - return err; - } + emit abort(err); + }); + mLauncherWatcher.start(); } void Core::attachFlashpoint(std::unique_ptr flashpointInstall) @@ -491,6 +520,13 @@ bool Core::blockNewInstances() CoreError Core::enqueueStartupTasks(const QString& serverOverride) { logEvent(NAME, LOG_EVENT_ENQ_START); + + if(mServicesMode == ServicesMode::Companion) + { + logEvent(NAME, LOG_EVENT_SERVICES_FROM_LAUNCHER); + // TODO: Allegedly apache and php are going away at some point so this hopefully isn't needed for long + return !serverOverride.isEmpty() ? CoreError(CoreError::CompanionModeServerOverride) : CoreError(); + } #ifdef __linux__ /* On Linux X11 Server needs to be temporarily be set to allow connections from root for docker, @@ -607,6 +643,13 @@ CoreError Core::enqueueStartupTasks(const QString& serverOverride) void Core::enqueueShutdownTasks() { logEvent(NAME, LOG_EVENT_ENQ_STOP); + + if(mServicesMode == ServicesMode::Companion) + { + logEvent(NAME, LOG_EVENT_SERVICES_FROM_LAUNCHER); + return; + } + // Add Stop entries from services for(const Fp::StartStop& stopEntry : qxAsConst(mFlashpointInstall->services().stop)) { diff --git a/app/src/kernel/core.h b/app/src/kernel/core.h index 4a53728..bcf6e20 100644 --- a/app/src/kernel/core.h +++ b/app/src/kernel/core.h @@ -15,6 +15,7 @@ // Qx Includes #include +#include #include // libfp Includes @@ -31,12 +32,14 @@ using ErrorCode = quint32; class QX_ERROR_TYPE(CoreError, "CoreError", 1200) { friend class Core; - //-Class Enums------------------------------------------------------------- +//-Class Enums------------------------------------------------------------- public: enum Type { NoError, InternalError, + CompanionModeLauncherClose, + CompanionModeServerOverride, InvalidOptions, TitleNotFound, TooManyResults, @@ -44,11 +47,13 @@ class QX_ERROR_TYPE(CoreError, "CoreError", 1200) UnknownDatapackParam }; - //-Class Variables------------------------------------------------------------- +//-Class Variables------------------------------------------------------------- private: static inline const QHash ERR_STRINGS{ {NoError, u""_s}, {InternalError, u"Internal error."_s}, + {CompanionModeLauncherClose, u"The standard launcher was closed while in companion mode."_s}, + {CompanionModeServerOverride, u"Cannot enact game server override in companion mode."_s}, {InvalidOptions, u"Invalid global options provided."_s}, {TitleNotFound, u"Could not find the title in the Flashpoint database."_s}, {TooManyResults, u"More results than can be presented were returned in a search."_s}, @@ -56,17 +61,17 @@ class QX_ERROR_TYPE(CoreError, "CoreError", 1200) {UnknownDatapackParam, u"Unrecognized datapack parameters were present. The game likely won't work correctly."_s}, }; - //-Instance Variables------------------------------------------------------------- +//-Instance Variables------------------------------------------------------------- private: Type mType; QString mSpecific; Qx::Severity mSeverity; - //-Constructor------------------------------------------------------------- +//-Constructor------------------------------------------------------------- private: CoreError(Type t = NoError, const QString& s = {}, Qx::Severity sv = Qx::Critical); - //-Instance Functions------------------------------------------------------------- +//-Instance Functions------------------------------------------------------------- public: bool isValid() const; Type type() const; @@ -85,6 +90,7 @@ class Core : public QObject //-Class Enums----------------------------------------------------------------------- public: enum class NotificationVerbosity { Full, Quiet, Silent }; + enum ServicesMode { Standalone, Companion }; //-Class Structs--------------------------------------------------------------------- public: @@ -154,6 +160,7 @@ class Core : public QObject // Logging - Messages static inline const QString LOG_EVENT_INIT = u"Initializing CLIFp..."_s; + static inline const QString LOG_EVENT_MODE_SET = u"Services mode set: %1"_s; static inline const QString LOG_EVENT_GLOBAL_OPT = u"Global Options: %1"_s; static inline const QString LOG_EVENT_FURTHER_INSTANCE_BLOCK_SUCC = u"Successfully locked standard instance count..."_s; static inline const QString LOG_EVENT_FURTHER_INSTANCE_BLOCK_FAIL = u"Failed to lock standard instance count"_s; @@ -175,6 +182,8 @@ class Core : public QObject static inline const QString LOG_EVENT_DATA_PACK_ALREADY_EXTRACTED = u"Extracted files already present"_s; static inline const QString LOG_EVENT_TASK_ENQ = u"Enqueued %1: {%2}"_s; static inline const QString LOG_EVENT_APP_PATH_ALT = u"App path \"%1\" maps to alternative \"%2\"."_s; + static inline const QString LOG_EVENT_SERVICES_FROM_LAUNCHER = u"Using services from standard Launcher due to companion mode."_s; + static inline const QString LOG_EVENT_LAUNCHER_WATCH = u"Starting bide on Launcher process..."_s; // Logging - Title Search static inline const QString LOG_EVENT_GAME_SEARCH = u"Searching for game with title '%1'"_s; @@ -252,6 +261,7 @@ class Core : public QObject std::unique_ptr mLogger; // Processing + ServicesMode mServicesMode; bool mCriticalErrorOccurred; NotificationVerbosity mNotificationVerbosity; std::queue mTaskQueue; @@ -262,6 +272,7 @@ class Core : public QObject // Other QProcessEnvironment mChildTitleProcEnv; + Qx::ProcessBider mLauncherWatcher; //-Constructor---------------------------------------------------------------------------------------------------------- public: @@ -286,6 +297,8 @@ class Core : public QObject public: // Setup Qx::Error initialize(QStringList& commandLine); + void setServicesMode(ServicesMode mode = ServicesMode::Standalone); + void watchLauncher(); void attachFlashpoint(std::unique_ptr flashpointInstall); // Helper (TODO: Move some of these to libfp Toolkit) @@ -324,6 +337,7 @@ class Core : public QObject bool requestQuestionAnswer(const QString& question); // Member access + ServicesMode mode() const; Fp::Install& fpInstall(); const QProcessEnvironment& childTitleProcessEnvironment(); NotificationVerbosity notifcationVerbosity() const; @@ -351,6 +365,9 @@ class Core : public QObject void message(const Message& message); void clipboardUpdateRequested(const QString& text); void questionAnswerRequested(QSharedPointer response, const QString& question); + + // Driver specific + void abort(CoreError err); }; //-Metatype Declarations----------------------------------------------------------------------------------------- diff --git a/app/src/kernel/driver.cpp b/app/src/kernel/driver.cpp index 3d75545..d23ba66 100644 --- a/app/src/kernel/driver.cpp +++ b/app/src/kernel/driver.cpp @@ -1,6 +1,9 @@ // Unit Include #include "driver.h" +// Qt Includes +#include + // Qx Includes #include #include @@ -65,6 +68,11 @@ void Driver::init() connect(mCore, &Core::itemSelectionRequested, this, &Driver::itemSelectionRequested); connect(mCore, &Core::clipboardUpdateRequested, this, &Driver::clipboardUpdateRequested); connect(mCore, &Core::questionAnswerRequested, this, &Driver::questionAnswerRequested); + connect(mCore, &Core::abort, this, [this](CoreError err){ + mCore->logEvent(NAME, LOG_EVENT_CORE_ABORT); + mErrorStatus = err; + quit(); + }); //-Setup deferred process manager------ /* NOTE: It looks like the manager should just be a stack member of TExec that is constructed @@ -166,6 +174,15 @@ void Driver::finish() emit finished(mCore->logFinish(NAME, mErrorStatus.value())); } +void Driver::quit() +{ + mQuitRequested = true; + + // Stop current task (assuming it can be) + if(mCurrentTask) + mCurrentTask->stop(); +} + // Helper functions std::unique_ptr Driver::findFlashpointInstall() { @@ -257,6 +274,12 @@ void Driver::drive() // Create command instance std::unique_ptr commandProcessor = Command::acquire(commandStr, *mCore); + //-Set Service Mode-------------------------------------------------------------------- + + // Check state of standard launcher + bool launcherRunning = Qx::processIsRunning(Fp::Install::LAUNCHER_NAME); + mCore->setServicesMode(launcherRunning && commandProcessor->requiresServices() ? Core::Companion : Core::Standalone); + //-Restrict app to only one instance--------------------------------------------------- if(commandProcessor->autoBlockNewInstances() && !mCore->blockNewInstances()) { @@ -267,19 +290,9 @@ void Driver::drive() return; } - //-Handle Flashpoint Steps---------------------------------------------------------- + //-Get Flashpoint Install------------------------------------------------------------- if(commandProcessor->requiresFlashpoint()) { - // Ensure Flashpoint Launcher isn't running - if(Qx::processIsRunning(Fp::Install::LAUNCHER_NAME)) - { - DriverError err(DriverError::LauncherRunning, ERR_LAUNCHER_RUNNING_TIP); - mCore->postError(NAME, err); - mErrorStatus = err; - finish(); - return; - } - // Find and link to Flashpoint Install std::unique_ptr flashpointInstall; mCore->logEvent(NAME, LOG_EVENT_FLASHPOINT_SEARCH); @@ -298,6 +311,15 @@ void Driver::drive() mCore->attachFlashpoint(std::move(flashpointInstall)); } + //-Catch early core errors------------------------------------------------------------------- + QThread::msleep(100); + QApplication::processEvents(); + if(mErrorStatus.isSet()) + { + finish(); + return; + } + //-Process command----------------------------------------------------------------------------- mErrorStatus = commandProcessor->process(mArguments); if(mErrorStatus.isSet()) @@ -333,10 +355,6 @@ void Driver::quitNow() return; } - mQuitRequested = true; mCore->logEvent(NAME, LOG_EVENT_QUIT_REQUEST); - - // Stop current task (assuming it can be) - if(mCurrentTask) - mCurrentTask->stop(); + quit(); } diff --git a/app/src/kernel/driver.h b/app/src/kernel/driver.h index b9b22e8..044c64f 100644 --- a/app/src/kernel/driver.h +++ b/app/src/kernel/driver.h @@ -21,7 +21,6 @@ class QX_ERROR_TYPE(DriverError, "DriverError", 1201) { NoError, AlreadyOpen, - LauncherRunning, InvalidInstall, }; @@ -30,7 +29,6 @@ class QX_ERROR_TYPE(DriverError, "DriverError", 1201) static inline const QHash ERR_STRINGS{ {NoError, u""_s}, {AlreadyOpen, u"Only one instance of CLIFp can be used at a time!"_s}, - {LauncherRunning, u"The CLI cannot be used while the Flashpoint Launcher is running."_s}, {InvalidInstall, u"CLIFp does not appear to be deployed in a valid Flashpoint install"_s} }; @@ -62,7 +60,6 @@ class Driver : public QObject //-Class Variables------------------------------------------------------------------------------------------------------ private: // Error Messages - static inline const QString ERR_LAUNCHER_RUNNING_TIP = u"Please close the Launcher first."_s; static inline const QString ERR_INSTALL_INVALID_TIP = u"You may need to update (i.e. the 'update' command)."_s; // Logging @@ -84,6 +81,7 @@ class Driver : public QObject static inline const QString LOG_EVENT_QUIT_REQUEST = u"Received quit request"_s; static inline const QString LOG_EVENT_QUIT_REQUEST_REDUNDANT = u"Received redundant quit request"_s; static inline const QString LOG_EVENT_CLEARED_UPDATE_CACHE = u"Cleared stale update cache."_s; + static inline const QString LOG_EVENT_CORE_ABORT = u"Core abort signaled, quitting now."_s; // Meta static inline const QString NAME = u"driver"_s; @@ -126,6 +124,7 @@ class Driver : public QObject void cleanup(); void finish(); + void quit(); // Helper std::unique_ptr findFlashpointInstall(); diff --git a/app/src/task/t-bideprocess.cpp b/app/src/task/t-bideprocess.cpp index bb80fbd..5a87aa2 100644 --- a/app/src/task/t-bideprocess.cpp +++ b/app/src/task/t-bideprocess.cpp @@ -1,6 +1,29 @@ // Unit Include #include "t-bideprocess.h" +//=============================================================================================================== +// TBideProcessError +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Private: +TBideProcessError::TBideProcessError(const QString& pn, Type t) : + mType(t), + mProcessName(pn) +{} + +//-Instance Functions------------------------------------------------------------- +//Public: +bool TBideProcessError::isValid() const { return mType != NoError; } +TBideProcessError::Type TBideProcessError::type() const { return mType; } +QString TBideProcessError::processName() const { return mProcessName; } + +//Private: +Qx::Severity TBideProcessError::deriveSeverity() const { return Qx::Critical; } +quint32 TBideProcessError::deriveValue() const { return mType; } +QString TBideProcessError::derivePrimary() const { return ERR_STRINGS.value(mType); } +QString TBideProcessError::deriveSecondary() const { return mProcessName; } + //=============================================================================================================== // TBideProcess //=============================================================================================================== @@ -8,18 +31,26 @@ //-Constructor-------------------------------------------------------------------- //Public: TBideProcess::TBideProcess(QObject* parent) : - Task(parent), - mProcessBider(nullptr) + Task(parent) { // Setup bider - mProcessBider.setRespawnGrace(STANDARD_GRACE); - connect(&mProcessBider, &ProcessBider::statusChanged, this, [this](QString statusMessage){ - emit eventOccurred(NAME, statusMessage); + using namespace std::chrono_literals; + static const auto grace = 2s; + mProcessBider.setRespawnGrace(grace); + connect(&mProcessBider, &Qx::ProcessBider::processStopped, this, [this]{ + emit eventOccurred(NAME, LOG_EVENT_BIDE_GRACE.arg(QString::number(grace.count()), mProcessName)); + }); + connect(&mProcessBider, &Qx::ProcessBider::established, this, [this]{ + emit eventOccurred(NAME, LOG_EVENT_BIDE_RUNNING.arg(mProcessName)); + emit eventOccurred(NAME, LOG_EVENT_BIDE_ON.arg(mProcessName)); }); - connect(&mProcessBider, &ProcessBider::errorOccurred, this, [this](ProcessBiderError errorMessage){ - emit errorOccurred(NAME, errorMessage); + connect(&mProcessBider, &Qx::ProcessBider::processStopped, this, [this]{ + emit eventOccurred(NAME, LOG_EVENT_BIDE_QUIT.arg(mProcessName)); }); - connect(&mProcessBider, &ProcessBider::bideFinished, this, &TBideProcess::postBide); + connect(&mProcessBider, &Qx::ProcessBider::errorOccurred, this, [this](Qx::ProcessBiderError err){ + emit errorOccurred(NAME, err); + }); + connect(&mProcessBider, &Qx::ProcessBider::finished, this, &TBideProcess::postBide); } //-Instance Functions------------------------------------------------------------- @@ -45,17 +76,22 @@ void TBideProcess::perform() void TBideProcess::stop() { - if(mProcessBider.isRunning()) + if(mProcessBider.isBiding()) { emit eventOccurred(NAME, LOG_EVENT_STOPPING_BIDE_PROCESS); - if(ProcessBiderError err = mProcessBider.closeProcess(); err.isValid()) - emit errorOccurred(NAME, err); + mProcessBider.closeProcess(); } } //-Signals & Slots------------------------------------------------------------------------------------------------------- //Private Slots: -void TBideProcess::postBide(Qx::Error errorStatus) +void TBideProcess::postBide(Qx::ProcessBider::ResultType type) { - emit complete(errorStatus); + if(type == Qx::ProcessBider::Fail)\ + emit complete(TBideProcessError(mProcessName, TBideProcessError::BideFail)); + else + { + emit eventOccurred(NAME, LOG_EVENT_BIDE_FINISHED.arg(mProcessName)); + emit complete(TBideProcessError()); + } } diff --git a/app/src/task/t-bideprocess.h b/app/src/task/t-bideprocess.h index d1b899d..9c4e6f9 100644 --- a/app/src/task/t-bideprocess.h +++ b/app/src/task/t-bideprocess.h @@ -1,9 +1,51 @@ #ifndef TBIDEPROCESS_H #define TBIDEPROCESS_H +// Qx Includes +#include + // Project Includes #include "task/task.h" -#include "tools/processbider.h" + +class QX_ERROR_TYPE(TBideProcessError, "TBideError", 1256) +{ + friend class TBideProcess; +//-Class Enums------------------------------------------------------------- +public: + enum Type + { + NoError, + BideFail, + }; + +//-Class Variables------------------------------------------------------------- +private: + static inline const QHash ERR_STRINGS{ + {NoError, u""_s}, + {BideFail, u"Could not bide on process."_s} + }; + +//-Instance Variables------------------------------------------------------------- +private: + Type mType; + QString mProcessName; + +//-Constructor------------------------------------------------------------- +private: + TBideProcessError(const QString& pn = {}, Type t = NoError); + +//-Instance Functions------------------------------------------------------------- +public: + bool isValid() const; + Type type() const; + QString processName() const; + +private: + Qx::Severity deriveSeverity() const override; + quint32 deriveValue() const override; + QString derivePrimary() const override; + QString deriveSecondary() const override; +}; class TBideProcess : public Task { @@ -14,18 +56,20 @@ class TBideProcess : public Task static inline const QString NAME = u"TBideProcess"_s; // Logging + static inline const QString LOG_EVENT_BIDE_GRACE = u"Waiting %1 seconds for process %2 to be running"_s; + static inline const QString LOG_EVENT_BIDE_RUNNING = u"Wait-on process %1 is running"_s; + static inline const QString LOG_EVENT_BIDE_ON = u"Waiting for process %1 to finish"_s; + static inline const QString LOG_EVENT_BIDE_QUIT = u"Wait-on process %1 has finished"_s; + static inline const QString LOG_EVENT_BIDE_FINISHED = u"Wait-on process %1 was not running after the grace period"_s; static inline const QString LOG_EVENT_STOPPING_BIDE_PROCESS = u"Stopping current bide process..."_s; // Errors static inline const QString ERR_CANT_CLOSE_BIDE_PROCESS = u"Could not automatically end the running title! It will have to be closed manually."_s; - // Functional - static const uint STANDARD_GRACE = 2; // Seconds to allow the process to restart in cases it does - //-Instance Variables------------------------------------------------------------------------------------------------ private: // Functional - ProcessBider mProcessBider; + Qx::ProcessBider mProcessBider; // Data QString mProcessName; @@ -48,7 +92,7 @@ class TBideProcess : public Task //-Signals & Slots------------------------------------------------------------------------------------------------------- private slots: - void postBide(Qx::Error errorStatus); + void postBide(Qx::ProcessBider::ResultType type); }; #endif // TBIDEPROCESS_H diff --git a/app/src/tools/processbider.cpp b/app/src/tools/processbider.cpp deleted file mode 100644 index e5aa2a9..0000000 --- a/app/src/tools/processbider.cpp +++ /dev/null @@ -1,144 +0,0 @@ -// Unit Include -#include "processbider.h" -#include "processbider_p.h" - -// Qx Includes -#include - -//=============================================================================================================== -// ProcessBiderError -//=============================================================================================================== - -//-Constructor------------------------------------------------------------- -//Private: -ProcessBiderError::ProcessBiderError(Type t, const QString& s) : - mType(t), - mSpecific(s) -{} - -//-Instance Functions------------------------------------------------------------- -//Public: -bool ProcessBiderError::isValid() const { return mType != NoError; } -QString ProcessBiderError::specific() const { return mSpecific; } -ProcessBiderError::Type ProcessBiderError::type() const { return mType; } - -//Private: -Qx::Severity ProcessBiderError::deriveSeverity() const { return Qx::Warning; } -quint32 ProcessBiderError::deriveValue() const { return mType; } -QString ProcessBiderError::derivePrimary() const { return ERR_STRINGS.value(mType); } -QString ProcessBiderError::deriveSecondary() const { return mSpecific; } - -//=============================================================================================================== -// ProcessBider -//=============================================================================================================== - -//-Constructor------------------------------------------------------------- -//Public: -ProcessBider::ProcessBider(QObject* parent, const QString& name) : - QThread(parent), - mProcessName(name), - mRespawnGrace(30000), - mPollRate(500) -{} - -//-Instance Functions------------------------------------------------------------- -//Private: -ProcessBiderError ProcessBider::doWait() -{ - mWaiter = new ProcessWaiter(mProcessName); - mWaiter->setPollRate(mPollRate); - - // Block outer access until waiting - QWriteLocker writeLock(&mRWLock); - - // Wait until process has stopped running for grace period - quint32 procId; - do - { - // Yield for grace period - emit statusChanged(LOG_EVENT_BIDE_GRACE.arg(mRespawnGrace).arg(mProcessName)); - if(mRespawnGrace > 0) - QThread::sleep(mRespawnGrace); - - // Find process ID by name - procId = Qx::processId(mProcessName); - - // Check that process was found (is running) - if(procId) - { - emit statusChanged(LOG_EVENT_BIDE_RUNNING.arg(mProcessName)); - - // Attempt to wait on process to terminate - emit statusChanged(LOG_EVENT_BIDE_ON.arg(mProcessName)); - mWaiter->updateId(procId); - writeLock.unlock(); // To allow close attempts - if(!mWaiter->wait()) // Blocks until process ends - { - ProcessBiderError err(ProcessBiderError::Wait, mProcessName); - emit errorOccurred(err); - return err; - } - writeLock.relock(); - - emit statusChanged(LOG_EVENT_BIDE_QUIT.arg(mProcessName)); - } - } - while(procId); - - // Return success - emit statusChanged(LOG_EVENT_BIDE_FINISHED.arg(mProcessName)); - return ProcessBiderError(); -} - -void ProcessBider::run() -{ - ProcessBiderError status = doWait(); - qxDelete(mWaiter); - emit bideFinished(status); -} - -//Public: -void ProcessBider::setProcessName(const QString& name) -{ - mRWLock.lockForWrite(); - mProcessName = name; - mRWLock.unlock(); -} -void ProcessBider::setRespawnGrace(uint respawnGrace) -{ - mRWLock.lockForWrite(); - mRespawnGrace = respawnGrace; - mRWLock.unlock(); -} - -void ProcessBider::setPollRate(uint pollRate) -{ - mRWLock.lockForWrite(); - mPollRate = pollRate; - mRWLock.unlock(); -} - -/* TODO: Since this doesn't allow explicitly abandoning the wait, if for some reason the process opens itself over and over, - * it's still possible to get stuck in a dead lock even if the close is successful - */ -ProcessBiderError ProcessBider::closeProcess() -{ - QReadLocker readLocker(&mRWLock); - if(mWaiter && mWaiter->isWaiting() && !mWaiter->close()) - { - ProcessBiderError err(ProcessBiderError::Close, mProcessName); - emit errorOccurred(err); - return err; - } - - return ProcessBiderError(); -} - -//-Signals & Slots------------------------------------------------------------------------------------------------------------ -//Public Slots: -void ProcessBider::start() -{ - // Start new thread for waiting - if(!isRunning()) - QThread::start(); -} diff --git a/app/src/tools/processbider.h b/app/src/tools/processbider.h deleted file mode 100644 index 3595f20..0000000 --- a/app/src/tools/processbider.h +++ /dev/null @@ -1,129 +0,0 @@ -#ifndef PROCESSWAITER_H -#define PROCESSWAITER_H - -// Qt Includes -#include -#include - -// Qx Includes -#include -#include - -/* This uses the approach of sub-classing QThread instead of the worker/object model. This means that by default there is no event - * loop running in the new thread (not needed with current setup), and that only the contents of run() take place in the new thread, - * with everything else happening in the thread that contains the instance of this class. - * - * This does mean that a Mutex must be used to protected against access to the same data between threads where necessary, but in - * this case that is desirable as when the parent thread calls `closeProcess()` we want it to get blocked if the run() thread is - * busy doing work until it goes back to waiting (or the wait process stops on its own), since we want to make sure that the wait - * process has ended before Driver takes its next steps. - * - * If for whatever reason the worker/object approach is desired in the future, an alternative to achieve the same thing would be to - * use a QueuedBlocking connection between the signal emitted by the worker instance method `endProcess()` (which acts as the - * interface to the object) and the slot in the object instance; however, in order for the object to be able to process the quit - * signal it cannot be sitting there blocked by WaitOnSingleObject (since it's now that threads responsibility to perform the quit - * instead of the thread that emitted the signal), so instead RegisterWaitForSingleObject would have to be used, which may have - * caveats since a thread spawned by the OS is used to trigger the specified callback function. It would potentially be safe if that - * callback function simply emits an internal signal with a queued connection that then triggers the thread managing the object to - * handle the quit upon its next event loop cycle. - * - * NOTE: Technically the thread synchronization here is imperfect as a blocked closeProcess() is unlocked one step before the wait - * actually starts so there could be a race between that and the waiter being marked as "in wait". Not a great way to avoid this though - * since the lock can't be unlocked any later since the waiting thread deadlocks once the wait is started. - */ - -class QX_ERROR_TYPE(ProcessBiderError, "ProcessBiderError", 1235) -{ - friend class ProcessBider; - //-Class Enums------------------------------------------------------------- -public: - enum Type - { - NoError = 0, - Wait = 1, - Close = 2 - }; - - //-Class Variables------------------------------------------------------------- -private: - static inline const QHash ERR_STRINGS{ - {NoError, u""_s}, - {Wait, u"Could not setup a wait on the process."_s}, - {Close, u"Could not close the wait on process."_s}, - }; - - //-Instance Variables------------------------------------------------------------- -private: - Type mType; - QString mSpecific; - - //-Constructor------------------------------------------------------------- -private: - ProcessBiderError(Type t = NoError, const QString& s = {}); - - //-Instance Functions------------------------------------------------------------- -public: - bool isValid() const; - Type type() const; - QString specific() const; - -private: - Qx::Severity deriveSeverity() const override; - quint32 deriveValue() const override; - QString derivePrimary() const override; - QString deriveSecondary() const override; -}; - -class ProcessWaiter; - -class ProcessBider : public QThread -{ - Q_OBJECT -//-Class Variables------------------------------------------------------------------------------------------------------ -private: - // Status Messages - static inline const QString LOG_EVENT_BIDE_GRACE = u"Waiting %1 seconds for process %2 to be running"_s; - static inline const QString LOG_EVENT_BIDE_RUNNING = u"Wait-on process %1 is running"_s; - static inline const QString LOG_EVENT_BIDE_ON = u"Waiting for process %1 to finish"_s; - static inline const QString LOG_EVENT_BIDE_QUIT = u"Wait-on process %1 has finished"_s; - static inline const QString LOG_EVENT_BIDE_FINISHED = u"Wait-on process %1 was not running after the grace period"_s; - -//-Instance Variables------------------------------------------------------------------------------------------------------------ -private: - // Work - ProcessWaiter* mWaiter; - QReadWriteLock mRWLock; - - // Process Info - QString mProcessName; - uint mRespawnGrace; - uint mPollRate; - -//-Constructor------------------------------------------------------------------------------------------------- -public: - ProcessBider(QObject* parent = nullptr, const QString& name = {}); - -//-Instance Functions--------------------------------------------------------------------------------------------------------- -private: - // Run in wait thread - ProcessBiderError doWait(); - void run() override; - -public: - // Run in external thread - void setProcessName(const QString& name); - void setRespawnGrace(uint respawnGrace); - void setPollRate(uint pollRate); // Ignored on Windows - ProcessBiderError closeProcess(); - -//-Signals & Slots------------------------------------------------------------------------------------------------------------ -public slots: - void start(); - -signals: - void statusChanged(QString statusMessage); - void errorOccurred(ProcessBiderError errorMessage); - void bideFinished(ProcessBiderError errorStatus); -}; - -#endif // PROCESSWAITER_H diff --git a/app/src/tools/processbider_p.h b/app/src/tools/processbider_p.h deleted file mode 100644 index 78900b4..0000000 --- a/app/src/tools/processbider_p.h +++ /dev/null @@ -1,73 +0,0 @@ -#ifndef PROCESSWAITER_P_H -#define PROCESSWAITER_P_H - -#include -#include -#ifdef __linux__ - #include -#endif -#ifdef _WIN32 - typedef void* HANDLE; -#endif - -class ProcessWaiter -{ -//-Instance Variables------------------------------------------------------------------------------------------------------------ -private: - bool mWaiting; - QString mName; - quint32 mId; - uint mPollRate; - QMutex mMutex; - -#ifdef _WIN32 - HANDLE mHandle; -#endif -#ifdef __linux__ - QWaitCondition mCloseNotifier; -#endif - -//-Constructor------------------------------------------------------------------------------------------------- -public: - ProcessWaiter(const QString& name) : - mWaiting(false), - mName(name), - mId(0), - mPollRate(500) - {} - -//-Instance Functions--------------------------------------------------------------------------------------------------------- -private: - bool _wait(); - bool _close(); - -public: - // Used in waiting thread - bool wait() - { - mWaiting = true; - bool r =_wait(); - mWaiting = false; - return r; - } - - // Used from external thread; - void updateId(quint32 id) - { - mMutex.lock(); - mId = id; - mMutex.unlock(); - }; - - void setPollRate(uint pollRate) - { - mMutex.lock(); - mPollRate = pollRate; - mMutex.unlock(); - } - - bool isWaiting() { return mWaiting; } - bool close() { return mWaiting ? _close() : true; } -}; - -#endif // PROCESSWAITER_P_H diff --git a/app/src/tools/processbider_p_linux.cpp b/app/src/tools/processbider_p_linux.cpp deleted file mode 100644 index 74e55ec..0000000 --- a/app/src/tools/processbider_p_linux.cpp +++ /dev/null @@ -1,48 +0,0 @@ -// Unit Include -#include "processbider_p.h" - -// Qt Includes -#include - -// Qx Includes -#include - -//=============================================================================================================== -// ProcessWaiter -//=============================================================================================================== - -//-Instance Functions------------------------------------------------------------- -//Public: -bool ProcessWaiter::_wait() -{ - // Poll for process existence - QString currentName = mName; - while(currentName == mName) - { - QThread::msleep(mPollRate); - mMutex.lock(); // Don't allow close during check - currentName = Qx::processName(mId); - mMutex.unlock(); - } - - // Notify that the process closed - mCloseNotifier.wakeAll(); - - return true; -} - -bool ProcessWaiter::_close() -{ - // NOTE: Does not handle killing processes that require greater permissions - - // Try clean close first - Qx::cleanKillProcess(mId); - - // See if process closes (max 2 seconds, though al) - mMutex.lock(); - if(mCloseNotifier.wait(&mMutex, std::max(uint(2000), mPollRate + 100))) - return true; - - // Force close - return !Qx::forceKillProcess(mId).isValid(); -} diff --git a/app/src/tools/processbider_p_win.cpp b/app/src/tools/processbider_p_win.cpp deleted file mode 100644 index afdf1c7..0000000 --- a/app/src/tools/processbider_p_win.cpp +++ /dev/null @@ -1,137 +0,0 @@ -// Unit Include -#include "processbider_p.h" - -// Qx Includes -#include -#include -#include - -// Windows Include -#include - -namespace -{ - -bool closeAdminProcess(DWORD processId, bool force) -{ - /* Killing an elevated process from this process while it is unelevated requires (without COM non-sense) starting - * a new process as admin to do the job. While a special purpose executable could be made, taskkill already - * perfectly suitable here - */ - - // Setup taskkill args - QString tkArgs; - if(force) - tkArgs += u"/F "_s; - tkArgs += u"/PID "_s; - tkArgs += QString::number(processId); - const std::wstring tkArgsStd = tkArgs.toStdWString(); - - // Setup taskkill info - SHELLEXECUTEINFOW tkExecInfo = {0}; - tkExecInfo.cbSize = sizeof(SHELLEXECUTEINFOW); // Required - tkExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS; // Causes hProcess member to be set to process handle - tkExecInfo.hwnd = NULL; - tkExecInfo.lpVerb = L"runas"; - tkExecInfo.lpFile = L"taskkill"; - tkExecInfo.lpParameters = tkArgsStd.data(); - tkExecInfo.lpDirectory = NULL; - tkExecInfo.nShow = SW_HIDE; - - // Start taskkill - if(!ShellExecuteEx(&tkExecInfo)) - return false; - - // Check for handle - HANDLE tkHandle = tkExecInfo.hProcess; - if(!tkHandle) - return false; - - // Wait for taskkill to finish (should be fast) - if(WaitForSingleObject(tkHandle, 5000) != WAIT_OBJECT_0) - return false; - - DWORD exitCode; - if(!GetExitCodeProcess(tkHandle, &exitCode)) - return false; - - // Cleanup taskkill handle - CloseHandle(tkHandle); - - // Return taskkill result - return exitCode == 0; -} - -} - -//=============================================================================================================== -// ProcessWaiter -//=============================================================================================================== - -//-Instance Functions------------------------------------------------------------- -//Public: -bool ProcessWaiter::_wait() -{ - // Prevent changes while setting up the wait - QMutexLocker mutLock(&mMutex); - - // Get process handle and see if it is valid - DWORD rights = PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE; - if((mHandle = OpenProcess(rights, FALSE, mId)) == NULL) // Can use Qx::lastError() for error info here if desired - return false; - - // Attempt to wait on process to terminate - mutLock.unlock(); // Allow changes while waiting - DWORD waitError = WaitForSingleObject(mHandle, INFINITE); - mutLock.relock(); // Prevent changes during cleanup - - // Close handle to process - CloseHandle(mHandle); - mHandle = nullptr; - - /* Here the status can technically can be WAIT_ABANDONED, WAIT_OBJECT_0, WAIT_TIMEOUT, or WAIT_FAILED, but the first - * and third should never occur here (the wait won't ever be abandoned, and the timeout is infinite), so this check is fine - */ - if(waitError != WAIT_OBJECT_0) // Can use Qx::lastError() for error info here if desired - return false; - - return true; -} - -bool ProcessWaiter::_close() -{ - if(!mHandle) - return true; - - // Prevent wait setup/cleanup during closure and auto-unlock when done - QMutexLocker mutLock(&mMutex); - - // Check if admin rights are needed (CLIFp shouldn't be run as admin, but check anyway) - bool selfElevated; - if(Qx::processIsElevated(selfElevated).isValid()) - selfElevated = false; // If check fails, assume CLIFP is not elevated to be safe - bool waitProcessElevated; - if(Qx::processIsElevated(waitProcessElevated, mId).isValid()) - waitProcessElevated = true; // If check fails, assume process is elevated to be safe - - bool elevate = !selfElevated && waitProcessElevated; - - // Try clean close first - if(!elevate) - Qx::cleanKillProcess(mId); - else - closeAdminProcess(mId, false); - - // Wait for process to close (allow up to 2 seconds) - DWORD waitRes = WaitForSingleObject(mHandle, 2000); - - // See if process closed - if(waitRes == WAIT_OBJECT_0) - return true; - - // Force close - if(!elevate) - return !Qx::forceKillProcess(mId).isValid(); - else - return closeAdminProcess(mId, true); -}