diff --git a/CMakeLists.txt b/CMakeLists.txt index 650c2b4..070179c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,14 +6,14 @@ cmake_minimum_required(VERSION 3.24.0...3.26.0) # Project # NOTE: DON'T USE TRAILING ZEROS IN VERSIONS project(CLIFp - VERSION 0.9.9.1 + VERSION 0.9.10 LANGUAGES CXX DESCRIPTION "Command-line Interface for Flashpoint Archive" ) # Get helper scripts include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchOBCMake.cmake) -fetch_ob_cmake("v0.3.3") +fetch_ob_cmake("v0.3.4") # Initialize project according to standard rules include(OB/Project) @@ -72,14 +72,14 @@ endif() include(OB/FetchQx) ob_fetch_qx( - REF "v0.5.5.1" + REF "v0.5.6" COMPONENTS ${CLIFP_QX_COMPONENTS} ) # Fetch libfp (build and import from source) include(OB/Fetchlibfp) -ob_fetch_libfp("v0.5.1.1") +ob_fetch_libfp("v0.5.2") # Fetch QI-QMP (build and import from source) include(OB/FetchQI-QMP) diff --git a/README.md b/README.md index 74de0b5..ab4dad9 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,12 @@ Update: ## Compatability ### General -All Flashpoint features are generally supported, other than editing configuration files and user data (like playlists) and querying title meta-data through the command-line, and searching is more limited. See the [All Commands/Options](#all-commandsoptions) section for more information. + +Most flashpoint features are supported. The regular launcher still must be used for the following: +- Changing user configuration, like preferences, playlists, etc. +- Updating the launcher and downloading game updates + +See the [All Commands/Options](#all-commandsoptions) section for more information. While constantly testing for complete compatibility is infeasible given the size of Flashpoint, CLIFp was designed with full compatibility in mind and theoretically is 100% compatible with the Flashpoint collection. @@ -105,7 +110,7 @@ Or if feeling spontaneous, use the **-r** switch, followed by a library filter t See the [All Commands/Options](#all-commandsoptions) section for more information. **Direct Execution:** -The legacy approach is to use the **run** command with the **--app** and **--param** switches. This will start Flashpoint's webserver and then start the application specified with the provided parameters: +The legacy approach is to use the **run** command with the **--app** and **--param** switches. This will start Flashpoint's services and then start the application specified with the provided parameters: CLIFp run --app="FPSoftware\Flash\flashplayer_32_sa.exe" --param="http://www.mowa.org/work/buttons/buttons_art/basic.swf" @@ -114,6 +119,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 +141,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. @@ -171,6 +183,14 @@ The **-title-strict** and **-subtitle-strict** options only consider exact match Tip: You can use **-subtitle** with an empty string (i.e. `-s ""`) to see all of the additional-apps for a given title. ### Command List: + +**download** - Downloads data packs for games that require them in bulk + +Options: +- **-p | --playlist** Name of the playlist to download games for. + +-------------------------------------------------------------------------------- + **link** - Creates a shortcut to a Flashpoint title Options: @@ -199,7 +219,7 @@ Options: -------------------------------------------------------------------------------- - **run** - Start Flashpoint's webserver and then execute the provided application + **run** - Start Flashpoint's services and then execute the provided application Options: - **-a | --app:** Relative (to Flashpoint Directory) path of application to launch diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index af6d3df..2bd230b 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -11,6 +11,8 @@ set(CLIFP_SOURCE kernel/errorstatus.cpp command/command.h command/command.cpp + command/c-download.h + command/c-download.cpp command/c-link.h command/c-link.cpp command/c-play.h @@ -88,8 +90,6 @@ if(CMAKE_SYSTEM_NAME STREQUAL Windows) task/t-exec_win.cpp task/t-bideprocess.h task/t-bideprocess.cpp - tools/processbider.h - tools/processbider.cpp ) list(APPEND CLIFP_LINKS diff --git a/app/src/command/c-download.cpp b/app/src/command/c-download.cpp new file mode 100644 index 0000000..fb8ad67 --- /dev/null +++ b/app/src/command/c-download.cpp @@ -0,0 +1,123 @@ +// Unit Include +#include "c-download.h" + +// Project Includes +#include "task/t-download.h" +#include "task/t-generic.h" + +//=============================================================================================================== +// CDownloadError +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Private: +CDownloadError::CDownloadError(Type t, const QString& s) : + mType(t), + mSpecific(s) +{} + +//-Instance Functions------------------------------------------------------------- +//Public: +bool CDownloadError::isValid() const { return mType != NoError; } +QString CDownloadError::specific() const { return mSpecific; } +CDownloadError::Type CDownloadError::type() const { return mType; } + +//Private: +Qx::Severity CDownloadError::deriveSeverity() const { return Qx::Critical; } +quint32 CDownloadError::deriveValue() const { return mType; } +QString CDownloadError::derivePrimary() const { return ERR_STRINGS.value(mType); } +QString CDownloadError::deriveSecondary() const { return mSpecific; } + +//=============================================================================================================== +// CDownload +//=============================================================================================================== + +//-Constructor------------------------------------------------------------- +//Public: +CDownload::CDownload(Core& coreRef) : Command(coreRef) {} + +//-Instance Functions------------------------------------------------------------- +//Protected: +QList CDownload::options() const { return CL_OPTIONS_SPECIFIC + Command::options(); } +QSet CDownload::requiredOptions() const { return CL_OPTIONS_REQUIRED + Command::requiredOptions(); } +QString CDownload::name() const { return NAME; } + +Qx::Error CDownload::perform() +{ + QString playlistName = mParser.value(CL_OPTION_PLAYLIST).trimmed(); + mCore.setStatus(STATUS_DOWNLOAD, playlistName); + + Fp::Db* db = mCore.fpInstall().database(); + Fp::PlaylistManager* pm = mCore.fpInstall().playlistManager(); + if(Qx::Error pError = pm->populate(); pError.isValid()) + return pError; + + // Find playlist + QList playlists = pm->playlists(); + auto pItr = std::find_if(playlists.cbegin(), playlists.cend(), [&playlistName](auto p){ + return p.title() == playlistName || p.title().trimmed() == playlistName; // Some playlists have spaces for sorting purposes + }); + + if(pItr == playlists.cend()) + { + CDownloadError err(CDownloadError::InvalidPlaylist, playlistName); + postError(err); + return err; + } + logEvent(LOG_EVENT_PLAYLIST_MATCH.arg(pItr->id().toString(QUuid::WithoutBraces))); + + // Queue downloads for each game + TDownload* downloadTask = new TDownload(&mCore); + downloadTask->setStage(Task::Stage::Primary); + downloadTask->setDescription(u"playlist data packs"_s); + QList dataIds; + + const Fp::Toolkit* tk = mCore.fpInstall().toolkit(); + for(const auto& pg : pItr->playlistGames()) + { + // Get data + Fp::GameData gameData; + if(Fp::DbError gdErr = db->getGameData(gameData, pg.gameId()); gdErr.isValid()) + { + postError(gdErr); + return gdErr; + } + + if(gameData.isNull()) + { + logEvent(LOG_EVENT_NON_DATAPACK.arg(pg.gameId().toString(QUuid::WithoutBraces))); + continue; + } + + if(tk->datapackIsPresent(gameData)) + continue; + + // Queue download + downloadTask->addFile({.target = tk->datapackUrl(gameData), .dest = tk->datapackPath(gameData), .checksum = gameData.sha256()}); + + // Note data id + dataIds.append(gameData.id()); + } + + if(downloadTask->isEmpty()) + { + logEvent(LOG_EVENT_NO_OP); + return CDownloadError(); + } + + // Enqueue download task + mCore.enqueueSingleTask(downloadTask); + + // Enqueue onDiskState update task + Core* corePtr = &mCore; // Safe, will outlive task + TGeneric* onDiskUpdateTask = new TGeneric(corePtr); + onDiskUpdateTask->setStage(Task::Stage::Primary); + onDiskUpdateTask->setDescription(u"Update GameData onDisk state."_s); + onDiskUpdateTask->setAction([dataIds, corePtr]{ + return corePtr->fpInstall().database()->updateGameDataOnDiskState(dataIds, true); + }); + mCore.enqueueSingleTask(onDiskUpdateTask); + + // Return success + return CDownloadError(); +} diff --git a/app/src/command/c-download.h b/app/src/command/c-download.h new file mode 100644 index 0000000..5a1f2a5 --- /dev/null +++ b/app/src/command/c-download.h @@ -0,0 +1,90 @@ +#ifndef CDOWNLOAD_H +#define CDOWNLOAD_H + +// Qx Includes +#include + +// Project Includes +#include "command/command.h" + +class QX_ERROR_TYPE(CDownloadError, "CDownloadError", 1217) +{ + friend class CDownload; + //-Class Enums------------------------------------------------------------- +public: + enum Type + { + NoError, + InvalidPlaylist + }; + + //-Class Variables------------------------------------------------------------- +private: + static inline const QHash ERR_STRINGS{ + {NoError, u""_s}, + {InvalidPlaylist, u""_s} + }; + + //-Instance Variables------------------------------------------------------------- +private: + Type mType; + QString mSpecific; + + //-Constructor------------------------------------------------------------- +private: + CDownloadError(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 CDownload : public Command +{ + //-Class Variables------------------------------------------------------------------------------------------------------ +private: + // Status + static inline const QString STATUS_DOWNLOAD = u"Downloading data packs"_s; + + // Logging + static inline const QString LOG_EVENT_PLAYLIST_MATCH = u"Playlist matches ID: %1"_s; + static inline const QString LOG_EVENT_NON_DATAPACK = u"Game %1 does not use a data pack."_s; + static inline const QString LOG_EVENT_NO_OP = u"No datapacks to download."_s; + + // Command line option strings + static inline const QString CL_OPT_PLAYLIST_S_NAME = u"p"_s; + static inline const QString CL_OPT_PLAYLIST_L_NAME = u"playlist"_s; + static inline const QString CL_OPT_PLAYLIST_DESC = u"Name of the playlist to download games for."_s; + + // Command line options + static inline const QCommandLineOption CL_OPTION_PLAYLIST{{CL_OPT_PLAYLIST_S_NAME, CL_OPT_PLAYLIST_L_NAME}, CL_OPT_PLAYLIST_DESC, u"playlist"_s}; // Takes value + static inline const QList CL_OPTIONS_SPECIFIC{&CL_OPTION_PLAYLIST}; + static inline const QSet CL_OPTIONS_REQUIRED{&CL_OPTION_PLAYLIST}; + +public: + // Meta + static inline const QString NAME = u"download"_s; + static inline const QString DESCRIPTION = u"Download game data packs in bulk"_s; + + //-Constructor---------------------------------------------------------------------------------------------------------- +public: + CDownload(Core& coreRef); + + //-Instance Functions------------------------------------------------------------------------------------------------------ +protected: + QList options() const override; + QSet requiredOptions() const override; + QString name() const override; + Qx::Error perform() override; +}; +REGISTER_COMMAND(CDownload::NAME, CDownload, CDownload::DESCRIPTION); + +#endif // CDOWNLOAD_H diff --git a/app/src/command/c-link.cpp b/app/src/command/c-link.cpp index d6e0489..959eb0b 100644 --- a/app/src/command/c-link.cpp +++ b/app/src/command/c-link.cpp @@ -34,9 +34,9 @@ CLink::CLink(Core& coreRef) : TitleCommand(coreRef) {} //-Instance Functions------------------------------------------------------------- //Protected: -QList CLink::options() { return CL_OPTIONS_SPECIFIC + TitleCommand::options(); } -QSet CLink::requiredOptions() { return CL_OPTIONS_REQUIRED + TitleCommand::requiredOptions(); } -QString CLink::name() { return NAME; } +QList CLink::options() const { return CL_OPTIONS_SPECIFIC + TitleCommand::options(); } +QSet CLink::requiredOptions() const { return CL_OPTIONS_REQUIRED + TitleCommand::requiredOptions(); } +QString CLink::name() const { return NAME; } Qx::Error CLink::perform() { @@ -55,11 +55,11 @@ Qx::Error CLink::perform() Fp::Db* database = mCore.fpInstall().database(); // Get entry (also confirms that ID is present in database, which is why we do this even if a custom name is set) - std::variant entry_v; + Fp::Entry entry_v; Fp::DbError dbError = database->getEntry(entry_v, shortcutId); if(dbError.isValid()) { - mCore.postError(NAME, dbError); + postError(dbError); return dbError; } @@ -76,7 +76,7 @@ Qx::Error CLink::perform() Fp::DbError dbError = database->getEntry(entry_v, addApp.parentId()); if(dbError.isValid()) { - mCore.postError(NAME, dbError); + postError(dbError); return dbError; } Q_ASSERT(std::holds_alternative(entry_v)); @@ -85,7 +85,7 @@ Qx::Error CLink::perform() shortcutName = parent.title() + u" ("_s + addApp.name() + u")"_s; } else - qCritical("Invalid variant state for std::variant."); + qCritical("Invalid variant state for Fp::Entry."); // Override shortcut name with user input if(mParser.isSet(CL_OPTION_NAME)) @@ -96,7 +96,7 @@ Qx::Error CLink::perform() shortcutDir = mParser.value(CL_OPTION_PATH); else { - mCore.logEvent(NAME, LOG_EVENT_NO_PATH); + logEvent(LOG_EVENT_NO_PATH); // Prompt user for path Core::ExistingDirRequest edr{ @@ -107,12 +107,12 @@ Qx::Error CLink::perform() if(selectedPath.isEmpty()) { - mCore.logEvent(NAME, LOG_EVENT_DIAG_CANCEL); + logEvent(LOG_EVENT_DIAG_CANCEL); return CLinkError(); } else { - mCore.logEvent(NAME, LOG_EVENT_SEL_PATH.arg(QDir::toNativeSeparators(selectedPath))); + logEvent(LOG_EVENT_SEL_PATH.arg(QDir::toNativeSeparators(selectedPath))); shortcutDir = selectedPath; } } @@ -123,10 +123,10 @@ Qx::Error CLink::perform() if(!shortcutDir.mkpath(shortcutDir.absolutePath())) { CLinkError err(CLinkError::InvalidPath); - mCore.postError(NAME, err); + postError(err); return err; } - mCore.logEvent(NAME, LOG_EVENT_CREATED_DIR_PATH.arg(QDir::toNativeSeparators(shortcutDir.absolutePath()))); + logEvent(LOG_EVENT_CREATED_DIR_PATH.arg(QDir::toNativeSeparators(shortcutDir.absolutePath()))); } // Create shortcut diff --git a/app/src/command/c-link.h b/app/src/command/c-link.h index 70778af..0f5e338 100644 --- a/app/src/command/c-link.h +++ b/app/src/command/c-link.h @@ -96,9 +96,9 @@ class CLink : public TitleCommand Qx::Error createShortcut(const QString& name, const QDir& dir, QUuid id); protected: - QList options() override; - QSet requiredOptions() override; - QString name() override; + QList options() const override; + QSet requiredOptions() const override; + QString name() const override; Qx::Error perform() override ; }; REGISTER_COMMAND(CLink::NAME, CLink, CLink::DESCRIPTION); diff --git a/app/src/command/c-link_win.cpp b/app/src/command/c-link_win.cpp index 0999d70..d97dab1 100644 --- a/app/src/command/c-link_win.cpp +++ b/app/src/command/c-link_win.cpp @@ -31,11 +31,11 @@ Qx::Error CLink::createShortcut(const QString& name, const QDir& dir, QUuid id) // Check for creation failure if(shortcutError.isValid()) { - mCore.postError(NAME, shortcutError); + postError(shortcutError); return shortcutError; } else - mCore.logEvent(NAME, LOG_EVENT_CREATED_SHORTCUT.arg(id.toString(QUuid::WithoutBraces), QDir::toNativeSeparators(fullShortcutPath))); + logEvent(LOG_EVENT_CREATED_SHORTCUT.arg(id.toString(QUuid::WithoutBraces), QDir::toNativeSeparators(fullShortcutPath))); // Return success return CLinkError(); diff --git a/app/src/command/c-play.cpp b/app/src/command/c-play.cpp index 092dca9..5331cc2 100644 --- a/app/src/command/c-play.cpp +++ b/app/src/command/c-play.cpp @@ -69,144 +69,151 @@ Fp::AddApp CPlay::buildAdditionalApp(const Fp::Db::QueryBuffer& addAppResult) //-Instance Functions------------------------------------------------------------- //Private: -Qx::Error CPlay::enqueueAutomaticTasks(bool& wasStandalone, QUuid targetId) +QString CPlay::getServerOverride(const Fp::GameData& gd) { - // Clear standalone check - wasStandalone = false; + QString override = gd.isNull() ? QString() : gd.parameters().server(); + if(!override.isNull()) + logEvent(LOG_EVENT_SERVER_OVERRIDE.arg(override)); - mCore.logEvent(NAME, LOG_EVENT_ENQ_AUTO); + return override; +} - // Get entry via ID - Fp::Db* database = mCore.fpInstall().database(); +Qx::Error CPlay::handleEntry(const Fp::Game& game) +{ + logEvent(LOG_EVENT_ID_MATCH_TITLE.arg(game.title())); + + Qx::Error sError; + Fp::Db* db = mCore.fpInstall().database(); - std::variant entry; - if(Fp::DbError eErr = database->getEntry(entry, targetId); eErr.isValid()) + // Get game data (if present) + Fp::GameData gameData; + if(Fp::DbError gdErr = db->getGameData(gameData, game.id()); gdErr.isValid()) { - mCore.postError(NAME, eErr); - return eErr; + postError(gdErr); + return gdErr; } + bool hasDatapack = !gameData.isNull(); - // Enqueue if result is additional app - if(std::holds_alternative(entry)) - { - // Get add app - const Fp::AddApp& addApp = std::get(entry); + // Get server override (if not present, will result in the default server being used) + QString serverOverride = getServerOverride(gameData); - mCore.logEvent(NAME, LOG_EVENT_ID_MATCH_ADDAPP.arg(addApp.name(), - addApp.parentId().toString(QUuid::WithoutBraces))); + // Enqueue services + if(sError = mCore.enqueueStartupTasks(serverOverride); sError.isValid()) + return sError; - // Clear queue if this entry is a message or extra - if(addApp.appPath() == Fp::Db::Table_Add_App::ENTRY_MESSAGE || addApp.appPath() == Fp::Db::Table_Add_App::ENTRY_EXTRAS) - { - mCore.clearTaskQueue(); - mCore.logEvent(NAME, LOG_EVENT_QUEUE_CLEARED); - wasStandalone = true; - } + // Handle datapack tasks + if(hasDatapack) + { + logEvent(LOG_EVENT_DATA_PACK_TITLE); - // Check if parent entry uses a data pack - QUuid parentId = addApp.parentId(); - Fp::GameData parentGameData; - if(Fp::DbError gdErr = database->getGameData(parentGameData, parentId); gdErr.isValid()) - { - mCore.postError(NAME, gdErr); - return gdErr; - } + if(sError = mCore.enqueueDataPackTasks(gameData); sError.isValid()) + return sError; + } - if(!parentGameData.isNull()) - { - mCore.logEvent(NAME, LOG_EVENT_DATA_PACK_TITLE); + // Get game's additional apps + Fp::Db::EntryFilter addAppFilter{.type = Fp::Db::EntryType::AddApp, .parent = game.id()}; - if(Qx::Error ee = mCore.enqueueDataPackTasks(parentGameData); ee.isValid()) - return ee; - } + Fp::DbError addAppSearchError; + Fp::Db::QueryBuffer addAppSearchResult; - // Get parent info to determine platform - Fp::Db::EntryFilter parentFilter{.type = Fp::Db::EntryType::Primary, .id = parentId}; + addAppSearchError = db->queryEntrys(addAppSearchResult, addAppFilter); + if(addAppSearchError.isValid()) + { + postError(addAppSearchError); + return addAppSearchError; + } + + // Enqueue auto-run before apps + for(int i = 0; i < addAppSearchResult.size; i++) + { + // Go to next record + addAppSearchResult.result.next(); - Fp::Db::QueryBuffer parentResult; + // Build + Fp::AddApp addApp = buildAdditionalApp(addAppSearchResult); - if(Fp::DbError pge = database->queryEntrys(parentResult, parentFilter); pge.isValid()) + // Enqueue if auto-run before + if(addApp.isAutorunBefore()) { - mCore.postError(NAME, pge); - return pge; + logEvent(LOG_EVENT_FOUND_AUTORUN.arg(addApp.name())); + + if(sError = enqueueAdditionalApp(addApp, game.platformName(), Task::Stage::Auxiliary); sError.isValid()) + return sError; } + } + + // Enqueue game + if(sError = enqueueGame(game, gameData, Task::Stage::Primary); sError.isValid()) + return sError; - // Advance result to only record - parentResult.result.next(); + // Enqueue service shutdown + mCore.enqueueShutdownTasks(); - // Determine platform (don't bother building entire game object since only one value is needed) - QString platformName = parentResult.result.value(Fp::Db::Table_Game::COL_PLATFORM_NAME).toString(); + return Qx::Error(); +} - // Enqueue - mCore.setStatus(STATUS_PLAY, addApp.name()); +Qx::Error CPlay::handleEntry(const Fp::AddApp& addApp) +{ + logEvent(LOG_EVENT_ID_MATCH_ADDAPP.arg(addApp.name(), + addApp.parentId().toString(QUuid::WithoutBraces))); - if(Qx::Error ee = enqueueAdditionalApp(addApp, platformName, Task::Stage::Primary); ee.isValid()) - return ee; - } - else if(std::holds_alternative(entry)) // Get auto-run additional apps if result is game + Qx::Error sError; + Fp::Db* db = mCore.fpInstall().database(); + + // Check if parent entry uses a data pack + QUuid parentId = addApp.parentId(); + Fp::GameData parentGameData; + if(Fp::DbError gdErr = db->getGameData(parentGameData, parentId); gdErr.isValid()) { - // Get game - const Fp::Game& game = std::get(entry); + postError(gdErr); + return gdErr; + } + bool hasDatapack = !parentGameData.isNull(); - mCore.logEvent(NAME, LOG_EVENT_ID_MATCH_TITLE.arg(game.title())); + // Get server override (if not present, will result in the default server being used) + QString serverOverride = getServerOverride(parentGameData); - // Get game data (if present) - Fp::GameData gameData; - if(Fp::DbError gdErr = database->getGameData(gameData, targetId); gdErr.isValid()) - { - mCore.postError(NAME, gdErr); - return gdErr; - } + // Enqueue services if needed + bool needsServices = addApp.appPath() != Fp::Db::Table_Add_App::ENTRY_MESSAGE && addApp.appPath() != Fp::Db::Table_Add_App::ENTRY_EXTRAS; + if(needsServices && (sError = mCore.enqueueStartupTasks(serverOverride)).isValid()) + return sError; - // Check if entry uses a data pack - if(!gameData.isNull()) - { - mCore.logEvent(NAME, LOG_EVENT_DATA_PACK_TITLE); + // Handle datapack tasks + if(hasDatapack) + { + logEvent(LOG_EVENT_DATA_PACK_TITLE); - if(Qx::Error ee = mCore.enqueueDataPackTasks(gameData); ee.isValid()) - return ee; - } + if(sError = mCore.enqueueDataPackTasks(parentGameData); sError.isValid()) + return sError; + } - // Get game's additional apps - Fp::Db::EntryFilter addAppFilter{.type = Fp::Db::EntryType::AddApp, .parent = targetId}; + // Get parent info to determine platform + Fp::Db::EntryFilter parentFilter{.type = Fp::Db::EntryType::Primary, .id = parentId}; - Fp::DbError addAppSearchError; - Fp::Db::QueryBuffer addAppSearchResult; + Fp::Db::QueryBuffer parentResult; - addAppSearchError = database->queryEntrys(addAppSearchResult, addAppFilter); - if(addAppSearchError.isValid()) - { - mCore.postError(NAME, addAppSearchError); - return addAppSearchError; - } + if(Fp::DbError pge = db->queryEntrys(parentResult, parentFilter); pge.isValid()) + { + postError(pge); + return pge; + } - // Enqueue auto-run before apps - for(int i = 0; i < addAppSearchResult.size; i++) - { - // Go to next record - addAppSearchResult.result.next(); + // Advance result to only record + parentResult.result.next(); - // Build - Fp::AddApp addApp = buildAdditionalApp(addAppSearchResult); + // Determine platform (don't bother building entire game object since only one value is needed) + QString platformName = parentResult.result.value(Fp::Db::Table_Game::COL_PLATFORM_NAME).toString(); - // Enqueue if auto-run before - if(addApp.isAutorunBefore()) - { - mCore.logEvent(NAME, LOG_EVENT_FOUND_AUTORUN.arg(addApp.name())); + // Enqueue + mCore.setStatus(STATUS_PLAY, addApp.name()); - if(Qx::Error ee = enqueueAdditionalApp(addApp, game.platformName(), Task::Stage::Auxiliary); ee.isValid()) - return ee; - } - } + if(sError = enqueueAdditionalApp(addApp, platformName, Task::Stage::Primary); sError.isValid()) + return sError; - // Enqueue game - if(Qx::Error ee = enqueueGame(game, gameData, Task::Stage::Primary); ee.isValid()) - return ee; - } - else - qFatal("Auto ID search result source must be 'game' or 'additional_app'"); + // Enqueue service shutdown if needed + if(needsServices) + mCore.enqueueShutdownTasks(); - // Return success return Qx::Error(); } @@ -231,14 +238,14 @@ Qx::Error CPlay::enqueueAdditionalApp(const Fp::AddApp& addApp, const QString& p } else { - QString addAppPath = mCore.resolveTrueAppPath(addApp.appPath(), platform); - QFileInfo fulladdAppPathInfo(mCore.fpInstall().fullPath() + '/' + addAppPath); + QString addAppPath = mCore.resolveFullAppPath(addApp.appPath(), platform); + QFileInfo addAppPathInfo(addAppPath); TExec* addAppTask = new TExec(&mCore); addAppTask->setIdentifier(addApp.name()); addAppTask->setStage(taskStage); - addAppTask->setExecutable(fulladdAppPathInfo.canonicalFilePath()); - addAppTask->setDirectory(fulladdAppPathInfo.absoluteDir()); + addAppTask->setExecutable(addAppPathInfo.canonicalFilePath()); + addAppTask->setDirectory(addAppPathInfo.absoluteDir()); addAppTask->setParameters(addApp.launchCommand()); addAppTask->setEnvironment(mCore.childTitleProcessEnvironment()); addAppTask->setProcessType(addApp.isWaitExit() || taskStage == Task::Stage::Primary ? TExec::ProcessType::Blocking : TExec::ProcessType::Deferred); @@ -247,7 +254,7 @@ Qx::Error CPlay::enqueueAdditionalApp(const Fp::AddApp& addApp, const QString& p #ifdef _WIN32 // Add wait task if required - if(Qx::Error ee = mCore.conditionallyEnqueueBideTask(fulladdAppPathInfo); ee.isValid()) + if(Qx::Error ee = mCore.conditionallyEnqueueBideTask(addAppPathInfo); ee.isValid()) return ee; #endif } @@ -258,15 +265,15 @@ Qx::Error CPlay::enqueueAdditionalApp(const Fp::AddApp& addApp, const QString& p Qx::Error CPlay::enqueueGame(const Fp::Game& game, const Fp::GameData& gameData, Task::Stage taskStage) { - QString gamePath = mCore.resolveTrueAppPath(!gameData.isNull() ? gameData.appPath() : game.appPath(), + QString gamePath = mCore.resolveFullAppPath(!gameData.isNull() ? gameData.appPath() : game.appPath(), game.platformName()); - QFileInfo fullGamePathInfo(mCore.fpInstall().fullPath() + '/' + gamePath); + QFileInfo gamePathInfo(gamePath); TExec* gameTask = new TExec(&mCore); gameTask->setIdentifier(game.title()); gameTask->setStage(taskStage); - gameTask->setExecutable(fullGamePathInfo.canonicalFilePath()); - gameTask->setDirectory(fullGamePathInfo.absoluteDir()); + gameTask->setExecutable(gamePathInfo.canonicalFilePath()); + gameTask->setDirectory(gamePathInfo.absoluteDir()); gameTask->setParameters(!gameData.isNull() ? gameData.launchCommand() : game.launchCommand()); gameTask->setEnvironment(mCore.childTitleProcessEnvironment()); gameTask->setProcessType(TExec::ProcessType::Blocking); @@ -276,7 +283,7 @@ Qx::Error CPlay::enqueueGame(const Fp::Game& game, const Fp::GameData& gameData, #ifdef _WIN32 // Add wait task if required - if(Qx::Error ee = mCore.conditionallyEnqueueBideTask(fullGamePathInfo); ee.isValid()) + if(Qx::Error ee = mCore.conditionallyEnqueueBideTask(gamePathInfo); ee.isValid()) return ee; #endif @@ -285,8 +292,8 @@ Qx::Error CPlay::enqueueGame(const Fp::Game& game, const Fp::GameData& gameData, } //Protected: -QList CPlay::options() { return CL_OPTIONS_SPECIFIC + TitleCommand::options(); } -QString CPlay::name() { return NAME; } +QList CPlay::options() const { return CL_OPTIONS_SPECIFIC + TitleCommand::options(); } +QString CPlay::name() const { return NAME; } Qx::Error CPlay::perform() { @@ -298,7 +305,7 @@ Qx::Error CPlay::perform() if(!urlMatch.hasMatch()) { CPlayError err(CPlayError::InvalidUrl); - mCore.postError(NAME, err); + postError(err); return err; } @@ -308,19 +315,21 @@ Qx::Error CPlay::perform() return ide; - Qx::Error errorStatus; - - // Enqueue required tasks - if((errorStatus = mCore.enqueueStartupTasks()).isValid()) - return errorStatus; + logEvent(LOG_EVENT_HANDLING_AUTO); - bool standaloneTask; - if((errorStatus = enqueueAutomaticTasks(standaloneTask, titleId)).isValid()) - return errorStatus; + // Get entry via ID + Fp::Db* db = mCore.fpInstall().database(); - if(!standaloneTask) - mCore.enqueueShutdownTasks(); + Fp::Entry entry; + if(Fp::DbError eErr = db->getEntry(entry, titleId); eErr.isValid()) + { + postError(eErr); + return eErr; + } - // Return success - return Qx::Error(); + // 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 44a5148..a76c41f 100644 --- a/app/src/command/c-play.h +++ b/app/src/command/c-play.h @@ -69,13 +69,13 @@ class CPlay : public TitleCommand static inline const QList CL_OPTIONS_SPECIFIC{&CL_OPTION_URL}; // Logging - Messages - static inline const QString LOG_EVENT_ENQ_AUTO = u"Enqueuing automatic tasks..."_s; + static inline const QString LOG_EVENT_HANDLING_AUTO = u"Handling automatic tasks..."_s; static inline const QString LOG_EVENT_URL_ID = u"ID from URL: %1"_s; static inline const QString LOG_EVENT_ID_MATCH_TITLE = u"ID matches main title: %1"_s; static inline const QString LOG_EVENT_ID_MATCH_ADDAPP = u"ID matches additional app: %1 (Child of %2)"_s; - static inline const QString LOG_EVENT_QUEUE_CLEARED = u"Previous queue entries cleared due to auto task being a Message/Extra"_s; static inline const QString LOG_EVENT_FOUND_AUTORUN = u"Found autorun-before additional app: %1"_s; static inline const QString LOG_EVENT_DATA_PACK_TITLE = u"Selected title uses a data pack"_s; + static inline const QString LOG_EVENT_SERVER_OVERRIDE = u"Selected title overrides the server to: %1"_s; public: // Meta @@ -93,16 +93,19 @@ class CPlay : public TitleCommand //-Instance Functions------------------------------------------------------------------------------------------------------ private: - // Queue - //TODO: Eventually rework to return via ref arg a list of tasks and a bool if app is message/extra so that startup tasks can be enqueued afterwords and queue clearing is unnecessary - Qx::Error enqueueAutomaticTasks(bool& wasStandalone, QUuid targetId); + QString getServerOverride(const Fp::GameData& gd); + Qx::Error handleEntry(const Fp::Game& game); + Qx::Error handleEntry(const Fp::AddApp& addApp); Qx::Error enqueueAdditionalApp(const Fp::AddApp& addApp, const QString& platform, Task::Stage taskStage); Qx::Error enqueueGame(const Fp::Game& game, const Fp::GameData& gameData, Task::Stage taskStage); protected: - QList options() override; - QString name() override; + QList options() const override; + QString name() const 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 fae8ef5..f233d03 100644 --- a/app/src/command/c-prepare.cpp +++ b/app/src/command/c-prepare.cpp @@ -14,8 +14,8 @@ CPrepare::CPrepare(Core& coreRef) : TitleCommand(coreRef) {} //-Instance Functions------------------------------------------------------------- //Protected: -QList CPrepare::options() { return {&CL_OPTION_ID, &CL_OPTION_TITLE, &CL_OPTION_TITLE_STRICT}; } -QString CPrepare::name() { return NAME; } +QList CPrepare::options() const { return TitleCommand::options(); } +QString CPrepare::name() const { return NAME; } Qx::Error CPrepare::perform() { @@ -28,7 +28,7 @@ Qx::Error CPrepare::perform() Fp::GameData titleGameData; if(Fp::DbError gdErr = mCore.fpInstall().database()->getGameData(titleGameData, id); gdErr.isValid()) { - mCore.postError(NAME, gdErr); + postError(gdErr); return gdErr; } @@ -40,8 +40,11 @@ Qx::Error CPrepare::perform() mCore.setStatus(STATUS_PREPARE, id.toString(QUuid::WithoutBraces)); } else - mCore.logError(NAME, Qx::GenericError(Qx::Warning, 12141, LOG_WRN_PREP_NOT_DATA_PACK.arg(id.toString(QUuid::WithoutBraces)))); + logError(Qx::GenericError(Qx::Warning, 12141, LOG_WRN_PREP_NOT_DATA_PACK.arg(id.toString(QUuid::WithoutBraces)))); // 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..8c9478d 100644 --- a/app/src/command/c-prepare.h +++ b/app/src/command/c-prepare.h @@ -28,9 +28,12 @@ class CPrepare : public TitleCommand //-Instance Functions------------------------------------------------------------------------------------------------------ protected: - QList options() override; - QString name() override; + QList options() const override; + QString name() const 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 c82dc60..76a9553 100644 --- a/app/src/command/c-run.cpp +++ b/app/src/command/c-run.cpp @@ -37,25 +37,18 @@ CRun::CRun(Core& coreRef) : Command(coreRef) {} //-Instance Functions------------------------------------------------------------- //Protected: -QList CRun::options() { return CL_OPTIONS_SPECIFIC + Command::options(); } -QString CRun::name() { return NAME; } +QList CRun::options() const { return CL_OPTIONS_SPECIFIC + Command::options(); } +QSet CRun::requiredOptions() const { return CL_OPTIONS_REQUIRED + Command::requiredOptions(); } +QString CRun::name() const { return NAME; } Qx::Error CRun::perform() { - // Make sure that at least an app was provided - if(!mParser.isSet(CL_OPTION_PARAM)) - { - CRunError err(CRunError::MissingApp); - mCore.logError(NAME, err); - return err; - } - // Enqueue startup tasks if(Qx::Error ee = mCore.enqueueStartupTasks(); ee.isValid()) return ee; - QString inputPath = mCore.resolveTrueAppPath(mParser.value(CL_OPTION_APP), u""_s); // No way of knowing platform - QFileInfo inputInfo = QFileInfo(mCore.fpInstall().fullPath() + '/' + inputPath); + QString inputPath = mCore.resolveFullAppPath(mParser.value(CL_OPTION_APP), u""_s); // No way of knowing platform + QFileInfo inputInfo = QFileInfo(inputPath); TExec* runTask = new TExec(&mCore); runTask->setIdentifier(NAME + u" program"_s); @@ -78,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 5c65a80..78a2af6 100644 --- a/app/src/command/c-run.h +++ b/app/src/command/c-run.h @@ -14,15 +14,13 @@ class QX_ERROR_TYPE(CRunError, "CRunError", 1215) public: enum Type { - NoError = 0, - MissingApp = 1 + NoError = 0 }; //-Class Variables------------------------------------------------------------- private: static inline const QHash ERR_STRINGS{ - {NoError, u""_s}, - {MissingApp, u"No application to run was provided."_s} + {NoError, u""_s} }; //-Instance Variables------------------------------------------------------------- @@ -67,6 +65,7 @@ class CRun : public Command static inline const QCommandLineOption CL_OPTION_APP{{CL_OPT_APP_S_NAME, CL_OPT_APP_L_NAME}, CL_OPT_APP_DESC, u"application"_s}; // Takes value static inline const QCommandLineOption CL_OPTION_PARAM{{CL_OPT_PARAM_S_NAME, CL_OPT_PARAM_L_NAME}, CL_OPT_PARAM_DESC, u"parameters"_s}; // Takes value static inline const QList CL_OPTIONS_SPECIFIC{&CL_OPTION_APP, &CL_OPTION_PARAM}; + static inline const QSet CL_OPTIONS_REQUIRED{&CL_OPTION_APP}; public: // Meta @@ -79,9 +78,13 @@ class CRun : public Command //-Instance Functions------------------------------------------------------------------------------------------------------ protected: - QList options() override; - QString name() override; + QList options() const override; + QSet requiredOptions() const override; + QString name() const override; Qx::Error perform() override; + +public: + bool requiresServices() const override; }; REGISTER_COMMAND(CRun::NAME, CRun, CRun::DESCRIPTION); diff --git a/app/src/command/c-share.cpp b/app/src/command/c-share.cpp index aa1f459..afc298f 100644 --- a/app/src/command/c-share.cpp +++ b/app/src/command/c-share.cpp @@ -41,20 +41,20 @@ CShare::CShare(Core& coreRef) : TitleCommand(coreRef) {} //-Instance Functions------------------------------------------------------------- //Protected: -QList CShare::options() { return CL_OPTIONS_SPECIFIC + TitleCommand::options(); } -QString CShare::name() { return NAME; } +QList CShare::options() const { return CL_OPTIONS_SPECIFIC + TitleCommand::options(); } +QString CShare::name() const { return NAME; } Qx::Error CShare::perform() { // Prioritize scheme (un)registration if(mParser.isSet(CL_OPTION_CONFIGURE)) { - mCore.logEvent(NAME, LOG_EVENT_REGISTRATION); + logEvent(LOG_EVENT_REGISTRATION); if(!Qx::setDefaultProtocolHandler(SCHEME, SCHEME_NAME)) { CShareError err(CShareError::RegistrationFailed); - mCore.postError(NAME, err); + postError(err); return err; } @@ -68,7 +68,7 @@ Qx::Error CShare::perform() } else if(mParser.isSet(CL_OPTION_UNCONFIGURE)) { - mCore.logEvent(NAME, LOG_EVENT_UNREGISTRATION); + logEvent(LOG_EVENT_UNREGISTRATION); #ifdef __linux__ // Function is too jank on linux right now, so always fail/no-op there if(true) @@ -77,7 +77,7 @@ Qx::Error CShare::perform() #endif { CShareError err(CShareError::UnregistrationFailed); - mCore.postError(NAME, err); + postError(err); return err; } @@ -100,7 +100,7 @@ Qx::Error CShare::perform() // Generate URL QString idStr = shareId.toString(QUuid::WithoutBraces); QString shareUrl = mParser.isSet(CL_OPTION_UNIVERSAL) ? SCHEME_TEMPLATE_UNI.arg(idStr) : SCHEME_TEMPLATE_STD.arg(idStr); - mCore.logEvent(NAME, LOG_EVENT_URL.arg(shareUrl)); + logEvent(LOG_EVENT_URL.arg(shareUrl)); // Add URL to clipboard mCore.requestClipboardUpdate(shareUrl); diff --git a/app/src/command/c-share.h b/app/src/command/c-share.h index c7196e3..54b7892 100644 --- a/app/src/command/c-share.h +++ b/app/src/command/c-share.h @@ -100,8 +100,8 @@ class CShare : public TitleCommand //-Instance Functions------------------------------------------------------------------------------------------------------ protected: - QList options() override; - QString name() override; + QList options() const override; + QString name() const override; Qx::Error perform() override; }; REGISTER_COMMAND(CShare::NAME, CShare, CShare::DESCRIPTION); diff --git a/app/src/command/c-show.cpp b/app/src/command/c-show.cpp index 7fdeeb4..2671d84 100644 --- a/app/src/command/c-show.cpp +++ b/app/src/command/c-show.cpp @@ -38,8 +38,8 @@ CShow::CShow(Core& coreRef) : Command(coreRef) {} //-Instance Functions------------------------------------------------------------- //Protected: -QList CShow::options() { return CL_OPTIONS_SPECIFIC + Command::options(); } -QString CShow::name() { return NAME; } +QList CShow::options() const { return CL_OPTIONS_SPECIFIC + Command::options(); } +QString CShow::name() const { return NAME; } Qx::Error CShow::perform() { @@ -65,7 +65,7 @@ Qx::Error CShow::perform() else { CShowError err(CShowError::MissingThing); - mCore.postError(NAME, err); + postError(err); return err; } diff --git a/app/src/command/c-show.h b/app/src/command/c-show.h index 679a29a..257a13f 100644 --- a/app/src/command/c-show.h +++ b/app/src/command/c-show.h @@ -80,8 +80,8 @@ class CShow : public Command //-Instance Functions------------------------------------------------------------------------------------------------------ protected: - QList options() override; - QString name() override; + QList options() const override; + QString name() const override; Qx::Error perform() override; }; REGISTER_COMMAND(CShow::NAME, CShow, CShow::DESCRIPTION); diff --git a/app/src/command/c-update.cpp b/app/src/command/c-update.cpp index 8ed4f8f..5d0c27d 100644 --- a/app/src/command/c-update.cpp +++ b/app/src/command/c-update.cpp @@ -209,7 +209,7 @@ QString CUpdate::getTargetAssetName(const QString& tagName) const CUpdateError CUpdate::handleTransfers(const UpdateTransfers& transfers) const { auto doTransfer = [&](const FileTransfer& ft, bool mkpath, bool move, bool overwrite){ - mCore.logEvent(NAME, LOG_EVENT_FILE_TRANSFER.arg(ft.source, ft.dest)); + logEvent(LOG_EVENT_FILE_TRANSFER.arg(ft.source, ft.dest)); if(mkpath) { @@ -225,12 +225,12 @@ CUpdateError CUpdate::handleTransfers(const UpdateTransfers& transfers) const }; // Backup, and note for restore - mCore.logEvent(NAME, LOG_EVENT_BACKUP_FILES); + logEvent(LOG_EVENT_BACKUP_FILES); QList restoreTransfers; QScopeGuard restoreOnFail([&]{ if(!restoreTransfers.isEmpty()) { - mCore.logEvent(NAME, LOG_EVENT_RESTORE_FILES); + logEvent(LOG_EVENT_RESTORE_FILES); for(const auto& t : restoreTransfers) doTransfer(t, false, true, true); } }); @@ -240,20 +240,20 @@ CUpdateError CUpdate::handleTransfers(const UpdateTransfers& transfers) const if(!doTransfer(ft, true, true, true)) { CUpdateError err(CUpdateError::TransferFail, ft.dest); - mCore.postError(NAME, err); + postError(err); return err; } restoreTransfers << FileTransfer{.source = ft.dest, .dest = ft.source}; } // Install - mCore.logEvent(NAME, LOG_EVENT_INSTALL_FILES); + logEvent(LOG_EVENT_INSTALL_FILES); for(const auto& ft : transfers.install) { if(!doTransfer(ft, true, false, false)) { CUpdateError err(CUpdateError::TransferFail, ft.dest); - mCore.postError(NAME, err); + postError(err); return err; } } @@ -268,19 +268,19 @@ CUpdateError CUpdate::checkAndPrepareUpdate() const if(!mCore.blockNewInstances()) { CUpdateError err(CUpdateError::AlreadyOpen); - mCore.postError(NAME, err); + postError(err); return err; } // Check for update mCore.setStatus(STATUS, STATUS_CHECKING); - mCore.logEvent(NAME, LOG_EVENT_CHECKING_FOR_NEWER_VERSION); + logEvent(LOG_EVENT_CHECKING_FOR_NEWER_VERSION); // Get new release data ReleaseData rd; if(CUpdateError ue = getLatestReleaseData(rd); ue.isValid()) { - mCore.postError(NAME, ue); + postError(ue); return ue; } @@ -291,17 +291,17 @@ CUpdateError CUpdate::checkAndPrepareUpdate() const if(newVersion.isNull()) { CUpdateError err(CUpdateError::InvalidReleaseVersion); - mCore.postError(NAME, err); + postError(err); return err; } if(newVersion <= currentVersion) { mCore.postMessage(Message{.text = MSG_NO_UPDATES}); - mCore.logEvent(NAME, MSG_NO_UPDATES); + logEvent(MSG_NO_UPDATES); return CUpdateError(); } - mCore.logEvent(NAME, LOG_EVENT_UPDATE_AVAILABLE.arg(rd.tag_name)); + logEvent(LOG_EVENT_UPDATE_AVAILABLE.arg(rd.tag_name)); // Get current build info BuildInfo bi = mCore.buildInfo(); @@ -315,13 +315,13 @@ CUpdateError CUpdate::checkAndPrepareUpdate() const if(aItr == rd.assets.cend()) { - mCore.postError(NAME, Qx::GenericError(Qx::Warning, 12181, WRN_NO_MATCHING_BUILD_P, WRN_NO_MATCHING_BUILD_S)); + postError(Qx::GenericError(Qx::Warning, 12181, WRN_NO_MATCHING_BUILD_P, WRN_NO_MATCHING_BUILD_S)); return CUpdateError(); } if(mCore.requestQuestionAnswer(QUES_UPDATE.arg(rd.name))) { - mCore.logEvent(NAME, LOG_EVENT_UPDATE_ACCEPED); + logEvent(LOG_EVENT_UPDATE_ACCEPED); // Queue update QDir uDownloadDir = updateDownloadDir(); @@ -330,9 +330,8 @@ CUpdateError CUpdate::checkAndPrepareUpdate() const QString tempName = u"clifp_update.zip"_s; TDownload* downloadTask = new TDownload(&mCore); downloadTask->setStage(Task::Stage::Primary); - downloadTask->setTargetFile(aItr->browser_download_url); - downloadTask->setDestinationPath(uDownloadDir.absolutePath()); - downloadTask->setDestinationFilename(tempName); + downloadTask->setDescription(u"update"_s); + downloadTask->addFile({.target = aItr->browser_download_url, .dest = uDownloadDir.absoluteFilePath(tempName)}); mCore.enqueueSingleTask(downloadTask); TExtract* extractTask = new TExtract(&mCore); @@ -355,7 +354,7 @@ CUpdateError CUpdate::checkAndPrepareUpdate() const smPersistCache = true; } else - mCore.logEvent(NAME, LOG_EVENT_UPDATE_REJECTED); + logEvent(LOG_EVENT_UPDATE_REJECTED); return CUpdateError(); } @@ -372,7 +371,7 @@ Qx::Error CUpdate::installUpdate(const QFileInfo& existingAppInfo) const do { - mCore.logEvent(NAME, LOG_EVENT_WAITING_ON_OLD_CLOSE.arg(totalGrace - currentGrace)); + logEvent(LOG_EVENT_WAITING_ON_OLD_CLOSE.arg(totalGrace - currentGrace)); QThread::msleep(step); currentGrace += step; haveLock = mCore.blockNewInstances(); @@ -383,18 +382,18 @@ Qx::Error CUpdate::installUpdate(const QFileInfo& existingAppInfo) const if(!haveLock) { CUpdateError err(CUpdateError::OldProcessNotFinished, "Aborting update."); - mCore.postError(NAME, err); + postError(err); return err; } //-Install update------------------------------------------------------------ - mCore.logEvent(NAME, LOG_EVENT_INSTALLING_UPDATE); + logEvent(LOG_EVENT_INSTALLING_UPDATE); // Ensure old executable exists where expected if(!existingAppInfo.exists()) { CUpdateError err(CUpdateError::InvalidPath, "Missing " + existingAppInfo.absoluteFilePath()); - mCore.postError(NAME, err); + postError(err); return err; } @@ -411,7 +410,7 @@ Qx::Error CUpdate::installUpdate(const QFileInfo& existingAppInfo) const QStringList updateFiles; if(Qx::IoOpReport rep = determineNewFiles(updateFiles, ts.updateRoot); rep.isFailure()) { - mCore.postError(NAME, rep); + postError(rep); return rep; } @@ -422,14 +421,14 @@ Qx::Error CUpdate::installUpdate(const QFileInfo& existingAppInfo) const return err; // Success - mCore.logEvent(NAME, MSG_UPDATE_COMPLETE); + logEvent(MSG_UPDATE_COMPLETE); mCore.postMessage(Message{.text = MSG_UPDATE_COMPLETE}); return CUpdateError(); } //Protected: -QList CUpdate::options() { return CL_OPTIONS_SPECIFIC + Command::options(); } -QString CUpdate::name() { return NAME; } +QList CUpdate::options() const { return CL_OPTIONS_SPECIFIC + Command::options(); } +QString CUpdate::name() const { return NAME; } Qx::Error CUpdate::perform() { diff --git a/app/src/command/c-update.h b/app/src/command/c-update.h index 24088cc..eecfe42 100644 --- a/app/src/command/c-update.h +++ b/app/src/command/c-update.h @@ -196,8 +196,8 @@ class CUpdate : public Command Qx::Error installUpdate(const QFileInfo& existingAppInfo) const; protected: - QList options() override; - QString name() override; + QList options() const override; + QString name() const override; Qx::Error perform() override; public: diff --git a/app/src/command/command.cpp b/app/src/command/command.cpp index 010fd9a..dec1087 100644 --- a/app/src/command/command.cpp +++ b/app/src/command/command.cpp @@ -102,7 +102,7 @@ CommandError Command::parse(const QStringList& commandLine) else { CommandError parseErr = CommandError(ERR_INVALID_ARGS).wDetails(mParser.errorText()); - mCore.postError(NAME, parseErr); + postError(parseErr); return parseErr; } } @@ -135,7 +135,7 @@ CommandError Command::checkRequiredOptions() void Command::showHelp() { - mCore.logEvent(name(), LOG_EVENT_C_HELP_SHOWN.arg(name())); + logEvent(LOG_EVENT_C_HELP_SHOWN.arg(name())); // Help string static QString helpStr; @@ -172,11 +172,22 @@ void Command::showHelp() } //Protected: -QList Command::options() { return CL_OPTIONS_STANDARD; } -QSet Command::requiredOptions() { return {}; } +QList Command::options() const { return CL_OPTIONS_STANDARD; } +QSet Command::requiredOptions() const { return {}; } + +// Notifications/Logging (core-forwarders) +void Command::logCommand(QString commandName) const {mCore.logCommand(name(), commandName); } +void Command::logCommandOptions(QString commandOptions) const {mCore.logCommandOptions(name(), commandOptions); } +void Command::logError(Qx::Error error) const {mCore.logError(name(), error); } +void Command::logEvent(QString event) const {mCore.logEvent(name(), event); } +void Command::logTask(const Task* task) const {mCore.logTask(name(), task); } +ErrorCode Command::logFinish(Qx::Error errorState) const {return mCore.logFinish(name(), errorState); } +void Command::postError(Qx::Error error, bool log) const {mCore.postError(name(), error, log); } +int Command::postBlockingError(Qx::Error error, bool log, QMessageBox::StandardButtons bs, QMessageBox::StandardButton def) const {return mCore.postBlockingError(name(), error, log); } //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) @@ -194,7 +205,7 @@ Qx::Error Command::process(const QStringList& commandLine) processError = checkRequiredOptions(); if(processError.isValid()) { - mCore.postError(NAME, processError); + postError(processError); return processError; } diff --git a/app/src/command/command.h b/app/src/command/command.h index 79a9169..54b1e5e 100644 --- a/app/src/command/command.h +++ b/app/src/command/command.h @@ -156,13 +156,24 @@ class Command protected: // Command specific - virtual QList options() = 0; - virtual QSet requiredOptions(); - virtual QString name() = 0; + virtual QList options() const = 0; + virtual QSet requiredOptions() const; + virtual QString name() const = 0; virtual Qx::Error perform() = 0; + // Notifications/Logging (core-forwarders) + void logCommand(QString commandName) const; + void logCommandOptions(QString commandOptions) const; + void logError(Qx::Error error) const; + void logEvent(QString event) const; + void logTask(const Task* task) const; + ErrorCode logFinish(Qx::Error errorState) const; + void postError(Qx::Error error, bool log = true) const; + int postBlockingError(Qx::Error error, bool log = true, QMessageBox::StandardButtons bs = QMessageBox::Ok, QMessageBox::StandardButton def = QMessageBox::NoButton) const; + public: virtual bool requiresFlashpoint() const; + virtual bool requiresServices() const; virtual bool autoBlockNewInstances() const; Qx::Error process(const QStringList& commandLine); }; diff --git a/app/src/command/title-command.cpp b/app/src/command/title-command.cpp index ab9b925..c0ba6d2 100644 --- a/app/src/command/title-command.cpp +++ b/app/src/command/title-command.cpp @@ -41,7 +41,7 @@ TitleCommand::TitleCommand(Core& coreRef) : //Private: Qx::Error TitleCommand::randomlySelectId(QUuid& mainIdBuffer, QUuid& subIdBuffer, Fp::Db::LibraryFilter lbFilter) { - mCore.logEvent(NAME, LOG_EVENT_SEL_RAND); + logEvent(LOG_EVENT_SEL_RAND); // Reset buffers mainIdBuffer = QUuid(); @@ -58,7 +58,7 @@ Qx::Error TitleCommand::randomlySelectId(QUuid& mainIdBuffer, QUuid& subIdBuffer searchError = database->queryAllGameIds(mainGameIdQuery, lbFilter); if(searchError.isValid()) { - mCore.postError(NAME, searchError); + postError(searchError); return searchError; } @@ -76,13 +76,13 @@ Qx::Error TitleCommand::randomlySelectId(QUuid& mainIdBuffer, QUuid& subIdBuffer if(!gameId.isNull()) playableIds.append(gameId); else - mCore.logError(NAME, Qx::GenericError(Qx::Warning, 12011, LOG_WRN_INVALID_RAND_ID.arg(gameIdString))); + logError(Qx::GenericError(Qx::Warning, 12011, LOG_WRN_INVALID_RAND_ID.arg(gameIdString))); } - mCore.logEvent(NAME, LOG_EVENT_PLAYABLE_COUNT.arg(QLocale(QLocale::system()).toString(playableIds.size()))); + logEvent(LOG_EVENT_PLAYABLE_COUNT.arg(QLocale(QLocale::system()).toString(playableIds.size()))); // Select main game mainIdBuffer = playableIds.value(QRandomGenerator::global()->bounded(playableIds.size())); - mCore.logEvent(NAME, LOG_EVENT_INIT_RAND_ID.arg(mainIdBuffer.toString(QUuid::WithoutBraces))); + logEvent(LOG_EVENT_INIT_RAND_ID.arg(mainIdBuffer.toString(QUuid::WithoutBraces))); // Get entry's playable additional apps Fp::Db::EntryFilter addAppFilter{.type = Fp::Db::EntryType::AddApp, .parent = mainIdBuffer, .playableOnly = true}; @@ -91,10 +91,10 @@ Qx::Error TitleCommand::randomlySelectId(QUuid& mainIdBuffer, QUuid& subIdBuffer searchError = database->queryEntrys(addAppQuery, addAppFilter); if(searchError.isValid()) { - mCore.postError(NAME, searchError); + postError(searchError); return searchError; } - mCore.logEvent(NAME, LOG_EVENT_INIT_RAND_PLAY_ADD_COUNT.arg(addAppQuery.size)); + logEvent(LOG_EVENT_INIT_RAND_PLAY_ADD_COUNT.arg(addAppQuery.size)); QVector playableSubIds; @@ -110,18 +110,18 @@ Qx::Error TitleCommand::randomlySelectId(QUuid& mainIdBuffer, QUuid& subIdBuffer if(!addAppId.isNull()) playableSubIds.append(addAppId); else - mCore.logError(NAME, Qx::GenericError(Qx::Warning, 12101, LOG_WRN_INVALID_RAND_ID.arg(addAppIdString))); + logError(Qx::GenericError(Qx::Warning, 12101, LOG_WRN_INVALID_RAND_ID.arg(addAppIdString))); } // Select final ID int randIndex = QRandomGenerator::global()->bounded(playableSubIds.size() + 1); if(randIndex == 0) - mCore.logEvent(NAME, LOG_EVENT_RAND_DET_PRIM); + logEvent(LOG_EVENT_RAND_DET_PRIM); else { subIdBuffer = playableSubIds.value(randIndex - 1); - mCore.logEvent(NAME, LOG_EVENT_RAND_DET_ADD_APP.arg(subIdBuffer.toString(QUuid::WithoutBraces))); + logEvent(LOG_EVENT_RAND_DET_ADD_APP.arg(subIdBuffer.toString(QUuid::WithoutBraces))); } // Return success @@ -130,7 +130,7 @@ Qx::Error TitleCommand::randomlySelectId(QUuid& mainIdBuffer, QUuid& subIdBuffer Qx::Error TitleCommand::getRandomSelectionInfo(QString& infoBuffer, QUuid mainId, QUuid subId) { - mCore.logEvent(NAME, LOG_EVENT_RAND_GET_INFO); + logEvent(LOG_EVENT_RAND_GET_INFO); // Reset buffer infoBuffer = QString(); @@ -145,11 +145,11 @@ Qx::Error TitleCommand::getRandomSelectionInfo(QString& infoBuffer, QUuid mainId Fp::Db* database = mCore.fpInstall().database(); // Get main entry info - std::variant entry_v; + Fp::Entry entry_v; searchError = database->getEntry(entry_v, mainId); if(searchError.isValid()) { - mCore.postError(NAME, searchError); + postError(searchError); return searchError; } @@ -171,7 +171,7 @@ Qx::Error TitleCommand::getRandomSelectionInfo(QString& infoBuffer, QUuid mainId searchError = database->getEntry(entry_v, subId); if(searchError.isValid()) { - mCore.postError(NAME, searchError); + postError(searchError); return searchError; } @@ -190,7 +190,7 @@ Qx::Error TitleCommand::getRandomSelectionInfo(QString& infoBuffer, QUuid mainId } //Protected: -QList TitleCommand::options() { return CL_OPTIONS_SPECIFIC + Command::options(); } +QList TitleCommand::options() const { return CL_OPTIONS_SPECIFIC + Command::options(); } Qx::Error TitleCommand::getTitleId(QUuid& id) { @@ -207,7 +207,7 @@ Qx::Error TitleCommand::getTitleId(QUuid& id) if((titleId = QUuid(idStr)).isNull()) { TitleCommandError err(TitleCommandError::InvalidId, idStr); - mCore.postError(NAME, err); + postError(err); return err; } } @@ -253,7 +253,7 @@ Qx::Error TitleCommand::getTitleId(QUuid& id) else { TitleCommandError err(TitleCommandError::InvalidRandomFilter, rawRandFilter); - mCore.postError(NAME, err); + postError(err); return err; } @@ -275,7 +275,7 @@ Qx::Error TitleCommand::getTitleId(QUuid& id) else { TitleCommandError err(TitleCommandError::MissingTitle); - mCore.postError(NAME, err); + postError(err); return err; } diff --git a/app/src/command/title-command.h b/app/src/command/title-command.h index 5bba30f..9f6cc18 100644 --- a/app/src/command/title-command.h +++ b/app/src/command/title-command.h @@ -135,7 +135,7 @@ class TitleCommand : public Command Qx::Error getRandomSelectionInfo(QString& infoBuffer, QUuid mainId, QUuid subId); protected: - virtual QList options() override; + virtual QList options() const override; Qx::Error getTitleId(QUuid& id); }; diff --git a/app/src/kernel/core.cpp b/app/src/kernel/core.cpp index 4f7c9b2..bf0b114 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 } @@ -166,16 +167,16 @@ Qx::Error Core::searchAndFilterEntity(QUuid& returnBuffer, QString name, bool ex if((searchError = mFlashpointInstall->database()->queryEntrys(searchResult, filter)).isValid()) { - postError(NAME, searchError); + postError(searchError); return searchError; } - logEvent(NAME, LOG_EVENT_TITLE_ID_COUNT.arg(searchResult.size).arg(name)); + logEvent(LOG_EVENT_TITLE_ID_COUNT.arg(searchResult.size).arg(name)); if(searchResult.size < 1) { CoreError err(CoreError::TitleNotFound, name); - postError(NAME, err); + postError(err); return err; } else if(searchResult.size == 1) @@ -189,19 +190,19 @@ Qx::Error Core::searchAndFilterEntity(QUuid& returnBuffer, QString name, bool ex Fp::Db::Table_Add_App::COL_ID; returnBuffer = QUuid(searchResult.result.value(idKey).toString()); - logEvent(NAME, LOG_EVENT_TITLE_ID_DETERMINED.arg(name, returnBuffer.toString(QUuid::WithoutBraces))); + logEvent(LOG_EVENT_TITLE_ID_DETERMINED.arg(name, returnBuffer.toString(QUuid::WithoutBraces))); return CoreError(); } else if (searchResult.size > FIND_ENTRY_LIMIT) { CoreError err(CoreError::TooManyResults, name); - postError(NAME, err); + postError(err); return err; } else { - logEvent(NAME, LOG_EVENT_TITLE_SEL_PROMNPT); + logEvent(LOG_EVENT_TITLE_SEL_PROMNPT); QHash idMap; QStringList idChoices; @@ -240,12 +241,12 @@ Qx::Error Core::searchAndFilterEntity(QUuid& returnBuffer, QString name, bool ex QString userChoice = requestItemSelection(isr); if(userChoice.isNull()) - logEvent(NAME, LOG_EVENT_TITLE_SEL_CANCELED); + logEvent(LOG_EVENT_TITLE_SEL_CANCELED); else { // Set return buffer returnBuffer = idMap.value(userChoice); // If user choice is null, this will - logEvent(NAME, LOG_EVENT_TITLE_ID_DETERMINED.arg(name, returnBuffer.toString(QUuid::WithoutBraces))); + logEvent(LOG_EVENT_TITLE_ID_DETERMINED.arg(name, returnBuffer.toString(QUuid::WithoutBraces))); } return CoreError(); @@ -270,23 +271,32 @@ void Core::logQtMessage(QtMsgType type, const QMessageLogContext& context, const switch (type) { case QtDebugMsg: - logEvent(NAME, u"SYSTEM DEBUG) "_s + msgWithContext); + logEvent(u"SYSTEM DEBUG) "_s + msgWithContext); break; case QtInfoMsg: - logEvent(NAME, u"SYSTEM INFO) "_s + msgWithContext); + logEvent(u"SYSTEM INFO) "_s + msgWithContext); break; case QtWarningMsg: - logError(NAME, CoreError(CoreError::InternalError, msgWithContext, Qx::Warning)); + logError(CoreError(CoreError::InternalError, msgWithContext, Qx::Warning)); break; case QtCriticalMsg: - logError(NAME, CoreError(CoreError::InternalError, msgWithContext, Qx::Err)); + logError(CoreError(CoreError::InternalError, msgWithContext, Qx::Err)); break; case QtFatalMsg: - logError(NAME, CoreError(CoreError::InternalError, msgWithContext, Qx::Critical)); + logError(CoreError(CoreError::InternalError, msgWithContext, Qx::Critical)); break; } } +void Core::logCommand(const QString& commandName) { logCommand(NAME, commandName); } +void Core::logCommandOptions(const QString& commandOptions) { logCommandOptions(NAME, commandOptions); } +void Core::logError(const Qx::Error& error) { logError(NAME, error); } +void Core::logEvent(const QString& event) { logEvent(NAME, event); } +void Core::logTask(const Task* task) { logTask(NAME, task); } +ErrorCode Core::logFinish(const Qx::Error& errorState) { return logFinish(NAME, errorState); } +void Core::postError(const Qx::Error& error, bool log) { postError(NAME, error, log); } +int Core::postBlockingError(const Qx::Error& error, bool log, QMessageBox::StandardButtons bs, QMessageBox::StandardButton def) { return postBlockingError(NAME, error, log); } + //Public: Qx::Error Core::initialize(QStringList& commandLine) { @@ -333,59 +343,91 @@ Qx::Error Core::initialize(QStringList& commandLine) // Open log Qx::IoOpReport logOpen = mLogger->openLog(); if(logOpen.isFailure()) - postError(NAME, Qx::Error(logOpen).setSeverity(Qx::Warning), false); + postError(Qx::Error(logOpen).setSeverity(Qx::Warning), false); // Log initialization step - logEvent(NAME, LOG_EVENT_INIT); + logEvent(LOG_EVENT_INIT); // Log global options - logEvent(NAME, LOG_EVENT_GLOBAL_OPT.arg(globalOptions)); + logEvent(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(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(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(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(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(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()); - postError(NAME, err); - return err; - } + // Return success + return CoreError(); +} + +void Core::setServicesMode(ServicesMode mode) +{ + logEvent(LOG_EVENT_MODE_SET.arg(ENUM_NAME(mode))); + mServicesMode = mode; + + if(mode == ServicesMode::Companion) + watchLauncher(); +} + +void Core::watchLauncher() +{ + logEvent(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::established, this, [this]{ + logEvent(LOG_EVENT_LAUNCHER_WATCH_HOOKED); + }); + connect(&mLauncherWatcher, &Qx::ProcessBider::errorOccurred, this, [this](Qx::ProcessBiderError err){ + logError(err); + }); + connect(&mLauncherWatcher, &Qx::ProcessBider::finished, this, [this]{ + // Launcher closed (or can't be hooked), need to bail + CoreError err(CoreError::CompanionModeLauncherClose, LOG_EVENT_LAUNCHER_CLOSED_RESULT); + postError(err); + emit abort(err); + }); + + mLauncherWatcher.start(); + // The bide is automatically abandoned when core, and therefore the bider, is destroyed } void Core::attachFlashpoint(std::unique_ptr flashpointInstall) @@ -394,14 +436,14 @@ void Core::attachFlashpoint(std::unique_ptr flashpointInstall) mFlashpointInstall = std::move(flashpointInstall); // Note install details - logEvent(NAME, LOG_EVENT_FLASHPOINT_VERSION_TXT.arg(mFlashpointInstall->nameVersionString())); - logEvent(NAME, LOG_EVENT_FLASHPOINT_VERSION.arg(mFlashpointInstall->version().toString())); - logEvent(NAME, LOG_EVENT_FLASHPOINT_EDITION.arg(ENUM_NAME(mFlashpointInstall->edition()))); - logEvent(NAME, LOG_EVENT_OUTFITTED_DAEMON.arg(ENUM_NAME(mFlashpointInstall->outfittedDaemon()))); + logEvent(LOG_EVENT_FLASHPOINT_VERSION_TXT.arg(mFlashpointInstall->nameVersionString())); + logEvent(LOG_EVENT_FLASHPOINT_VERSION.arg(mFlashpointInstall->version().toString())); + logEvent(LOG_EVENT_FLASHPOINT_EDITION.arg(ENUM_NAME(mFlashpointInstall->edition()))); + logEvent(LOG_EVENT_OUTFITTED_DAEMON.arg(ENUM_NAME(mFlashpointInstall->outfittedDaemon()))); // Initialize child process env vars QProcessEnvironment de = QProcessEnvironment::systemEnvironment(); - QString fpPath = mFlashpointInstall->fullPath(); + QString fpPath = mFlashpointInstall->dir().absolutePath(); #ifdef __linux__ // Add platform support environment variables @@ -453,70 +495,51 @@ void Core::attachFlashpoint(std::unique_ptr flashpointInstall) #endif } -// TODO: Might make sense to make this a function in libfp -QString Core::resolveTrueAppPath(const QString& appPath, const QString& platform) +QString Core::resolveFullAppPath(const QString& appPath, const QString& platform) { - // If appPath is absolute, convert it to relative temporarily - QString transformedPath = appPath; - - QString fpPath = mFlashpointInstall->fullPath(); - bool isFpAbsolute = transformedPath.startsWith(fpPath); - if(isFpAbsolute) - { - // Remove FP root and separator - transformedPath.remove(fpPath); - if(!transformedPath.isEmpty() && (transformedPath.front() == '/' || transformedPath.front() == '\\')) - transformedPath = transformedPath.mid(1); - } - - /* TODO: If this is made into a libfp function, isolate this part of it so it stays here, - * or add a map argument to the libfp function that allows for passing custom "swaps" - * and then call it with one for the following switch. - * - * CLIFp doesn't support the Launcher's built in browser (obviously), so manually - * override it with Basilisk. Basilisk was removed in FP11 but the app path overrides - * contains an entry for it that's appropriate on both platforms. - */ - if(transformedPath == u":browser-mode:"_s) - transformedPath = u"FPSoftware\\Basilisk-Portable\\Basilisk-Portable.exe"_s; - - // Resolve both swap types - transformedPath = mFlashpointInstall->resolveExecSwaps(transformedPath, platform); - transformedPath = mFlashpointInstall->resolveAppPathOverrides(transformedPath); + // We don't have a browser mode. Since Electron bundles chromium, chrome should give the closest experience to the launcher's browser mode. + static const QHash clifpOverrides{ + {u":browser-mode:"_s, u"FPSoftware\\startChrome.bat"_s} + }; - // Rebuild full path if applicable - if(isFpAbsolute) - transformedPath = fpPath + '/' + transformedPath; + const Fp::Toolkit* tk = mFlashpointInstall->toolkit(); - if(transformedPath != appPath) - logEvent(NAME, LOG_EVENT_APP_PATH_ALT.arg(appPath, transformedPath)); + QString swapPath = appPath; + if(tk->resolveTrueAppPath(swapPath, platform, clifpOverrides)) + logEvent(LOG_EVENT_APP_PATH_ALT.arg(appPath, swapPath)); - // Convert Windows separators to universal '/' - return transformedPath.replace('\\','/'); + return mFlashpointInstall->dir().absoluteFilePath(swapPath); } Qx::Error Core::findGameIdFromTitle(QUuid& returnBuffer, QString title, bool exactTitle) { - logEvent(NAME, LOG_EVENT_GAME_SEARCH.arg(title)); + logEvent(LOG_EVENT_GAME_SEARCH.arg(title)); return searchAndFilterEntity(returnBuffer, title, exactTitle); } Qx::Error Core::findAddAppIdFromName(QUuid& returnBuffer, QUuid parent, QString name, bool exactName) { - logEvent(NAME, LOG_EVENT_ADD_APP_SEARCH.arg(name, parent.toString())); + logEvent(LOG_EVENT_ADD_APP_SEARCH.arg(name, parent.toString())); return searchAndFilterEntity(returnBuffer, name, exactName, parent); } bool Core::blockNewInstances() { bool b = Qx::enforceSingleInstance(SINGLE_INSTANCE_ID); - logEvent(NAME, b ? LOG_EVENT_FURTHER_INSTANCE_BLOCK_SUCC : LOG_EVENT_FURTHER_INSTANCE_BLOCK_FAIL); + logEvent(b ? LOG_EVENT_FURTHER_INSTANCE_BLOCK_SUCC : LOG_EVENT_FURTHER_INSTANCE_BLOCK_FAIL); return b; } -CoreError Core::enqueueStartupTasks() +CoreError Core::enqueueStartupTasks(const QString& serverOverride) { - logEvent(NAME, LOG_EVENT_ENQ_START); + logEvent(LOG_EVENT_ENQ_START); + + if(mServicesMode == ServicesMode::Companion) + { + logEvent(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, @@ -529,16 +552,17 @@ CoreError Core::enqueueStartupTasks() xhostSet->setIdentifier(u"xhost Set"_s); xhostSet->setStage(Task::Stage::Startup); xhostSet->setExecutable(u"xhost"_s); - xhostSet->setDirectory(mFlashpointInstall->fullPath()); + xhostSet->setDirectory(mFlashpointInstall->dir()); xhostSet->setParameters({u"+SI:localuser:root"_s}); xhostSet->setProcessType(TExec::ProcessType::Blocking); mTaskQueue.push(xhostSet); - logTask(NAME, xhostSet); + logTask(xhostSet); } #endif // Get settings + const Fp::Toolkit* fpTk = mFlashpointInstall->toolkit(); Fp::Services fpServices = mFlashpointInstall->services(); Fp::Config fpConfig = mFlashpointInstall->config(); Fp::Preferences fpPreferences = mFlashpointInstall->preferences(); @@ -550,36 +574,37 @@ CoreError Core::enqueueStartupTasks() currentTask->setIdentifier(startEntry.filename); currentTask->setStage(Task::Stage::Startup); currentTask->setExecutable(startEntry.filename); - currentTask->setDirectory(mFlashpointInstall->fullPath() + '/' + startEntry.path); + currentTask->setDirectory(mFlashpointInstall->dir().absoluteFilePath(startEntry.path)); currentTask->setParameters(startEntry.arguments); currentTask->setProcessType(TExec::ProcessType::Blocking); mTaskQueue.push(currentTask); - logTask(NAME, currentTask); + logTask(currentTask); } // Add Server entry from services if applicable if(fpConfig.startServer) { - if(!fpServices.server.contains(fpPreferences.server)) + std::optional foundServer = fpTk->getServer(serverOverride); // Will pull fpPreferences.server if empty + if(!foundServer) { CoreError err(CoreError::ConfiguredServerMissing); - postError(NAME, err); + postError(err); return err; } - Fp::ServerDaemon configuredServer = fpServices.server.value(fpPreferences.server); + Fp::ServerDaemon server = foundServer.value(); TExec* serverTask = new TExec(this); serverTask->setIdentifier(u"Server"_s); serverTask->setStage(Task::Stage::Startup); - serverTask->setExecutable(configuredServer.filename); - serverTask->setDirectory(mFlashpointInstall->fullPath() + '/' + configuredServer.path); - serverTask->setParameters(configuredServer.arguments); - serverTask->setProcessType(configuredServer.kill ? TExec::ProcessType::Deferred : TExec::ProcessType::Detached); + serverTask->setExecutable(server.filename); + serverTask->setDirectory(mFlashpointInstall->dir().absoluteFilePath(server.path)); + serverTask->setParameters(server.arguments); + serverTask->setProcessType(server.kill ? TExec::ProcessType::Deferred : TExec::ProcessType::Detached); mTaskQueue.push(serverTask); - logTask(NAME, serverTask); + logTask(serverTask); } // Add Daemon entry from services @@ -589,12 +614,12 @@ CoreError Core::enqueueStartupTasks() currentTask->setIdentifier(u"Daemon"_s); currentTask->setStage(Task::Stage::Startup); currentTask->setExecutable(d.filename); - currentTask->setDirectory(mFlashpointInstall->fullPath() + '/' + d.path); + currentTask->setDirectory(mFlashpointInstall->dir().absoluteFilePath(d.path)); currentTask->setParameters(d.arguments); currentTask->setProcessType(d.kill ? TExec::ProcessType::Deferred : TExec::ProcessType::Detached); mTaskQueue.push(currentTask); - logTask(NAME, currentTask); + logTask(currentTask); } #ifdef __linux__ @@ -608,7 +633,7 @@ CoreError Core::enqueueStartupTasks() dockerWait->setTimeout(10000); mTaskQueue.push(dockerWait); - logTask(NAME, dockerWait); + logTask(dockerWait); } #endif @@ -622,7 +647,7 @@ CoreError Core::enqueueStartupTasks() initDelay->setDuration(1500); // NOTE: Might need to be made longer mTaskQueue.push(initDelay); - logTask(NAME, initDelay); + logTask(initDelay); // Return success return CoreError(); @@ -630,7 +655,14 @@ CoreError Core::enqueueStartupTasks() void Core::enqueueShutdownTasks() { - logEvent(NAME, LOG_EVENT_ENQ_STOP); + logEvent(LOG_EVENT_ENQ_STOP); + + if(mServicesMode == ServicesMode::Companion) + { + logEvent(LOG_EVENT_SERVICES_FROM_LAUNCHER); + return; + } + // Add Stop entries from services for(const Fp::StartStop& stopEntry : qxAsConst(mFlashpointInstall->services().stop)) { @@ -638,12 +670,12 @@ void Core::enqueueShutdownTasks() shutdownTask->setIdentifier(stopEntry.filename); shutdownTask->setStage(Task::Stage::Shutdown); shutdownTask->setExecutable(stopEntry.filename); - shutdownTask->setDirectory(mFlashpointInstall->fullPath() + '/' + stopEntry.path); + shutdownTask->setDirectory(mFlashpointInstall->dir().absoluteFilePath(stopEntry.path)); shutdownTask->setParameters(stopEntry.arguments); shutdownTask->setProcessType(TExec::ProcessType::Blocking); mTaskQueue.push(shutdownTask); - logTask(NAME, shutdownTask); + logTask(shutdownTask); } #ifdef __linux__ @@ -654,12 +686,12 @@ void Core::enqueueShutdownTasks() xhostClear->setIdentifier(u"xhost Clear"_s); xhostClear->setStage(Task::Stage::Shutdown); xhostClear->setExecutable(u"xhost"_s); - xhostClear->setDirectory(mFlashpointInstall->fullPath()); + xhostClear->setDirectory(mFlashpointInstall->dir()); xhostClear->setParameters({"-SI:localuser:root"}); xhostClear->setProcessType(TExec::ProcessType::Blocking); mTaskQueue.push(xhostClear); - logTask(NAME, xhostClear); + logTask(xhostClear); } #endif } @@ -667,12 +699,14 @@ void Core::enqueueShutdownTasks() #ifdef _WIN32 Qx::Error Core::conditionallyEnqueueBideTask(QFileInfo precedingAppInfo) { + const Fp::Toolkit* tk = mFlashpointInstall->toolkit(); + // Add wait for apps that involve secure player bool involvesSecurePlayer; - Qx::Error securePlayerCheckError = Fp::Install::appInvolvesSecurePlayer(involvesSecurePlayer, precedingAppInfo); + Qx::Error securePlayerCheckError = tk->appInvolvesSecurePlayer(involvesSecurePlayer, precedingAppInfo); if(securePlayerCheckError.isValid()) { - postError(NAME, securePlayerCheckError); + postError(securePlayerCheckError); return securePlayerCheckError; } @@ -680,10 +714,10 @@ Qx::Error Core::conditionallyEnqueueBideTask(QFileInfo precedingAppInfo) { TBideProcess* waitTask = new TBideProcess(this); waitTask->setStage(Task::Stage::Auxiliary); - waitTask->setProcessName(Fp::Install::SECURE_PLAYER_INFO.fileName()); + waitTask->setProcessName(tk->SECURE_PLAYER_INFO.fileName()); mTaskQueue.push(waitTask); - logTask(NAME, waitTask); + logTask(waitTask); } // Return success @@ -695,56 +729,27 @@ Qx::Error Core::conditionallyEnqueueBideTask(QFileInfo precedingAppInfo) Qx::Error Core::enqueueDataPackTasks(const Fp::GameData& gameData) { - logEvent(NAME, LOG_EVENT_ENQ_DATA_PACK); + logEvent(LOG_EVENT_ENQ_DATA_PACK); - // Extract relevant data - QString packDestFolderPath = mFlashpointInstall->fullPath() + '/' + mFlashpointInstall->preferences().dataPacksFolderPath; - QString packFileName = gameData.path(); - QString packSha256 = gameData.sha256(); - QString packParameters = gameData.parameters(); - QFile packFile(packDestFolderPath + '/' + packFileName); + const Fp::Toolkit* tk = mFlashpointInstall->toolkit(); - // Get current file checksum if it exists - bool checksumMatches = false; + QString packFilename = gameData.path(); + QString packPath = tk->datapackPath(gameData); - if(gameData.presentOnDisk() && packFile.exists()) + // Enqueue pack download if it's not available + if(!tk->datapackIsPresent(gameData)) { - // Checking the sum in addition to the flag is somewhat overkill, but may help in situations - // where the flag is set but the datapack's contents have changed - Qx::IoOpReport checksumReport = Qx::fileMatchesChecksum(checksumMatches, packFile, packSha256, QCryptographicHash::Sha256); - if(checksumReport.isFailure()) - logError(NAME, checksumReport); + logEvent(LOG_EVENT_DATA_PACK_MISS); - if(checksumMatches) - logEvent(NAME, LOG_EVENT_DATA_PACK_FOUND); - else - postError(NAME, CoreError(CoreError::DataPackSumMismatch, packFileName, Qx::Warning)); - } - else - logEvent(NAME, LOG_EVENT_DATA_PACK_MISS); - - // Enqueue pack download if it doesn't exist or is different than expected - if(!packFile.exists() || !checksumMatches) - { - if(!mFlashpointInstall->preferences().gameDataSources.contains(u"Flashpoint Project"_s)) - { - CoreError err(CoreError::DataPackSourceMissing, u"Flashpoint Project"_s); - postError(NAME, err); - return err; - } - - Fp::GameDataSource gameSource = mFlashpointInstall->preferences().gameDataSources.value(u"Flashpoint Project"_s); - QString gameSourceBase = gameSource.arguments.value(0); + QUrl packUrl = tk->datapackUrl(gameData); TDownload* downloadTask = new TDownload(this); downloadTask->setStage(Task::Stage::Auxiliary); - downloadTask->setDestinationPath(packDestFolderPath); - downloadTask->setDestinationFilename(packFileName); - downloadTask->setTargetFile(gameSourceBase + '/' + packFileName); - downloadTask->setSha256(packSha256); + downloadTask->setDescription(u"data pack "_s + packFilename); + downloadTask->addFile({.target = packUrl, .dest = packPath, .checksum = gameData.sha256()}); mTaskQueue.push(downloadTask); - logTask(NAME, downloadTask); + logTask(downloadTask); // Add task to update DB with onDiskState int gameDataId = gameData.id(); @@ -753,66 +758,79 @@ Qx::Error Core::enqueueDataPackTasks(const Fp::GameData& gameData) onDiskUpdateTask->setStage(Task::Stage::Auxiliary); onDiskUpdateTask->setDescription(u"Update GameData onDisk state."_s); onDiskUpdateTask->setAction([gameDataId, this]{ - return mFlashpointInstall->database()->updateGameDataOnDiskState(gameDataId, true); + return mFlashpointInstall->database()->updateGameDataOnDiskState({gameDataId}, true); }); mTaskQueue.push(onDiskUpdateTask); - logTask(NAME, onDiskUpdateTask); + logTask(onDiskUpdateTask); } + else + logEvent(LOG_EVENT_DATA_PACK_FOUND); - // Enqueue pack mount or extract - if(packParameters.contains(u"-extract"_s)) - { - logEvent(NAME, LOG_EVENT_DATA_PACK_NEEDS_EXTRACT); + // Handle datapack parameters + Fp::GameDataParameters param = gameData.parameters(); + if(param.hasError()) + postError(CoreError(CoreError::UnknownDatapackParam, param.errorString(), Qx::Warning)); - TExtract* extractTask = new TExtract(this); - extractTask->setStage(Task::Stage::Auxiliary); - extractTask->setPackPath(packDestFolderPath + '/' + packFileName); - extractTask->setPathInPack(u"content"_s); - extractTask->setDestinationPath(mFlashpointInstall->preferences().htdocsFolderPath); + if(param.isExtract()) + { + logEvent(LOG_EVENT_DATA_PACK_NEEDS_EXTRACT); - mTaskQueue.push(extractTask); - logTask(NAME, extractTask); + QDir extractRoot(mFlashpointInstall->dir().absoluteFilePath(mFlashpointInstall->preferences().htdocsFolderPath)); + QString marker = QDir::fromNativeSeparators(param.extractedMarkerFile()); + // Check if files are already present + if(!marker.isEmpty() && QFile::exists(extractRoot.absoluteFilePath(marker))) + logEvent(LOG_EVENT_DATA_PACK_ALREADY_EXTRACTED); + else + { + TExtract* extractTask = new TExtract(this); + extractTask->setStage(Task::Stage::Auxiliary); + extractTask->setPackPath(packPath); + extractTask->setPathInPack(u"content"_s); + extractTask->setDestinationPath(extractRoot.absolutePath()); + + mTaskQueue.push(extractTask); + logTask(extractTask); + } } else { - logEvent(NAME, LOG_EVENT_DATA_PACK_NEEDS_MOUNT); + logEvent(LOG_EVENT_DATA_PACK_NEEDS_MOUNT); // Create task TMount* mountTask = new TMount(this); mountTask->setStage(Task::Stage::Auxiliary); mountTask->setTitleId(gameData.gameId()); - mountTask->setPath(packDestFolderPath + '/' + packFileName); + mountTask->setPath(packPath); mountTask->setDaemon(mFlashpointInstall->outfittedDaemon()); mTaskQueue.push(mountTask); - logTask(NAME, mountTask); + logTask(mountTask); } // Return success return CoreError(); } -void Core::enqueueSingleTask(Task* task) { mTaskQueue.push(task); logTask(NAME, task); } -void Core::clearTaskQueue() { mTaskQueue = {}; } +void Core::enqueueSingleTask(Task* task) { mTaskQueue.push(task); logTask(task); } bool Core::isLogOpen() const { return mLogger->isOpen(); } -void Core::logCommand(QString src, QString commandName) +void Core::logCommand(const QString& src, const QString& commandName) { Qx::IoOpReport logReport = mLogger->recordGeneralEvent(src, COMMAND_LABEL.arg(commandName)); if(logReport.isFailure()) postError(src, Qx::Error(logReport).setSeverity(Qx::Warning), false); } -void Core::logCommandOptions(QString src, QString commandOptions) +void Core::logCommandOptions(const QString& src, const QString& commandOptions) { Qx::IoOpReport logReport = mLogger->recordGeneralEvent(src, COMMAND_OPT_LABEL.arg(commandOptions)); if(logReport.isFailure()) postError(src, Qx::Error(logReport).setSeverity(Qx::Warning), false); } -void Core::logError(QString src, Qx::Error error) +void Core::logError(const QString& src, const Qx::Error& error) { Qx::IoOpReport logReport = mLogger->recordErrorEvent(src, error); @@ -823,16 +841,16 @@ void Core::logError(QString src, Qx::Error error) mCriticalErrorOccurred = true; } -void Core::logEvent(QString src, QString event) +void Core::logEvent(const QString& src, const QString& event) { Qx::IoOpReport logReport = mLogger->recordGeneralEvent(src, event); if(logReport.isFailure()) postError(src, Qx::Error(logReport).setSeverity(Qx::Warning), false); } -void Core::logTask(QString src, const Task* task) { logEvent(src, LOG_EVENT_TASK_ENQ.arg(task->name(), task->members().join(u", "_s))); } +void Core::logTask(const QString& src, const Task* task) { logEvent(src, LOG_EVENT_TASK_ENQ.arg(task->name(), task->members().join(u", "_s))); } -ErrorCode Core::logFinish(QString src, Qx::Error errorState) +ErrorCode Core::logFinish(const QString& src, const Qx::Error& errorState) { if(mCriticalErrorOccurred) logEvent(src, LOG_ERR_CRITICAL); @@ -847,7 +865,7 @@ ErrorCode Core::logFinish(QString src, Qx::Error errorState) return code; } -void Core::postError(QString src, Qx::Error error, bool log) +void Core::postError(const QString& src, const Qx::Error& error, bool log) { // Logging if(log) @@ -867,7 +885,7 @@ void Core::postError(QString src, Qx::Error error, bool log) } } -int Core::postBlockingError(QString src, Qx::Error error, bool log, QMessageBox::StandardButtons bs, QMessageBox::StandardButton def) +int Core::postBlockingError(const QString& src, const Qx::Error& error, bool log, QMessageBox::StandardButtons bs, QMessageBox::StandardButton def) { // Logging if(log) diff --git a/app/src/kernel/core.h b/app/src/kernel/core.h index f134043..8c1a6cd 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,44 +32,46 @@ 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, ConfiguredServerMissing, - DataPackSumMismatch, - DataPackSourceMissing + UnknownDatapackParam }; - //-Class Variables------------------------------------------------------------- +//-Class Variables------------------------------------------------------------- private: static inline const QHash ERR_STRINGS{ {NoError, u""_s}, - {InternalError, u"Internal system error."_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}, - {ConfiguredServerMissing, u"The server specified in the Flashpoint config was not found within the Flashpoint services store."_s}, - {DataPackSumMismatch, u"The existing Data Pack of the selected title does not contain the data expected. It will be re-downloaded."_s}, - {DataPackSourceMissing, u"The expected primary data pack source was missing."_s} + {ConfiguredServerMissing, u"The configured server was not found within the Flashpoint services store."_s}, + {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; @@ -87,6 +90,7 @@ class Core : public QObject //-Class Enums----------------------------------------------------------------------- public: enum class NotificationVerbosity { Full, Quiet, Silent }; + enum ServicesMode { Standalone, Companion }; //-Class Structs--------------------------------------------------------------------- public: @@ -156,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; @@ -174,8 +179,13 @@ class Core : public QObject static inline const QString LOG_EVENT_DATA_PACK_FOUND = u"Title Data Pack with correct hash is already present, no need to download"_s; static inline const QString LOG_EVENT_DATA_PACK_NEEDS_MOUNT = u"Title Data Pack requires mounting"_s; static inline const QString LOG_EVENT_DATA_PACK_NEEDS_EXTRACT = u"Title Data Pack requires extraction"_s; + 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; + static inline const QString LOG_EVENT_LAUNCHER_WATCH_HOOKED = u"Launcher hooked for waiting"_s; + static inline const QString LOG_EVENT_LAUNCHER_CLOSED_RESULT = u"CLIFp cannot continue running in companion mode without the launcher's services."_s; // Logging - Title Search static inline const QString LOG_EVENT_GAME_SEARCH = u"Searching for game with title '%1'"_s; @@ -253,6 +263,7 @@ class Core : public QObject std::unique_ptr mLogger; // Processing + ServicesMode mServicesMode; bool mCriticalErrorOccurred; NotificationVerbosity mNotificationVerbosity; std::queue mTaskQueue; @@ -263,6 +274,7 @@ class Core : public QObject // Other QProcessEnvironment mChildTitleProcEnv; + Qx::ProcessBider mLauncherWatcher; //-Constructor---------------------------------------------------------------------------------------------------------- public: @@ -284,37 +296,55 @@ class Core : public QObject Qx::Error searchAndFilterEntity(QUuid& returnBuffer, QString name, bool exactName, QUuid parent = QUuid()); void logQtMessage(QtMsgType type, const QMessageLogContext& context, const QString& msg); + /* TODO: See if instead of repeating these with auto-source overloads everywhere if instead a template function can be made that just works + * in all places where core is available. This would likely require a public ::NAME static member for each type that uses core, though this + * would be tricky for the tasks that emit signals instead of using core directly. + */ + // Notifications/Logging (self-forwarders) + void logCommand(const QString& commandName); + void logCommandOptions(const QString& commandOptions); + void logError(const Qx::Error& error); + void logEvent(const QString& event); + void logTask(const Task* task); + ErrorCode logFinish(const Qx::Error& errorState); + void postError(const Qx::Error& error, bool log = true); + int postBlockingError(const Qx::Error& error, bool log = true, QMessageBox::StandardButtons bs = QMessageBox::Ok, QMessageBox::StandardButton def = QMessageBox::NoButton); + public: // Setup Qx::Error initialize(QStringList& commandLine); + void setServicesMode(ServicesMode mode = ServicesMode::Standalone); + void watchLauncher(); void attachFlashpoint(std::unique_ptr flashpointInstall); - // Helper - QString resolveTrueAppPath(const QString& appPath, const QString& platform); + // Helper (TODO: Move some of these to libfp Toolkit) + QString resolveFullAppPath(const QString& appPath, const QString& platform); Qx::Error findGameIdFromTitle(QUuid& returnBuffer, QString title, bool exactTitle = true); Qx::Error findAddAppIdFromName(QUuid& returnBuffer, QUuid parent, QString name, bool exactName = true); // Common bool blockNewInstances(); - CoreError enqueueStartupTasks(); + CoreError enqueueStartupTasks(const QString& serverOverride = {}); void enqueueShutdownTasks(); #ifdef _WIN32 Qx::Error conditionallyEnqueueBideTask(QFileInfo precedingAppInfo); #endif Qx::Error enqueueDataPackTasks(const Fp::GameData& gameData); void enqueueSingleTask(Task* task); - void clearTaskQueue(); // TODO: See if this can be done away with, it's awkward (i.e. not fill queue in first place). Think I tried to before though. // Notifications/Logging + /* TODO: Within each place that uses the log options that need the src parameter, like the Commands, and maybe even Core itself, add methods + * with the same names that call mCore.logX(NAME, ...) automatically so that NAME doesn't need to be passed every time + */ bool isLogOpen() const; - void logCommand(QString src, QString commandName); - void logCommandOptions(QString src, QString commandOptions); - void logError(QString src, Qx::Error error); - void logEvent(QString src, QString event); - void logTask(QString src, const Task* task); - ErrorCode logFinish(QString src, Qx::Error errorState); - void postError(QString src, Qx::Error error, bool log = true); - int postBlockingError(QString src, Qx::Error error, bool log = true, QMessageBox::StandardButtons bs = QMessageBox::Ok, QMessageBox::StandardButton def = QMessageBox::NoButton); + void logCommand(const QString& src, const QString& commandName); + void logCommandOptions(const QString& src, const QString& commandOptions); + void logError(const QString& src, const Qx::Error& error); + void logEvent(const QString& src, const QString& event); + void logTask(const QString& src, const Task* task); + ErrorCode logFinish(const QString& src, const Qx::Error& errorState); + void postError(const QString& src, const Qx::Error& error, bool log = true); + int postBlockingError(const QString& src, const Qx::Error& error, bool log = true, QMessageBox::StandardButtons bs = QMessageBox::Ok, QMessageBox::StandardButton def = QMessageBox::NoButton); void postMessage(const Message& msg); QString requestSaveFilePath(const SaveFileRequest& request); QString requestExistingDirPath(const ExistingDirRequest& request); @@ -323,6 +353,7 @@ class Core : public QObject bool requestQuestionAnswer(const QString& question); // Member access + ServicesMode mode() const; Fp::Install& fpInstall(); const QProcessEnvironment& childTitleProcessEnvironment(); NotificationVerbosity notifcationVerbosity() const; @@ -350,6 +381,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 b215947..cc7fb75 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){ + logEvent(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 @@ -77,8 +85,9 @@ void Driver::init() * would make deleting the object slightly tricky. This way it can just be parented to core */ DeferredProcessManager* dpm = new DeferredProcessManager(mCore); - connect(dpm, &DeferredProcessManager::eventOccurred, mCore, &Core::logEvent); - connect(dpm, &DeferredProcessManager::errorOccurred, mCore, &Core::logError); + // qOverload because it gets confused with the shorter versions within core even though they're private :/ + connect(dpm, &DeferredProcessManager::eventOccurred, mCore, qOverload(&Core::logEvent)); + connect(dpm, &DeferredProcessManager::errorOccurred, mCore, qOverload(&Core::logError)); TExec::installDeferredProcessManager(dpm); } @@ -94,7 +103,7 @@ void Driver::startNextTask() mCurrentTaskNumber++; // Log task start - mCore->logEvent(NAME, LOG_EVENT_TASK_START.arg(QString::number(mCurrentTaskNumber), + logEvent(LOG_EVENT_TASK_START.arg(QString::number(mCurrentTaskNumber), mCurrentTask->name(), ENUM_NAME(mCurrentTask->stage()))); @@ -102,7 +111,7 @@ void Driver::startNextTask() bool isShutdown = mCurrentTask->stage() == Task::Stage::Shutdown; if(mErrorStatus.isSet() && !isShutdown) { - mCore->logEvent(NAME, LOG_EVENT_TASK_SKIP_ERROR); + logEvent(LOG_EVENT_TASK_SKIP_ERROR); // Queue up finished handler directly (executes on next event loop cycle) since task was skipped // Can't connect directly because newer connect syntax doesn't support default args @@ -110,7 +119,7 @@ void Driver::startNextTask() } else if(mQuitRequested && !isShutdown) { - mCore->logEvent(NAME, LOG_EVENT_TASK_SKIP_QUIT); + logEvent(LOG_EVENT_TASK_SKIP_QUIT); // Queue up finished handler directly (executes on next event loop cycle) since task was skipped // Can't connect directly because newer connect syntax doesn't support default args @@ -120,7 +129,7 @@ void Driver::startNextTask() { // Connect task notifiers connect(mCurrentTask, &Task::notificationReady, mCore, &Core::postMessage); - connect(mCurrentTask, &Task::eventOccurred, mCore, &Core::logEvent); + connect(mCurrentTask, &Task::eventOccurred, mCore, qOverload(&Core::logEvent)); connect(mCurrentTask, &Task::errorOccurred, mCore, [this](QString taskName, Qx::Error error){ mCore->postError(taskName, error); // Can't connect directly because newer connect syntax doesn't support default args }); @@ -143,13 +152,13 @@ void Driver::startNextTask() void Driver::cleanup() { - mCore->logEvent(NAME, LOG_EVENT_CLEANUP_START); + logEvent(LOG_EVENT_CLEANUP_START); // Close each remaining child process - mCore->logEvent(NAME, LOG_EVENT_ENDING_CHILD_PROCESSES); + logEvent(LOG_EVENT_ENDING_CHILD_PROCESSES); TExec::deferredProcessManager()->closeProcesses(); - mCore->logEvent(NAME, LOG_EVENT_CLEANUP_FINISH); + logEvent(LOG_EVENT_CLEANUP_FINISH); } void Driver::finish() @@ -158,12 +167,21 @@ void Driver::finish() if(CUpdate::isUpdateCacheClearable()) { if(CUpdateError err = CUpdate::clearUpdateCache(); err.isValid()) - mCore->logError(NAME, err); + logError(err); else - mCore->logEvent(NAME, LOG_EVENT_CLEARED_UPDATE_CACHE); + logEvent(LOG_EVENT_CLEARED_UPDATE_CACHE); } - emit finished(mCore->logFinish(NAME, mErrorStatus.value())); + emit finished(logFinish(mErrorStatus.value())); +} + +void Driver::quit() +{ + mQuitRequested = true; + + // Stop current task (assuming it can be) + if(mCurrentTask) + mCurrentTask->stop(); } // Helper functions @@ -174,7 +192,7 @@ std::unique_ptr Driver::findFlashpointInstall() do { - mCore->logEvent(NAME, LOG_EVENT_FLASHPOINT_ROOT_CHECK.arg(QDir::toNativeSeparators(currentDir.absolutePath()))); + logEvent(LOG_EVENT_FLASHPOINT_ROOT_CHECK.arg(QDir::toNativeSeparators(currentDir.absolutePath()))); // Attempt to instantiate fpInstall = std::make_unique(currentDir.absolutePath()); @@ -182,7 +200,7 @@ std::unique_ptr Driver::findFlashpointInstall() { if(fpInstall->outfittedDaemon() == Fp::Daemon::Unknown) { - mCore->logError(NAME, Qx::GenericError(Qx::Warning, 12011, LOG_WARN_FP_UNRECOGNIZED_DAEMON)); + logError(Qx::GenericError(Qx::Warning, 12011, LOG_WARN_FP_UNRECOGNIZED_DAEMON)); fpInstall.reset(); } else @@ -190,7 +208,7 @@ std::unique_ptr Driver::findFlashpointInstall() } else { - mCore->logError(NAME, fpInstall->error().setSeverity(Qx::Warning)); + logError(fpInstall->error().setSeverity(Qx::Warning)); fpInstall.reset(); } } @@ -200,7 +218,15 @@ std::unique_ptr Driver::findFlashpointInstall() return std::move(fpInstall); } - +// Notifications/Logging (core-forwarders) +void Driver::logCommand(QString commandName) { Q_ASSERT(mCore); mCore->logCommand(NAME, commandName); } +void Driver::logCommandOptions(QString commandOptions) { Q_ASSERT(mCore); mCore->logCommandOptions(NAME, commandOptions); } +void Driver::logError(Qx::Error error) { Q_ASSERT(mCore); mCore->logError(NAME, error); } +void Driver::logEvent(QString event) { Q_ASSERT(mCore); mCore->logEvent(NAME, event); } +void Driver::logTask(const Task* task) { Q_ASSERT(mCore); mCore->logTask(NAME, task); } +ErrorCode Driver::logFinish(Qx::Error errorState) { Q_ASSERT(mCore); return mCore->logFinish(NAME, errorState); } +void Driver::postError(Qx::Error error, bool log) { Q_ASSERT(mCore); mCore->postError(NAME, error, log); } +int Driver::postBlockingError(Qx::Error error, bool log, QMessageBox::StandardButtons bs, QMessageBox::StandardButton def) { Q_ASSERT(mCore); return mCore->postBlockingError(NAME, error, log); } //-Slots-------------------------------------------------------------------------------- //Private: @@ -210,11 +236,11 @@ void Driver::completeTaskHandler(Qx::Error e) if(e.isValid()) { mErrorStatus = e; - mCore->logEvent(NAME, LOG_EVENT_TASK_FINISH_ERR.arg(mCurrentTaskNumber)); // Record early end of task + logEvent(LOG_EVENT_TASK_FINISH_ERR.arg(mCurrentTaskNumber)); // Record early end of task } // Cleanup handled task - mCore->logEvent(NAME, LOG_EVENT_TASK_FINISH.arg(mCurrentTaskNumber)); + logEvent(LOG_EVENT_TASK_FINISH.arg(mCurrentTaskNumber)); qxDelete(mCurrentTask); // Perform next task if any remain @@ -222,7 +248,7 @@ void Driver::completeTaskHandler(Qx::Error e) startNextTask(); else { - mCore->logEvent(NAME, LOG_EVENT_QUEUE_FINISH); + logEvent(LOG_EVENT_QUEUE_FINISH); cleanup(); finish(); } @@ -248,7 +274,7 @@ void Driver::drive() // Check for valid command if(CommandError ce = Command::isRegistered(commandStr); ce.isValid()) { - mCore->postError(NAME, ce); + postError(ce); mErrorStatus = ce; finish(); return; @@ -257,47 +283,52 @@ 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()) { DriverError err(DriverError::AlreadyOpen); - mCore->postError(NAME, err); + postError(err); mErrorStatus = err; finish(); 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); + logEvent(LOG_EVENT_FLASHPOINT_SEARCH); if(!(flashpointInstall = findFlashpointInstall())) { DriverError err(DriverError::InvalidInstall, ERR_INSTALL_INVALID_TIP); - mCore->postError(NAME, err); + postError(err); mErrorStatus = err; finish(); return; } - mCore->logEvent(NAME, LOG_EVENT_FLASHPOINT_LINK.arg(QDir::toNativeSeparators(flashpointInstall->fullPath()))); + logEvent(LOG_EVENT_FLASHPOINT_LINK.arg(QDir::toNativeSeparators(flashpointInstall->dir().absolutePath()))); // Insert into core 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()) @@ -307,11 +338,11 @@ void Driver::drive() } //-Handle Tasks----------------------------------------------------------------------- - mCore->logEvent(NAME, LOG_EVENT_TASK_COUNT.arg(mCore->taskCount())); + logEvent(LOG_EVENT_TASK_COUNT.arg(mCore->taskCount())); if(mCore->hasTasks()) { // Process task queue - mCore->logEvent(NAME, LOG_EVENT_QUEUE_START); + logEvent(LOG_EVENT_QUEUE_START); startNextTask(); } else @@ -329,14 +360,10 @@ void Driver::quitNow() // Handle quit state if(mQuitRequested) { - mCore->logEvent(NAME, LOG_EVENT_QUIT_REQUEST_REDUNDANT); + logEvent(LOG_EVENT_QUIT_REQUEST_REDUNDANT); return; } - mQuitRequested = true; - mCore->logEvent(NAME, LOG_EVENT_QUIT_REQUEST); - - // Stop current task (assuming it can be) - if(mCurrentTask) - mCurrentTask->stop(); + logEvent(LOG_EVENT_QUIT_REQUEST); + quit(); } diff --git a/app/src/kernel/driver.h b/app/src/kernel/driver.h index b9b22e8..1451ed7 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,10 +124,21 @@ class Driver : public QObject void cleanup(); void finish(); + void quit(); // Helper std::unique_ptr findFlashpointInstall(); + // Notifications/Logging (core-forwarders) + void logCommand(QString commandName); + void logCommandOptions(QString commandOptions); + void logError(Qx::Error error); + void logEvent(QString event); + void logTask(const Task* task); + ErrorCode logFinish(Qx::Error errorState); + void postError(Qx::Error error, bool log = true); + int postBlockingError(Qx::Error error, bool log = true, QMessageBox::StandardButtons bs = QMessageBox::Ok, QMessageBox::StandardButton def = QMessageBox::NoButton); + //-Signals & Slots------------------------------------------------------------------------------------------------------------ private slots: void completeTaskHandler(Qx::Error e = {}); diff --git a/app/src/task/t-bideprocess.cpp b/app/src/task/t-bideprocess.cpp index e380e48..36e3032 100644 --- a/app/src/task/t-bideprocess.cpp +++ b/app/src/task/t-bideprocess.cpp @@ -7,22 +7,22 @@ //-Constructor------------------------------------------------------------- //Private: -TBideProcessError::TBideProcessError(Type t, const QString& s) : +TBideProcessError::TBideProcessError(const QString& pn, Type t) : mType(t), - mSpecific(s) + mProcessName(pn) {} //-Instance Functions------------------------------------------------------------- //Public: bool TBideProcessError::isValid() const { return mType != NoError; } -QString TBideProcessError::specific() const { return mSpecific; } TBideProcessError::Type TBideProcessError::type() const { return mType; } +QString TBideProcessError::processName() const { return mProcessName; } //Private: -Qx::Severity TBideProcessError::deriveSeverity() const { return Qx::Err; } +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 mSpecific; } +QString TBideProcessError::deriveSecondary() const { return mProcessName; } //=============================================================================================================== // TBideProcess @@ -31,17 +31,27 @@ QString TBideProcessError::deriveSecondary() const { return mSpecific; } //-Constructor-------------------------------------------------------------------- //Public: TBideProcess::TBideProcess(QObject* parent) : - Task(parent), - mProcessBider(nullptr, STANDARD_GRACE) + Task(parent) { // Setup bider - 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); + mProcessBider.setInitialGrace(true); // Process will be stopped at first + connect(&mProcessBider, &Qx::ProcessBider::established, this, [this]{ + emitEventOccurred(LOG_EVENT_BIDE_RUNNING.arg(mProcessName)); + emitEventOccurred(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]{ + emitEventOccurred(LOG_EVENT_BIDE_QUIT.arg(mProcessName)); }); - connect(&mProcessBider, &ProcessBider::bideFinished, this, &TBideProcess::postBide); + connect(&mProcessBider, &Qx::ProcessBider::graceStarted, this, [this]{ + emitEventOccurred(LOG_EVENT_BIDE_GRACE.arg(QString::number(grace.count()), mProcessName)); + }); + connect(&mProcessBider, &Qx::ProcessBider::errorOccurred, this, [this](Qx::ProcessBiderError err){ + emitErrorOccurred(err); + }); + connect(&mProcessBider, &Qx::ProcessBider::finished, this, &TBideProcess::postBide); } //-Instance Functions------------------------------------------------------------- @@ -61,22 +71,28 @@ void TBideProcess::setProcessName(QString processName) { mProcessName = processN void TBideProcess::perform() { // Start bide - mProcessBider.start(mProcessName); + mProcessBider.setProcessName(mProcessName); + mProcessBider.start(); } void TBideProcess::stop() { - if(mProcessBider.isRunning()) + if(mProcessBider.isBiding()) { - emit eventOccurred(NAME, LOG_EVENT_STOPPING_BIDE_PROCESS); - if(!mProcessBider.closeProcess()) - emit errorOccurred(NAME, TBideProcessError(TBideProcessError::CantClose)); + emitEventOccurred(LOG_EVENT_STOPPING_BIDE_PROCESS); + 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 + { + emitEventOccurred(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 eebb9cd..9c4e6f9 100644 --- a/app/src/task/t-bideprocess.h +++ b/app/src/task/t-bideprocess.h @@ -2,44 +2,43 @@ #define TBIDEPROCESS_H // Qx Includes -#include +#include // Project Includes #include "task/task.h" -#include "tools/processbider.h" -class QX_ERROR_TYPE(TBideProcessError, "TBideProcessError", 1251) +class QX_ERROR_TYPE(TBideProcessError, "TBideError", 1256) { friend class TBideProcess; - //-Class Enums------------------------------------------------------------- +//-Class Enums------------------------------------------------------------- public: enum Type { - NoError = 0, - CantClose = 1, + NoError, + BideFail, }; - //-Class Variables------------------------------------------------------------- +//-Class Variables------------------------------------------------------------- private: static inline const QHash ERR_STRINGS{ {NoError, u""_s}, - {CantClose, u"Could not automatically end the running title! It will have to be closed manually."_s}, + {BideFail, u"Could not bide on process."_s} }; - //-Instance Variables------------------------------------------------------------- +//-Instance Variables------------------------------------------------------------- private: Type mType; - QString mSpecific; + QString mProcessName; - //-Constructor------------------------------------------------------------- +//-Constructor------------------------------------------------------------- private: - TBideProcessError(Type t = NoError, const QString& s = {}); + TBideProcessError(const QString& pn = {}, Type t = NoError); - //-Instance Functions------------------------------------------------------------- +//-Instance Functions------------------------------------------------------------- public: bool isValid() const; Type type() const; - QString specific() const; + QString processName() const; private: Qx::Severity deriveSeverity() const override; @@ -57,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; @@ -91,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/task/t-download.cpp b/app/src/task/t-download.cpp index 3fadb9b..439fd09 100644 --- a/app/src/task/t-download.cpp +++ b/app/src/task/t-download.cpp @@ -35,27 +35,25 @@ TDownload::TDownload(QObject* parent) : { // Setup download manager mDownloadManager.setOverwrite(true); - - // Since this is only for one download, the size will be adjusted to the correct total as soon as the download starts - mDownloadManager.setSkipEnumeration(true); + mDownloadManager.setVerificationMethod(QCryptographicHash::Sha256); // Download event handlers connect(&mDownloadManager, &Qx::AsyncDownloadManager::sslErrors, this, [this](Qx::Error errorMsg, bool* ignore) { int choice; - emit blockingErrorOccurred(NAME, &choice, errorMsg, QMessageBox::Yes | QMessageBox::No); + emitBlockingErrorOccurred(&choice, errorMsg, QMessageBox::Yes | QMessageBox::No); *ignore = choice == QMessageBox::Yes; }); connect(&mDownloadManager, &Qx::AsyncDownloadManager::authenticationRequired, this, [this](QString prompt) { - emit eventOccurred(NAME, LOG_EVENT_DOWNLOAD_AUTH.arg(prompt)); + emitEventOccurred(LOG_EVENT_DOWNLOAD_AUTH.arg(prompt)); }); connect(&mDownloadManager, &Qx::AsyncDownloadManager::preSharedKeyAuthenticationRequired, this, [this](QString prompt) { - emit eventOccurred(NAME, LOG_EVENT_DOWNLOAD_AUTH.arg(prompt)); + emitEventOccurred(LOG_EVENT_DOWNLOAD_AUTH.arg(prompt)); }); connect(&mDownloadManager, &Qx::AsyncDownloadManager::proxyAuthenticationRequired, this, [this](QString prompt) { - emit eventOccurred(NAME, LOG_EVENT_DOWNLOAD_AUTH.arg(prompt)); + emitEventOccurred(LOG_EVENT_DOWNLOAD_AUTH.arg(prompt)); }); connect(&mDownloadManager, &Qx::AsyncDownloadManager::downloadTotalChanged, this, &TDownload::longTaskTotalChanged); @@ -69,37 +67,40 @@ QString TDownload::name() const { return NAME; } QStringList TDownload::members() const { QStringList ml = Task::members(); - ml.append(u".destinationPath() = \""_s + QDir::toNativeSeparators(mDestinationPath) + u"\""_s); - ml.append(u".destinationFilename() = \""_s + mDestinationFilename + u"\""_s); - ml.append(u".targetFile() = \""_s + mTargetFile.toString() + u"\""_s); - ml.append(u".sha256() = "_s + mSha256); + + QString files = u".files() = {\n"_s; + for(auto i = 0; i < 10 && i < mFiles.size(); i++) + { + auto f = mFiles.at(i); + bool sum = !f.checksum.isEmpty(); + files += u"\t"_s + FILE_DOWNLOAD_TEMPLATE.arg(f.target.toString(), f.dest, sum ? f.checksum : FILE_NO_CHECKSUM) + '\n'; + } + if(mFiles.size() > 10) + files += FILE_DOWNLOAD_ELIDE.arg(mFiles.size() - 10) + '\n'; + files += u"}"_s; + ml.append(files); + ml.append(u".description() = \""_s + mDescription + u"\""_s); + return ml; } -QString TDownload::destinationPath() const { return mDestinationPath; } -QString TDownload::destinationFilename() const { return mDestinationFilename; } -QUrl TDownload::targetFile() const { return mTargetFile; } -QString TDownload::sha256() const { return mSha256; } +bool TDownload::isEmpty() const { return mFiles.isEmpty(); } +qsizetype TDownload::fileCount() const { return mFiles.size(); } +QList TDownload::files() const { return mFiles; } +QString TDownload::description() const { return mDescription; } -void TDownload::setDestinationPath(QString path) { mDestinationPath = path; } -void TDownload::setDestinationFilename(QString filename) { mDestinationFilename = filename; } -void TDownload::setTargetFile(QUrl targetFile) { mTargetFile = targetFile; } -void TDownload::setSha256(QString sha256) { mSha256 = sha256; } +void TDownload::addFile(const Qx::DownloadTask file) { mFiles.append(file); } +void TDownload::setDescription(const QString& desc) { mDescription = desc; } void TDownload::perform() { - // Setup download - QFile file(mDestinationPath + '/' + mDestinationFilename); - QFileInfo fileInfo(file); - Qx::DownloadTask download{ - .target = mTargetFile, - .dest = fileInfo.absoluteFilePath() - }; - mDownloadManager.appendTask(download); + // Add files + for(const auto& f : mFiles) + mDownloadManager.appendTask(f); // Log/label string - QString label = LOG_EVENT_DOWNLOADING_FILE.arg(fileInfo.fileName()); - emit eventOccurred(NAME, label); + QString label = LOG_EVENT_DOWNLOAD.arg(mDescription); + emitEventOccurred(label); // Start download emit longTaskStarted(label); @@ -110,7 +111,7 @@ void TDownload::stop() { if(mDownloadManager.isProcessing()) { - emit eventOccurred(NAME, LOG_EVENT_STOPPING_DOWNLOADS); + emitEventOccurred(LOG_EVENT_STOPPING_DOWNLOADS); mDownloadManager.abort(); } } @@ -123,29 +124,12 @@ void TDownload::postDownload(Qx::DownloadManagerReport downloadReport) // Handle result emit longTaskFinished(); - if(downloadReport.wasSuccessful()) - { - // Confirm checksum is correct, if supplied - if(!mSha256.isEmpty()) - { - QFile file(mDestinationPath + '/' + mDestinationFilename); - bool checksumMatch; - Qx::IoOpReport cr = Qx::fileMatchesChecksum(checksumMatch, file, mSha256, QCryptographicHash::Sha256); - if(cr.isFailure() || !checksumMatch) - { - TDownloadError err(TDownloadError::ChecksumMismatch, cr.isFailure() ? cr.outcomeInfo() : u""_s); - errorStatus = err; - emit errorOccurred(NAME, errorStatus); - } - } - - emit eventOccurred(NAME, LOG_EVENT_DOWNLOAD_SUCC); - } + if(downloadReport.wasSuccessful()) + emitEventOccurred(LOG_EVENT_DOWNLOAD_SUCC); else { - TDownloadError err(TDownloadError::Incomeplete, downloadReport.outcomeString()); - errorStatus = err; - emit errorOccurred(NAME, errorStatus); + errorStatus = TDownloadError(TDownloadError::Incomeplete, downloadReport.outcomeString()); + emitErrorOccurred(errorStatus); } emit complete(errorStatus); diff --git a/app/src/task/t-download.h b/app/src/task/t-download.h index ff40bb9..11e8055 100644 --- a/app/src/task/t-download.h +++ b/app/src/task/t-download.h @@ -11,33 +11,31 @@ class QX_ERROR_TYPE(TDownloadError, "TDownloadError", 1252) { friend class TDownload; - //-Class Enums------------------------------------------------------------- +//-Class Enums------------------------------------------------------------- public: enum Type { NoError = 0, - ChecksumMismatch = 1, Incomeplete = 2 }; - //-Class Variables------------------------------------------------------------- +//-Class Variables------------------------------------------------------------- private: static inline const QHash ERR_STRINGS{ {NoError, u""_s}, - {ChecksumMismatch, u"The file's checksum does not match its record!"_s}, - {Incomeplete, u"The download could not be completed."_s} + {Incomeplete, u"The download(s) could not be completed."_s} }; - //-Instance Variables------------------------------------------------------------- +//-Instance Variables------------------------------------------------------------- private: Type mType; QString mSpecific; - //-Constructor------------------------------------------------------------- +//-Constructor------------------------------------------------------------- private: TDownloadError(Type t = NoError, const QString& s = {}); - //-Instance Functions------------------------------------------------------------- +//-Instance Functions------------------------------------------------------------- public: bool isValid() const; Type type() const; @@ -59,25 +57,24 @@ class TDownload : public Task static inline const QString NAME = u"TDownload"_s; // Logging - static inline const QString LOG_EVENT_DOWNLOADING_FILE = u"Downloading file %1"_s; - static inline const QString LOG_EVENT_DOWNLOAD_SUCC = u"File downloaded successfully"_s; + static inline const QString LOG_EVENT_DOWNLOAD = u"Downloading %1"_s; + static inline const QString LOG_EVENT_DOWNLOAD_SUCC = u"File(s) downloaded successfully"_s; static inline const QString LOG_EVENT_DOWNLOAD_AUTH = u"File download unexpectedly requires authentication (%1)"_s; static inline const QString LOG_EVENT_STOPPING_DOWNLOADS = u"Stopping current download(s)..."_s; + // Members + static inline const QString FILE_NO_CHECKSUM = u"NO SUM"_s; + static inline const QString FILE_DOWNLOAD_TEMPLATE = uR"("%1" -> "%2" (%3))"_s; + static inline const QString FILE_DOWNLOAD_ELIDE = u"+%1 more..."_s; + //-Instance Variables------------------------------------------------------------------------------------------------ private: // Functional Qx::AsyncDownloadManager mDownloadManager; - /* NOTE: If it ever becomes required to perform multiple downloads in a run the DM instance should - * be made a static member of TDownload, or all downloads need to be determined at the same time - * and the task made capable of holding all of them - */ // Data - QString mDestinationPath; - QString mDestinationFilename; - QUrl mTargetFile; - QString mSha256; + QList mFiles; + QString mDescription; //-Constructor---------------------------------------------------------------------------------------------------------- public: @@ -88,15 +85,13 @@ class TDownload : public Task QString name() const override; QStringList members() const override; - QString destinationPath() const; - QString destinationFilename() const; - QUrl targetFile() const; - QString sha256() const; + bool isEmpty() const; + qsizetype fileCount() const; + QList files() const; + QString description() const; - void setDestinationPath(QString path); - void setDestinationFilename(QString filename); - void setTargetFile(QUrl targetFile); - void setSha256(QString sha256); + void addFile(const Qx::DownloadTask file); + void setDescription(const QString& desc); void perform() override; void stop() override; diff --git a/app/src/task/t-exec.cpp b/app/src/task/t-exec.cpp index 96ae23f..8318011 100644 --- a/app/src/task/t-exec.cpp +++ b/app/src/task/t-exec.cpp @@ -85,7 +85,7 @@ QString TExec::createEscapedShellArguments() QString args = std::get(mParameters); escapedArgs = escapeForShell(args); if(args != escapedArgs) - emit eventOccurred(NAME, LOG_EVENT_ARGS_ESCAPED.arg(args, escapedArgs)); + emitEventOccurred(LOG_EVENT_ARGS_ESCAPED.arg(args, escapedArgs)); } else { @@ -99,7 +99,7 @@ QString TExec::createEscapedShellArguments() QStringList rebuild = QProcess::splitCommand(escapedArgs); if(rebuild != parameters) { - emit eventOccurred(NAME, LOG_EVENT_ARGS_ESCAPED.arg(u"{\""_s + parameters.join(uR"(", ")"_s) + u"\"}"_s, + emitEventOccurred(LOG_EVENT_ARGS_ESCAPED.arg(u"{\""_s + parameters.join(uR"(", ")"_s) + u"\"}"_s, u"{\""_s + rebuild.join(uR"(", ")"_s) + u"\"}"_s)); } } @@ -107,6 +107,46 @@ QString TExec::createEscapedShellArguments() return escapedArgs; } +void TExec::removeRedundantFullQuotes(QProcess& process) +{ + /* Sometimes service arguments have been observed to be "pre-prepped" for shell use by being fully quoted even + * when not needed. This is an issue since QProcess will quote non-native arguments automatically when not using + * the shell, which we don't for most things other than scripts, so we have to remove such quotes here. Note this + * affects all execution tasks though, not just services. + */ + QStringList args = process.arguments(); + for(QString& a : args) + { + // Determine if arg is simply fully quoted + if(a.size() < 3 || (a.front() != '"' && a.back() != '"')) // min 3 maintains " and "" which theoretically could be significant + continue; + + QStringView inner(a.cbegin() + 1, a.cend() - 1); + bool redundant = true; + bool escaped = false; + for(const QChar& c : inner) + { + if(c == '\\') + escaped = true; + else if(c == '"' && !escaped) + { + redundant = false; + break; + } + else + escaped = false; + } + + if(redundant) + { + emitEventOccurred(LOG_EVENT_REMOVED_REDUNDANT_QUOTES.arg(a)); + a = inner.toString(); + } + } + + process.setArguments(args); +} + TExecError TExec::cleanStartProcess(QProcess* process) { // Note directories @@ -115,27 +155,27 @@ TExecError TExec::cleanStartProcess(QProcess* process) // Go to working directory QDir::setCurrent(newDirPath); - emit eventOccurred(NAME, LOG_EVENT_CD.arg(QDir::toNativeSeparators(newDirPath))); + emitEventOccurred(LOG_EVENT_CD.arg(QDir::toNativeSeparators(newDirPath))); // Start process process->start(); - emit eventOccurred(NAME, LOG_EVENT_STARTING.arg(mIdentifier, process->program())); + emitEventOccurred(LOG_EVENT_STARTING.arg(mIdentifier, process->program())); // Return to previous working directory QDir::setCurrent(currentDirPath); - emit eventOccurred(NAME, LOG_EVENT_CD.arg(QDir::toNativeSeparators(currentDirPath))); + emitEventOccurred(LOG_EVENT_CD.arg(QDir::toNativeSeparators(currentDirPath))); // Make sure process starts if(!process->waitForStarted()) { TExecError err(TExecError::CouldNotStart, ERR_DETAILS_TEMPLATE.arg(process->program(), ENUM_NAME(process->error()))); - emit errorOccurred(NAME, err); + emitErrorOccurred(err); delete process; // Clear finished process handle from heap return err; } // Return success - emit eventOccurred(NAME, LOG_EVENT_STARTED_PROCESS.arg(mIdentifier)); + emitEventOccurred(LOG_EVENT_STARTED_PROCESS.arg(mIdentifier)); return TExecError(); } @@ -171,20 +211,21 @@ void TExec::setIdentifier(QString identifier) { mIdentifier = identifier; } void TExec::perform() { - emit eventOccurred(NAME, LOG_EVENT_PREPARING_PROCESS.arg(ENUM_NAME(mProcessType), mIdentifier, mExecutable)); + emitEventOccurred(LOG_EVENT_PREPARING_PROCESS.arg(ENUM_NAME(mProcessType), mIdentifier, mExecutable)); // Get final executable path QString execPath = resolveExecutablePath(); if(execPath.isEmpty()) { TExecError err(TExecError::CouldNotFind, mExecutable, mStage == Stage::Shutdown ? Qx::Err : Qx::Critical); - emit errorOccurred(NAME, err); + emitErrorOccurred(err); emit complete(err); return; } // Prepare process object QProcess* taskProcess = prepareProcess(QFileInfo(execPath)); + removeRedundantFullQuotes(*taskProcess); logPreparedProcess(taskProcess); // Set common process properties @@ -234,7 +275,7 @@ void TExec::perform() if(!taskProcess->startDetached()) { TExecError err(TExecError::CouldNotStart, ERR_DETAILS_TEMPLATE.arg(taskProcess->program(), ENUM_NAME(taskProcess->error()))); - emit errorOccurred(NAME, err); + emitErrorOccurred(err); emit complete(err); return; } @@ -249,7 +290,7 @@ void TExec::stop() { if(mBlockingProcessManager) { - emit eventOccurred(NAME, LOG_EVENT_STOPPING_BLOCKING_PROCESS.arg(mIdentifier)); + emitEventOccurred(LOG_EVENT_STOPPING_BLOCKING_PROCESS.arg(mIdentifier)); mBlockingProcessManager->closeProcess(); } } diff --git a/app/src/task/t-exec.h b/app/src/task/t-exec.h index 844a060..ac5342f 100644 --- a/app/src/task/t-exec.h +++ b/app/src/task/t-exec.h @@ -73,6 +73,7 @@ class TExec : public Task // Logging - Process Prep static inline const QString LOG_EVENT_PREPARING_PROCESS = u"Preparing %1 process '%2' (%3)..."_s; + static inline const QString LOG_EVENT_REMOVED_REDUNDANT_QUOTES = u"Removed unnecessary outer quotes on argument (%1)."_s; static inline const QString LOG_EVENT_FINAL_EXECUTABLE = u"Final Executable: %1"_s; static inline const QString LOG_EVENT_FINAL_PARAMETERS = u"Final Parameters: %1"_s; @@ -133,6 +134,7 @@ class TExec : public Task QString escapeForShell(const QString& argStr); QString createEscapedShellArguments(); QProcess* prepareProcess(const QFileInfo& execInfo); + void removeRedundantFullQuotes(QProcess& process); TExecError cleanStartProcess(QProcess* process); // Logging diff --git a/app/src/task/t-exec_linux.cpp b/app/src/task/t-exec_linux.cpp index 4d57b16..d4fe9a6 100644 --- a/app/src/task/t-exec_linux.cpp +++ b/app/src/task/t-exec_linux.cpp @@ -125,10 +125,11 @@ QString TExec::escapeForShell(const QString& argStr) QString escapedArgs; bool inQuotes = false; + auto lastQuoteIdx = argStr.lastIndexOf('"'); // If uneven number of quotes, treat last quote as a regular char for(int i = 0; i < argStr.size(); i++) { const QChar& chr = argStr.at(i); - if(chr== '"' && (inQuotes || i != argStr.lastIndexOf('"'))) + if(chr== '"' && (inQuotes || i != lastQuoteIdx)) inQuotes = !inQuotes; if(inQuotes) diff --git a/app/src/task/t-exec_win.cpp b/app/src/task/t-exec_win.cpp index 142bf42..8a051b6 100644 --- a/app/src/task/t-exec_win.cpp +++ b/app/src/task/t-exec_win.cpp @@ -86,10 +86,11 @@ QString TExec::escapeForShell(const QString& argStr) QString escapedArgs; bool inQuotes = false; + auto lastQuoteIdx = argStr.lastIndexOf('"'); // If uneven number of quotes, treat last quote as a regular char for(int i = 0; i < argStr.size(); i++) { const QChar& chr = argStr.at(i); - if(chr== '"' && (inQuotes || i != argStr.lastIndexOf('"'))) + if(chr== '"' && (inQuotes || i != lastQuoteIdx)) inQuotes = !inQuotes; escapedArgs.append((!inQuotes && escapeChars.contains(chr)) ? '^' + chr : chr); @@ -112,8 +113,8 @@ QProcess* TExec::prepareProcess(const QFileInfo& execInfo) void TExec::logPreparedProcess(const QProcess* process) { - emit eventOccurred(NAME, LOG_EVENT_FINAL_EXECUTABLE.arg(process->program())); - emit eventOccurred(NAME, LOG_EVENT_FINAL_PARAMETERS.arg(!process->nativeArguments().isEmpty() ? + emitEventOccurred(LOG_EVENT_FINAL_EXECUTABLE.arg(process->program())); + emitEventOccurred(LOG_EVENT_FINAL_PARAMETERS.arg(!process->nativeArguments().isEmpty() ? process->nativeArguments() : !process->arguments().isEmpty() ? u"{\""_s + process->arguments().join(uR"(", ")"_s) + u"\"}"_s : diff --git a/app/src/task/t-extra.cpp b/app/src/task/t-extra.cpp index 6c5231d..d4a1015 100644 --- a/app/src/task/t-extra.cpp +++ b/app/src/task/t-extra.cpp @@ -62,12 +62,12 @@ void TExtra::perform() { // Open extra QDesktopServices::openUrl(QUrl::fromLocalFile(mDirectory.absolutePath())); - emit eventOccurred(NAME, LOG_EVENT_SHOW_EXTRA.arg(QDir::toNativeSeparators(mDirectory.path()))); + emitEventOccurred(LOG_EVENT_SHOW_EXTRA.arg(QDir::toNativeSeparators(mDirectory.path()))); } else { errorStatus = TExtraError(TExtraError::NotFound, QDir::toNativeSeparators(mDirectory.path())); - emit errorOccurred(NAME, errorStatus); + emitErrorOccurred(errorStatus); } emit complete(errorStatus); diff --git a/app/src/task/t-extract.cpp b/app/src/task/t-extract.cpp index 19c90b7..3500644 100644 --- a/app/src/task/t-extract.cpp +++ b/app/src/task/t-extract.cpp @@ -215,13 +215,13 @@ void TExtract::perform() { // Log string QFileInfo packFileInfo(mPackPath); - emit eventOccurred(NAME, LOG_EVENT_EXTRACTING_ARCHIVE.arg(packFileInfo.fileName())); + emitEventOccurred(LOG_EVENT_EXTRACTING_ARCHIVE.arg(packFileInfo.fileName())); // Extract pack Extractor extractor(mPackPath, mPathInPack, mDestinationPath); TExtractError ee = extractor.extract(); if(ee.isValid()) - emit errorOccurred(NAME, ee); + emitErrorOccurred(ee); emit complete(ee); } diff --git a/app/src/task/t-generic.cpp b/app/src/task/t-generic.cpp index eb04c2d..16ae5b5 100644 --- a/app/src/task/t-generic.cpp +++ b/app/src/task/t-generic.cpp @@ -28,12 +28,12 @@ void TGeneric::setAction(std::function action) { mAction = action; void TGeneric::perform() { - emit eventOccurred(NAME, LOG_EVENT_START_ACTION.arg(mDescription)); + emitEventOccurred(LOG_EVENT_START_ACTION.arg(mDescription)); Qx::Error err = mAction(); if(err.isValid()) - emit errorOccurred(NAME, err); + emitErrorOccurred(err); - emit eventOccurred(NAME, LOG_EVENT_END_ACTION); + emitEventOccurred(LOG_EVENT_END_ACTION); emit complete(err); } diff --git a/app/src/task/t-message.cpp b/app/src/task/t-message.cpp index 68ec0f0..43582cf 100644 --- a/app/src/task/t-message.cpp +++ b/app/src/task/t-message.cpp @@ -38,7 +38,7 @@ void TMessage::perform() .blocking = mBlocking, .selectable = mSelectable }); - emit eventOccurred(NAME, LOG_EVENT_SHOW_MESSAGE); + emitEventOccurred(LOG_EVENT_SHOW_MESSAGE); // Return success emit complete(Qx::Error()); diff --git a/app/src/task/t-mount.cpp b/app/src/task/t-mount.cpp index 934f471..1be8bf1 100644 --- a/app/src/task/t-mount.cpp +++ b/app/src/task/t-mount.cpp @@ -146,7 +146,7 @@ void TMount::stop() { if(mMounting) { - emit eventOccurred(NAME, LOG_EVENT_STOPPING_MOUNT); + emitEventOccurred(LOG_EVENT_STOPPING_MOUNT); // TODO: This could benefit from the mounters using a shared base, or // some other kind of type erasure like the duck typing above. diff --git a/app/src/task/t-sleep.cpp b/app/src/task/t-sleep.cpp index dafd864..f3661b1 100644 --- a/app/src/task/t-sleep.cpp +++ b/app/src/task/t-sleep.cpp @@ -31,13 +31,13 @@ void TSleep::setDuration(uint msecs) { mDuration = msecs; } void TSleep::perform() { - emit eventOccurred(NAME, LOG_EVENT_START_SLEEP.arg(mDuration)); + emitEventOccurred(LOG_EVENT_START_SLEEP.arg(mDuration)); mSleeper.start(mDuration); } void TSleep::stop() { - emit eventOccurred(NAME, LOG_EVENT_SLEEP_INTERUPTED); + emitEventOccurred(LOG_EVENT_SLEEP_INTERUPTED); mSleeper.stop(); emit complete(Qx::Error()); } @@ -46,6 +46,6 @@ void TSleep::stop() //Privates Slots: void TSleep::timerFinished() { - emit eventOccurred(NAME, LOG_EVENT_FINISH_SLEEP); + emitEventOccurred(LOG_EVENT_FINISH_SLEEP); emit complete(Qx::Error()); } diff --git a/app/src/task/task.cpp b/app/src/task/task.cpp index 621c01c..7480e48 100644 --- a/app/src/task/task.cpp +++ b/app/src/task/task.cpp @@ -22,6 +22,12 @@ Task::Task(QObject* parent) : {} //-Instance Functions------------------------------------------------------------- +//Protected: +// Notifications/Logging (signal-forwarders) +void Task::emitEventOccurred(const QString& event) { emit eventOccurred(name(), event); } +void Task::emitErrorOccurred(const Qx::Error& error) { emit errorOccurred(name(), error); } +void Task::emitBlockingErrorOccurred(int* response, const Qx::Error& error, QMessageBox::StandardButtons choices) { emit blockingErrorOccurred(name(), response, error, choices); } + //Public: QStringList Task::members() const { return {u".stage() = "_s + ENUM_NAME(mStage)}; } diff --git a/app/src/task/task.h b/app/src/task/task.h index 4ee3e39..304c078 100644 --- a/app/src/task/task.h +++ b/app/src/task/task.h @@ -31,6 +31,12 @@ class Task : public QObject virtual ~Task() = default; //-Instance Functions------------------------------------------------------------------------------------------------------ +protected: + // Notifications/Logging (signal-forwarders) + void emitEventOccurred(const QString& event); + void emitErrorOccurred(const Qx::Error& error); + void emitBlockingErrorOccurred(int* response, const Qx::Error& error, QMessageBox::StandardButtons choices); + public: virtual QString name() const = 0; virtual QStringList members() const; @@ -44,16 +50,16 @@ class Task : public QObject //-Signals & Slots------------------------------------------------------------------------------------------------------------ signals: void notificationReady(const Message& msg); - void eventOccurred(QString taskName, QString event); - void errorOccurred(QString taskName, Qx::Error error); - void blockingErrorOccurred(QString taskName, int* response, Qx::Error error, QMessageBox::StandardButtons choices); + void eventOccurred(const QString& taskName, const QString& event); + void errorOccurred(const QString& taskName, const Qx::Error& error); + void blockingErrorOccurred(const QString& taskName, int* response, const Qx::Error& error, QMessageBox::StandardButtons choices); - void longTaskStarted(QString procedure); + void longTaskStarted(const QString& procedure); void longTaskTotalChanged(quint64 total); void longTaskProgressChanged(quint64 progress); void longTaskFinished(); - void complete(Qx::Error errorState); + void complete(const Qx::Error& errorState); }; #endif // TASK_H diff --git a/app/src/tools/blockingprocessmanager.h b/app/src/tools/blockingprocessmanager.h index d66ad1c..f2b7b00 100644 --- a/app/src/tools/blockingprocessmanager.h +++ b/app/src/tools/blockingprocessmanager.h @@ -46,7 +46,7 @@ private slots: void processStandardErrorHandler(); signals: - void eventOccurred(QString name, QString event); + void eventOccurred(const QString& name, const QString& event); void finished(); }; diff --git a/app/src/tools/deferredprocessmanager.h b/app/src/tools/deferredprocessmanager.h index a3221cf..724e257 100644 --- a/app/src/tools/deferredprocessmanager.h +++ b/app/src/tools/deferredprocessmanager.h @@ -54,8 +54,8 @@ private slots: void processStandardErrorHandler(); signals: - void eventOccurred(QString name, QString event); - void errorOccurred(QString name, Qx::GenericError error); + void eventOccurred(const QString& name, const QString& event); + void errorOccurred(const QString& name, const Qx::GenericError& error); }; #endif // DEFERREDPROCESSMANAGER_H diff --git a/app/src/tools/mounter_proxy.cpp b/app/src/tools/mounter_proxy.cpp index 1eff247..1227acb 100644 --- a/app/src/tools/mounter_proxy.cpp +++ b/app/src/tools/mounter_proxy.cpp @@ -59,18 +59,18 @@ MounterProxy::MounterProxy(QObject* parent) : * them to be used as to help make that clear in the logs when the update causes this to stop working). */ connect(&mNam, &QNetworkAccessManager::authenticationRequired, this, [this](){ - emit eventOccurred(NAME, u"Unexpected use of authentication by PHP server!"_s); + signalEventOccurred(u"Unexpected use of authentication by PHP server!"_s); }); connect(&mNam, &QNetworkAccessManager::preSharedKeyAuthenticationRequired, this, [this](){ - emit eventOccurred(NAME, u"Unexpected use of PSK authentication by PHP server!"_s); + signalEventOccurred(u"Unexpected use of PSK authentication by PHP server!"_s); }); connect(&mNam, &QNetworkAccessManager::proxyAuthenticationRequired, this, [this](){ - emit eventOccurred(NAME, u"Unexpected use of proxy by PHP server!"_s); + signalEventOccurred(u"Unexpected use of proxy by PHP server!"_s); }); connect(&mNam, &QNetworkAccessManager::sslErrors, this, [this](QNetworkReply* reply, const QList& errors){ Q_UNUSED(reply); QString errStrList = Qx::String::join(errors, [](const QSslError& err){ return err.errorString(); }, u","_s); - emit eventOccurred(NAME, u"Unexpected SSL errors from PHP server! {"_s + errStrList + u"}"_s"}"); + signalEventOccurred(u"Unexpected SSL errors from PHP server! {"_s + errStrList + u"}"_s"}"); }); } @@ -84,14 +84,17 @@ void MounterProxy::finish(const MounterProxyError& errorState) void MounterProxy::noteProxyRequest(QNetworkAccessManager::Operation op, const QUrl& url, QByteArrayView data) { - emit eventOccurred(NAME, EVENT_REQUEST_SENT.arg(ENUM_NAME(op), url.toString(), QString::fromLatin1(data))); + signalEventOccurred(EVENT_REQUEST_SENT.arg(ENUM_NAME(op), url.toString(), QString::fromLatin1(data))); } void MounterProxy::noteProxyResponse(const QString& response) { - emit eventOccurred(NAME, EVENT_PROXY_RESPONSE.arg(response)); + signalEventOccurred(EVENT_PROXY_RESPONSE.arg(response)); } +void MounterProxy::signalEventOccurred(const QString& event) { emit eventOccurred(NAME, event); } +void MounterProxy::signalErrorOccurred(const MounterProxyError& errorMessage) { emit errorOccurred(NAME, errorMessage); } + //Public: bool MounterProxy::isMounting() { return mMounting; } @@ -112,7 +115,7 @@ void MounterProxy::proxyMountFinishedHandler(QNetworkReply* reply) if(reply->error() != QNetworkReply::NoError) { err = MounterProxyError(MounterProxyError::ProxyMount, reply->errorString()); - emit errorOccurred(NAME, err); + signalErrorOccurred(err); } else { @@ -126,7 +129,7 @@ void MounterProxy::proxyMountFinishedHandler(QNetworkReply* reply) //Public Slots: void MounterProxy::mount() { - emit eventOccurred(NAME, EVENT_MOUNTING); + signalEventOccurred(EVENT_MOUNTING); //-Create mount request------------------------- diff --git a/app/src/tools/mounter_proxy.h b/app/src/tools/mounter_proxy.h index 4e9d5a4..864d183 100644 --- a/app/src/tools/mounter_proxy.h +++ b/app/src/tools/mounter_proxy.h @@ -91,6 +91,9 @@ class MounterProxy : public QObject void noteProxyRequest(QNetworkAccessManager::Operation op, const QUrl& url, QByteArrayView data); void noteProxyResponse(const QString& response); + void signalEventOccurred(const QString& event); + void signalErrorOccurred(const MounterProxyError& errorMessage); + public: bool isMounting(); @@ -109,9 +112,9 @@ public slots: void abort(); signals: - void eventOccurred(QString name, const QString& event); - void errorOccurred(QString name, MounterProxyError errorMessage); - void mountFinished(MounterProxyError errorState); + void eventOccurred(const QString& name, const QString& event); + void errorOccurred(const QString& name, const MounterProxyError& errorMessage); + void mountFinished(const MounterProxyError& errorState); // For now these just cause a busy state void mountProgress(qint64 progress); diff --git a/app/src/tools/mounter_qmp.cpp b/app/src/tools/mounter_qmp.cpp index bbb34a1..3341e89 100644 --- a/app/src/tools/mounter_qmp.cpp +++ b/app/src/tools/mounter_qmp.cpp @@ -74,7 +74,7 @@ void MounterQmp::finish() void MounterQmp::createMountPoint() { - emit eventOccurred(NAME, EVENT_CREATING_MOUNT_POINT); + signalEventOccurred(EVENT_CREATING_MOUNT_POINT); // Build commands QString blockDevAddCmd = u"blockdev-add"_s; @@ -113,6 +113,9 @@ void MounterQmp::createMountPoint() // Await finished() signal... } +void MounterQmp::signalEventOccurred(const QString& event) { emit eventOccurred(NAME, event); } +void MounterQmp::signalErrorOccurred(const MounterQmpError& errorMessage) { emit errorOccurred(NAME, errorMessage); } + //Public: bool MounterQmp::isMounting() { return mMounting; } @@ -136,12 +139,12 @@ void MounterQmp::qmpiConnectedHandler(QJsonObject version, QJsonArray capabiliti QString versionStr = formatter.toJson(QJsonDocument::Compact); formatter.setArray(capabilities); QString capabilitiesStr = formatter.toJson(QJsonDocument::Compact); - emit eventOccurred(NAME, EVENT_QMP_WELCOME_MESSAGE.arg(versionStr, capabilitiesStr)); + signalEventOccurred(EVENT_QMP_WELCOME_MESSAGE.arg(versionStr, capabilitiesStr)); } void MounterQmp::qmpiCommandsExhaustedHandler() { - emit eventOccurred(NAME, EVENT_DISCONNECTING_FROM_QEMU); + signalEventOccurred(EVENT_DISCONNECTING_FROM_QEMU); mQemuMounter.disconnectFromHost(); } @@ -157,7 +160,7 @@ void MounterQmp::qmpiConnectionErrorHandler(QAbstractSocket::SocketError error) MounterQmpError err(MounterQmpError::QemuConnection, ENUM_NAME(error)); mErrorStatus = err; - emit errorOccurred(NAME, err); + signalErrorOccurred(err); } void MounterQmp::qmpiCommunicationErrorHandler(Qmpi::CommunicationError error) @@ -165,7 +168,7 @@ void MounterQmp::qmpiCommunicationErrorHandler(Qmpi::CommunicationError error) MounterQmpError err(MounterQmpError::QemuCommunication, ENUM_NAME(error)); mErrorStatus = err; - emit errorOccurred(NAME, err); + signalErrorOccurred(err); } void MounterQmp::qmpiCommandErrorHandler(QString errorClass, QString description, std::any context) @@ -175,13 +178,13 @@ void MounterQmp::qmpiCommandErrorHandler(QString errorClass, QString description MounterQmpError err(MounterQmpError::QemuCommand, commandErr); mErrorStatus = err; - emit errorOccurred(NAME, err); + signalErrorOccurred(err); mQemuMounter.abort(); } void MounterQmp::qmpiCommandResponseHandler(QJsonValue value, std::any context) { - emit eventOccurred(NAME, EVENT_QMP_COMMAND_RESPONSE.arg(std::any_cast(context), Qx::asString(value))); + signalEventOccurred(EVENT_QMP_COMMAND_RESPONSE.arg(std::any_cast(context), Qx::asString(value))); } void MounterQmp::qmpiEventOccurredHandler(QString name, QJsonObject data, QDateTime timestamp) @@ -189,14 +192,14 @@ void MounterQmp::qmpiEventOccurredHandler(QString name, QJsonObject data, QDateT QJsonDocument formatter(data); QString dataStr = formatter.toJson(QJsonDocument::Compact); QString timestampStr = timestamp.toString(u"hh:mm:s s.zzz"_s); - emit eventOccurred(NAME, EVENT_QMP_EVENT.arg(name, dataStr, timestampStr)); + signalEventOccurred(EVENT_QMP_EVENT.arg(name, dataStr, timestampStr)); } //Public Slots: void MounterQmp::mount() { // Connect to QEMU instance - emit eventOccurred(NAME, EVENT_CONNECTING_TO_QEMU); + signalEventOccurred(EVENT_CONNECTING_TO_QEMU); mQemuMounter.connectToHost(); // Await readyForCommands() signal... @@ -210,7 +213,7 @@ void MounterQmp::abort() MounterQmpError err(MounterQmpError::QemuConnection, ERR_QMP_CONNECTION_ABORT); mErrorStatus = err; - emit errorOccurred(NAME, err); + signalErrorOccurred(err); mQemuMounter.abort(); // Call last here because it causes finished signal to emit immediately } } diff --git a/app/src/tools/mounter_qmp.h b/app/src/tools/mounter_qmp.h index 36e9efa..0e3fe58 100644 --- a/app/src/tools/mounter_qmp.h +++ b/app/src/tools/mounter_qmp.h @@ -110,6 +110,9 @@ class MounterQmp : public QObject void finish(); void createMountPoint(); + void signalEventOccurred(const QString& event); + void signalErrorOccurred(const MounterQmpError& errorMessage); + public: bool isMounting(); @@ -143,9 +146,9 @@ public slots: void abort(); signals: - void eventOccurred(QString name, QString event); - void errorOccurred(QString name, MounterQmpError errorMessage); - void mountFinished(MounterQmpError errorState); + void eventOccurred(const QString& name, const QString& event); + void errorOccurred(const QString& name, const MounterQmpError& errorMessage); + void mountFinished(const MounterQmpError& errorState); }; #endif // MOUNTER_QMP_H diff --git a/app/src/tools/mounter_router.cpp b/app/src/tools/mounter_router.cpp index 05f6500..81beeb7 100644 --- a/app/src/tools/mounter_router.cpp +++ b/app/src/tools/mounter_router.cpp @@ -56,18 +56,18 @@ MounterRouter::MounterRouter(QObject* parent) : * them to be used as to help make that clear in the logs when the update causes this to stop working). */ connect(&mNam, &QNetworkAccessManager::authenticationRequired, this, [this](){ - emit eventOccurred(NAME, u"Unexpected use of authentication by PHP server!"_s); + signalEventOccurred(u"Unexpected use of authentication by PHP server!"_s); }); connect(&mNam, &QNetworkAccessManager::preSharedKeyAuthenticationRequired, this, [this](){ - emit eventOccurred(NAME, u"Unexpected use of PSK authentication by PHP server!"_s); + signalEventOccurred(u"Unexpected use of PSK authentication by PHP server!"_s); }); connect(&mNam, &QNetworkAccessManager::proxyAuthenticationRequired, this, [this](){ - emit eventOccurred(NAME, u"Unexpected use of proxy by PHP server!"_s); + signalEventOccurred(u"Unexpected use of proxy by PHP server!"_s); }); connect(&mNam, &QNetworkAccessManager::sslErrors, this, [this](QNetworkReply* reply, const QList& errors){ Q_UNUSED(reply); QString errStrList = Qx::String::join(errors, [](const QSslError& err){ return err.errorString(); }, u","_s); - emit eventOccurred(NAME, u"Unexpected SSL errors from PHP server! {"_s + errStrList + u"}"_s"}"); + signalEventOccurred(u"Unexpected SSL errors from PHP server! {"_s + errStrList + u"}"_s"}"); }); } @@ -88,6 +88,9 @@ QString MounterRouter::mountValue() const { return mMountValue; } void MounterRouter::setRouterPort(quint16 port) { mRouterPort = port; } void MounterRouter::setMountValue(const QString& value) { mMountValue = value; } +void MounterRouter::signalEventOccurred(const QString& event) { emit eventOccurred(NAME, event); } +void MounterRouter::signalErrorOccurred(const MounterRouterError& errorMessage) { emit errorOccurred(NAME, errorMessage); } + //-Signals & Slots------------------------------------------------------------------------------------------------------------ //Private Slots: void MounterRouter::mountFinishedHandler(QNetworkReply* reply) @@ -100,12 +103,12 @@ void MounterRouter::mountFinishedHandler(QNetworkReply* reply) if(reply->error() != QNetworkReply::NoError && reply->error() != QNetworkReply::InternalServerError) { err = MounterRouterError(MounterRouterError::Failed, reply->errorString()); - emit errorOccurred(NAME, err); + signalErrorOccurred(err); } else { QByteArray response = reply->readAll(); - emit eventOccurred(NAME, EVENT_ROUTER_RESPONSE.arg(response)); + signalEventOccurred(EVENT_ROUTER_RESPONSE.arg(response)); } finish(err); @@ -114,7 +117,7 @@ void MounterRouter::mountFinishedHandler(QNetworkReply* reply) //Public Slots: void MounterRouter::mount() { - emit eventOccurred(NAME, EVENT_MOUNTING_THROUGH_ROUTER); + signalEventOccurred(EVENT_MOUNTING_THROUGH_ROUTER); // Create mount request QUrl mountUrl; @@ -135,7 +138,7 @@ void MounterRouter::mount() mRouterMountReply = mNam.get(mountReq); // Log request - emit eventOccurred(NAME, EVENT_REQUEST_SENT.arg(ENUM_NAME(mRouterMountReply->operation()), mountUrl.toString())); + signalEventOccurred(EVENT_REQUEST_SENT.arg(ENUM_NAME(mRouterMountReply->operation()), mountUrl.toString())); // Await finished() signal... } diff --git a/app/src/tools/mounter_router.h b/app/src/tools/mounter_router.h index 7c201bd..ea85892 100644 --- a/app/src/tools/mounter_router.h +++ b/app/src/tools/mounter_router.h @@ -91,6 +91,9 @@ class MounterRouter : public QObject private: void finish(const MounterRouterError& result); + void signalEventOccurred(const QString& event); + void signalErrorOccurred(const MounterRouterError& errorMessage); + public: bool isMounting(); @@ -109,8 +112,8 @@ public slots: void abort(); signals: - void eventOccurred(QString name, QString event); - void errorOccurred(QString name, MounterRouterError errorMessage); + void eventOccurred(const QString& name, const QString& event); + void errorOccurred(const QString& name, const MounterRouterError& errorMessage); void mountFinished(MounterRouterError errorState); }; diff --git a/app/src/tools/processbider.cpp b/app/src/tools/processbider.cpp deleted file mode 100644 index 03d6867..0000000 --- a/app/src/tools/processbider.cpp +++ /dev/null @@ -1,227 +0,0 @@ -// Unit Include -#include "processbider.h" - -// Qx Includes -#include -#include -#include - -// Windows Include -#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, uint respawnGrace) : - QThread(parent), - mRespawnGrace(respawnGrace), - mProcessHandle(nullptr) -{} - -//-Class Functions------------------------------------------------------------- -//Private: -bool ProcessBider::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; -} - -//-Instance Functions------------------------------------------------------------- -//Private: -ProcessBiderError ProcessBider::doWait() -{ - // Lock other threads from interaction while managing process handle - QMutexLocker handleLocker(&mProcessHandleMutex); - - // Wait until process has stopped running for grace period - DWORD spProcessId; - 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 - spProcessId = Qx::processId(mProcessName); - - // Check that process was found (is running) - if(spProcessId) - { - emit statusChanged(LOG_EVENT_BIDE_RUNNING.arg(mProcessName)); - - // Get process handle and see if it is valid - DWORD rights = PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE; - if((mProcessHandle = OpenProcess(rights, FALSE, spProcessId)) == NULL) - { - Qx::SystemError nativeError = Qx::SystemError::fromHresult(HRESULT_FROM_WIN32(GetLastError())); - ProcessBiderError err(ProcessBiderError::HandleAquisition, nativeError.cause()); - - emit errorOccurred(err); - return err; - } - - // Attempt to wait on process to terminate - emit statusChanged(LOG_EVENT_BIDE_ON.arg(mProcessName)); - handleLocker.unlock(); // Allow interaction while waiting - DWORD waitError = WaitForSingleObject(mProcessHandle, INFINITE); - handleLocker.relock(); // Explicitly lock again - - // Close handle to process - CloseHandle(mProcessHandle); - mProcessHandle = 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) - { - Qx::SystemError nativeError = Qx::SystemError::fromHresult(HRESULT_FROM_WIN32(GetLastError())); - ProcessBiderError err(ProcessBiderError::ProcessHook, nativeError.cause()); - - emit errorOccurred(err); - return err; - } - emit statusChanged(LOG_EVENT_BIDE_QUIT.arg(mProcessName)); - } - } - while(spProcessId); - - // Return success - emit statusChanged(LOG_EVENT_BIDE_FINISHED.arg(mProcessName)); - return ProcessBiderError(); -} - -void ProcessBider::run() -{ - ProcessBiderError status = doWait(); - emit bideFinished(status); -} - -//Public: -void ProcessBider::setRespawnGrace(uint respawnGrace) { mRespawnGrace = respawnGrace; } - -bool ProcessBider::closeProcess() -{ - if(!mProcessHandle) - return false; - - // Lock access to handle and auto-unlock when done - QMutexLocker handleLocker(&mProcessHandleMutex); - - /* Get process ID for use in some of the following calls so that the specific permissions the mProcessHandle - * was opened with don't have to be considered - */ - DWORD processId = GetProcessId(mProcessHandle); - - // 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, processId).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(processId); - else - closeAdminProcess(processId, false); - - // Wait for process to close (allow up to 2 seconds) - DWORD waitRes = WaitForSingleObject(mProcessHandle, 2000); - - // See if process closed - if(waitRes == WAIT_OBJECT_0) - return true; - - // Force close - if(!elevate) - return !Qx::forceKillProcess(processId).isValid(); - else - return closeAdminProcess(processId, true); -} - -//-Signals & Slots------------------------------------------------------------------------------------------------------------ -//Public Slots: -void ProcessBider::start(QString processName) -{ - // Start new thread for waiting - if(!isRunning()) - { - mProcessName = processName; - QThread::start(); - } -} diff --git a/app/src/tools/processbider.h b/app/src/tools/processbider.h deleted file mode 100644 index db493c2..0000000 --- a/app/src/tools/processbider.h +++ /dev/null @@ -1,123 +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 `endProcess()` 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. - */ - -class QX_ERROR_TYPE(ProcessBiderError, "ProcessBiderError", 1235) -{ - friend class ProcessBider; - //-Class Enums------------------------------------------------------------- -public: - enum Type - { - NoError = 0, - HandleAquisition = 1, - ProcessHook = 2 - }; - - //-Class Variables------------------------------------------------------------- -private: - static inline const QHash ERR_STRINGS{ - {NoError, u""_s}, - {HandleAquisition, u"Could not get a wait handle to a restartable process, the title will likely not work correctly."_s}, - {ProcessHook, u"Could not hook a restartable process for waiting, the title will likely not work correctly."_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 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: - // Process Info - QString mProcessName; - uint mRespawnGrace; - - // Process Handling - HANDLE mProcessHandle; - QMutex mProcessHandleMutex; - -//-Constructor------------------------------------------------------------------------------------------------- -public: - ProcessBider(QObject* parent = nullptr, uint respawnGrace = 30000); - -//-Class Functions--------------------------------------------------------------------------------------------------------- -private: - static bool closeAdminProcess(DWORD processId, bool force); - -//-Instance Functions--------------------------------------------------------------------------------------------------------- -private: - ProcessBiderError doWait(); - void run() override; - -public: - void setRespawnGrace(uint respawnGrace); - - bool closeProcess(); - -//-Signals & Slots------------------------------------------------------------------------------------------------------------ -public slots: - void start(QString processName); - -signals: - void statusChanged(QString statusMessage); - void errorOccurred(ProcessBiderError errorMessage); - void bideFinished(ProcessBiderError errorStatus); -}; - -#endif // PROCESSWAITER_H