diff --git a/CMakeLists.txt b/CMakeLists.txt index 375743cf..fc55469a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ project(Qx # Get helper scripts include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchOBCMake.cmake) -fetch_ob_cmake("v0.3.2.1") +fetch_ob_cmake("928cbafc2036f97cfaeb50cd892209b6e1037b6d") # Initialize project according to standard rules include(OB/Project) diff --git a/lib/core/CMakeLists.txt b/lib/core/CMakeLists.txt index 883b07a8..10a8a985 100644 --- a/lib/core/CMakeLists.txt +++ b/lib/core/CMakeLists.txt @@ -2,7 +2,11 @@ qx_add_component("Core" HEADERS_PRIVATE qx-json_p.h + qx-processbider_p.h qx-system_p.h + __private/qx-processwaiter.h + __private/qx-processwaiter_win.h + __private/qx-processwaiter_linux.h HEADERS_API qx-abstracterror.h qx-algorithm.h @@ -25,6 +29,7 @@ qx_add_component("Core" qx-iostream.h qx-json.h qx-list.h + qx-processbider.h qx-progressgroup.h qx-table.h qx-versionnumber.h @@ -53,6 +58,7 @@ qx_add_component("Core" qx-iostream_win.cpp qx-json.cpp qx-json_p.cpp + qx-processbider.cpp qx-progressgroup.cpp qx-versionnumber.cpp qx-string.cpp @@ -65,6 +71,10 @@ qx_add_component("Core" qx-systemerror_linux.cpp qx-systemerror_win.cpp __private/qx-internalerror.cpp + __private/qx-processwaiter.cpp + __private/qx-processwaiter.h + __private/qx-processwaiter_win.cpp + __private/qx-processwaiter_linux.cpp DOC_ONLY qx-regularexpression.dox qx-bytearray.dox diff --git a/lib/core/include/qx/core/qx-processbider.h b/lib/core/include/qx/core/qx-processbider.h new file mode 100644 index 00000000..fcfac344 --- /dev/null +++ b/lib/core/include/qx/core/qx-processbider.h @@ -0,0 +1,129 @@ +#ifndef QX_PROCCESSBIDER_H +#define QX_PROCCESSBIDER_H + +// Shared Lib Support +#include "qx/core/qx_core_export.h" + +// Qt Includes +#include + +// Inter-component Includes +#include + +using namespace std::chrono_literals; + +namespace Qx +{ + +class QX_CORE_EXPORT ProcessBiderError final : public AbstractError<"Qx::ProcessBiderError", 6> +{ + friend class ProcessBider; +//-Class Enums------------------------------------------------------------- +public: + enum Type + { + NoError, + FailedToHook, + FailedToClose + }; + +//-Class Variables------------------------------------------------------------- +private: + static inline const QHash ERR_STRINGS{ + {NoError, u""_s}, + {FailedToHook, u"Could not hook the process in order to bide on it."_s}, + {FailedToClose, u"Could not close the bided process."_s} + }; + +//-Instance Variables------------------------------------------------------------- +private: + Type mType; + QString mProcessName; + +//-Constructor------------------------------------------------------------- +private: + ProcessBiderError(Type t, const QString& pn); + +//-Instance Functions------------------------------------------------------------- +private: + quint32 deriveValue() const override; + QString derivePrimary() const override; + QString deriveSecondary() const override; + +public: + bool isValid() const; + Type type() const; + QString processName() const; +}; + +class QX_CORE_EXPORT ProcessBider : public QObject +{ + friend class ProcessBiderManager; + Q_OBJECT +//-Class Types---------------------------------------------------------------------------------------------- +public: + enum ResultType { Fail, Expired, Abandoned }; + +//-Instance Members------------------------------------------------------------------------------------------ +private: + // Data + QString mName; + std::chrono::milliseconds mGrace; +#ifdef __linux__ + std::chrono::milliseconds mPollRate; +#endif + bool mInitialGrace; + + // Functional + bool mBiding; + +//-Constructor---------------------------------------------------------------------------------------------- +public: + explicit ProcessBider(QObject* parent = nullptr, const QString& processName = {}); + +//-Instance Functions---------------------------------------------------------------------------------------------- +public: + bool isBiding() const; + QString processName() const; + std::chrono::milliseconds respawnGrace() const; + bool initialGrace() const; + + void setProcessName(const QString& name); + void setRespawnGrace(std::chrono::milliseconds grace); + void setInitialGrace(bool initialGrace); + +#ifdef __linux__ + std::chrono::milliseconds pollRate() const; + void setPollRate(std::chrono::milliseconds rate); +#endif + +//-Slots------------------------------------------------------------------------------------------------------------ +private slots: + void handleResultReady(ResultType result); + void handleCloseFailure(); + +public slots: + void start(); + void stop(); + void closeProcess(std::chrono::milliseconds timeout = 1000ms, bool force = false); + +//-Signals------------------------------------------------------------------------------------------------------------ +signals: + void started(); + void established(); + void graceStarted(); + void processStopped(); + void processClosing(); + void stopped(); + void errorOccurred(ProcessBiderError error); + void finished(ResultType result); + +/*! @cond */ + // Temporary until using PIMPL or changing implementation otherwise + void __startClose(std::chrono::milliseconds timeout, bool force); +/*! @endcond */ +}; + +} + +#endif // QX_PROCCESSBIDER_H diff --git a/lib/core/src/__private/qx-processwaiter.cpp b/lib/core/src/__private/qx-processwaiter.cpp new file mode 100644 index 00000000..e35d3de2 --- /dev/null +++ b/lib/core/src/__private/qx-processwaiter.cpp @@ -0,0 +1,70 @@ +// Unit Includes +#include "qx-processwaiter.h" + +namespace Qx +{ +/*! @cond */ + +//=============================================================================================================== +// AbstractProcessWaiter +//=============================================================================================================== + +//-Constructor---------------------------------------------------------------------------------------------- +//Public: +AbstractProcessWaiter::AbstractProcessWaiter(QObject* parent) : + QObject(parent), + mId(0) +{} + +//-Instance Functions--------------------------------------------------------------------------------------------- +//Private: +void AbstractProcessWaiter::postDeadWait(bool died) +{ + // Move out callback incase the callback replaces itself + auto cb = std::move(mDeadWaitCallback); + mDeadWaitCallback = {}; + + // Call + cb(died); +} + +void AbstractProcessWaiter::timerEvent(QTimerEvent* event) +{ + Q_UNUSED(event); + mDeadWaitTimer.stop(); + postDeadWait(false); +} + +//Protected: +void AbstractProcessWaiter::waitForDead(std::chrono::milliseconds timeout, std::function callback) +{ + Q_ASSERT(!mDeadWaitCallback); // Current implementation doesn't support multiple callbacks + + // Store callback + mDeadWaitCallback = std::move(callback); + + // One-shot wait on dead signal + connect(this, &AbstractProcessWaiter::dead, this, [this]{ + if(mDeadWaitTimer.isActive()) // In case timer already expired and this was behind in queue + { + mDeadWaitTimer.stop(); + postDeadWait(true); + } + }, Qt::ConnectionType(Qt::DirectConnection | Qt::SingleShotConnection)); + mDeadWaitTimer.start(timeout, this); +} + +//Public: +void AbstractProcessWaiter::close(std::chrono::milliseconds timeout, bool force) +{ + // If waiting happened to stop, ignore + if(!isWaiting()) + return; + + closeImpl(timeout, force); +} + +void AbstractProcessWaiter::setId(quint32 id) { mId = id; } + +/*! @endcond */ +} diff --git a/lib/core/src/__private/qx-processwaiter.h b/lib/core/src/__private/qx-processwaiter.h new file mode 100644 index 00000000..c5ef0495 --- /dev/null +++ b/lib/core/src/__private/qx-processwaiter.h @@ -0,0 +1,60 @@ +#ifndef QX_PROCCESSWAITER_H +#define QX_PROCCESSWAITER_H + +// Qt Includes +#include +#include + +namespace Qx +{ +/*! @cond */ + +class AbstractProcessWaiter : public QObject +{ + Q_OBJECT +//-Class Members------------------------------------------------------------------------------------------ +protected: + static const int CLEAN_KILL_GRACE_MS = 5000; + +//-Instance Members------------------------------------------------------------------------------------------ +protected: + // Data + quint32 mId; + + // Functional + QBasicTimer mDeadWaitTimer; + std::function mDeadWaitCallback; + +//-Constructor---------------------------------------------------------------------------------------------- +public: + explicit AbstractProcessWaiter(QObject* parent); + +//-Instance Functions---------------------------------------------------------------------------------------------- +private: + void postDeadWait(bool died); + void timerEvent(QTimerEvent* event) override; + +protected: + void waitForDead(std::chrono::milliseconds timeout, std::function callback); + virtual void closeImpl(std::chrono::milliseconds timeout, bool force) = 0; + +public: + virtual bool wait() = 0; + virtual bool isWaiting() const = 0; + void close(std::chrono::milliseconds timeout, bool force); + void setId(quint32 id); + +//-Slots------------------------------------------------------------------------------------------------------------ +protected slots: + virtual void handleProcessSignaled() = 0; + +//-Signals------------------------------------------------------------------------------------------------------------ +signals: + void dead(); + void closeFailed(); +}; + +/*! @endcond */ +} + +#endif // QX_PROCCESSWAITER_H diff --git a/lib/core/src/__private/qx-processwaiter_linux.cpp b/lib/core/src/__private/qx-processwaiter_linux.cpp new file mode 100644 index 00000000..272b7c58 --- /dev/null +++ b/lib/core/src/__private/qx-processwaiter_linux.cpp @@ -0,0 +1,217 @@ +// Unit Includes +#include "qx-processwaiter_linux.h" + +// Qt Includes +#include +#include +#include +#include + +// System Includes +#include + +// Inter-compoent Includes +#include + +namespace Qx +{ +/*! @cond */ + +//=============================================================================================================== +// ProcessPoller +//=============================================================================================================== + +class ProcessPoller : public QObject +{ + Q_OBJECT +//-Instance Members------------------------------------------------------------------------------------------ +private: + QMap> mWaiters; + +//-Instance Functions---------------------------------------------------------------------------------------------- +private: + void cleanupWait(int timerId) + { + mWaiters.remove(timerId); + killTimer(timerId); + if(mWaiters.isEmpty()) + emit noPollsRemaining(); + } + + void timerEvent(QTimerEvent* event) override + { + int timerId = event->timerId(); + auto waiter = mWaiters[timerId]; + + // Check if we're still being waited on + if(!waiter) + { + cleanupWait(timerId); + return; + } + + + /* Signal if process died + * + * TODO: While still not perfect, it would be better if the name of the process was passed through + * to the waiter so that here we could use something like Qx::processName(waiter->id()) to simultaneously + * check if the process is alive and if the name matches the expected name, to help avoid situations + * where the process did die, but it's ID got reused during the poll break. The issue is that currently + * that function reads from /proc/pid/stat, which is generally the most "accurate" process name, but also + * potentially the slowest as it involes the most string manipulation vs /proc/pid/cmdline or /proc/pid/exe; + * however, those can potentially be modified after the process starts and might have other complications. + * + * Need to profile the processName() function as is via stat and if it's not that slow just use it; otherwise, + * go back and research what the exact catches are with checking the exe name through those other two methods + * and see if their use here would be acceptable. + */ + if(kill(waiter->id(), 0) != 0) + { + QMetaObject::invokeMethod(waiter, &ProcessWaiter::handleProcessSignaled); + cleanupWait(timerId); + } + } + +//-Slots------------------------------------------------------------------------------------------------------------ +public slots: + void handlePollRequest(QPointer waiter) { mWaiters[startTimer(waiter->pollRate())] = waiter; } + +//-Signals---------------------------------------------------------------------------------------------------------- +signals: + void noPollsRemaining(); +}; + +//=============================================================================================================== +// ProcessPollerManager +//=============================================================================================================== + +// NOTE: This could be made a non-QObject by using ExclusiveAccess like ProcessBiderManager does. + +class ProcessPollerManager : public QObject +{ + Q_OBJECT +//-Instance Members------------------------------------------------------------------------------------------ +private: + QThread* mThread; + ProcessPoller* mPoller; + +//-Constructor---------------------------------------------------------------------------------------------- +public: + explicit ProcessPollerManager() : + mThread(nullptr) + {} + +//-Destructor---------------------------------------------------------------------------------------------- +public: + ~ProcessPollerManager() { stopThreadIfStarted(true); } + +//-Instance Functions---------------------------------------------------------------------------------------------- +private: + void startThreadIfStopped() + { + // Setup thread + if(mThread) + return; + + mThread = new QThread(this); + mThread->start(); + + // Create and insert poller + mPoller = new ProcessPoller(); + mPoller->moveToThread(mThread); + connect(this, &ProcessPollerManager::pollRequested, mPoller, &ProcessPoller::handlePollRequest); + connect(mPoller, &ProcessPoller::noPollsRemaining, this, &ProcessPollerManager::handlePollerEmpty); + } + + void stopThreadIfStarted(bool wait = false) + { + if(!mThread || !mThread->isRunning()) + return; + + // Quit thread, queue it for deletion, and abandon it + mPoller->deleteLater(); // Schedule the poller to be delete when the thread stops + mThread->quit(); + connect(mThread, &QThread::finished, mThread, &QObject::deleteLater); + if(wait) + mThread->wait(); + mThread = nullptr; + } + +public: + void pollForWaiter(ProcessWaiter* waiter) + { + startThreadIfStopped(); + + // Wrap in QPointer so poller can know if the wait was stopped/deleted + emit pollRequested(QPointer(waiter)); + } + +//-Slots------------------------------------------------------------------------------------------------------------ +private slots: + void handlePollerEmpty() { stopThreadIfStarted(); } + +//-Signals---------------------------------------------------------------------------------------------------------- +signals: + void pollRequested(QPointer waiter); +}; + +//=============================================================================================================== +// ProcessWaiter +//=============================================================================================================== + +//-Constructor---------------------------------------------------------------------------------------------- +//Public: +ProcessWaiter::ProcessWaiter(QObject* parent) : + AbstractProcessWaiter(parent), + mPollRate(500), + mWaiting(false) +{} + +//-Class Functions--------------------------------------------------------------------------------------------- +//Private: +ProcessPollerManager* ProcessWaiter::pollerManager() { static ProcessPollerManager m; return &m; } + +//-Instance Functions--------------------------------------------------------------------------------------------- +//Private: +void ProcessWaiter::closeImpl(std::chrono::milliseconds timeout, bool force) +{ + Qx::cleanKillProcess(mId); + waitForDead(timeout, [this, force](bool dead){ + if(!dead && (!force || Qx::forceKillProcess(mId).isValid())) + emit closeFailed(); + }); +} + +//Public: +quint32 ProcessWaiter::id() const { return mId; } +void ProcessWaiter::setPollRate(std::chrono::milliseconds rate) { mPollRate = rate; } +std::chrono::milliseconds ProcessWaiter::pollRate() const { return mPollRate; } + +bool ProcessWaiter::wait() +{ + // Quick alive check + if(kill(mId, 0) != 0) + return false; + + // Poll + pollerManager()->pollForWaiter(this); + mWaiting = true; + return true; +} + +bool ProcessWaiter::isWaiting() const { return mWaiting; } + +//-Slots--------------------------------------------------------------------------------------------------------- +//Public: +void ProcessWaiter::handleProcessSignaled() +{ + mWaiting = false; + + // Notify + emit dead(); +} + +/*! @endcond */ +} + +#include "qx-processwaiter_linux.moc" diff --git a/lib/core/src/__private/qx-processwaiter_linux.h b/lib/core/src/__private/qx-processwaiter_linux.h new file mode 100644 index 00000000..5afcb978 --- /dev/null +++ b/lib/core/src/__private/qx-processwaiter_linux.h @@ -0,0 +1,52 @@ +#ifndef QX_PROCCESSBIDER_P_LINUX_H +#define QX_PROCCESSBIDER_P_LINUX_H + +// Qt Includes +#include + +// Inter-component Includes +#include "qx-processwaiter.h" + +namespace Qx +{ +/*! @cond */ + +class ProcessPollerManager; + +class ProcessWaiter : public AbstractProcessWaiter +{ + Q_OBJECT + +//-Instance Members------------------------------------------------------------------------------------------ +private: + std::chrono::milliseconds mPollRate; + bool mWaiting; + +//-Constructor---------------------------------------------------------------------------------------------- +public: + explicit ProcessWaiter(QObject* parent); + +//-Class Functions---------------------------------------------------------------------------------------------- +private: + static ProcessPollerManager* pollerManager(); + +//-Instance Functions---------------------------------------------------------------------------------------------- +private: + void closeImpl(std::chrono::milliseconds timeout, bool force) override; + +public: + quint32 id() const; + void setPollRate(std::chrono::milliseconds rate); + std::chrono::milliseconds pollRate() const; + bool wait() override; + bool isWaiting() const override; + +//-Slots------------------------------------------------------------------------------------------------------------ +public slots: + void handleProcessSignaled() override; +}; + +/*! @endcond */ +} + +#endif // QX_PROCCESSBIDER_P_LINUX_H diff --git a/lib/core/src/__private/qx-processwaiter_win.cpp b/lib/core/src/__private/qx-processwaiter_win.cpp new file mode 100644 index 00000000..959ca5e2 --- /dev/null +++ b/lib/core/src/__private/qx-processwaiter_win.cpp @@ -0,0 +1,298 @@ +// Unit Includes +#include "qx-processwaiter_win.h" + +// Windows Includes +#define WIN32_LEAN_AND_MEAN +#include "windows.h" +#include + +// Inter-component Includes +#include "qx/core/qx-system.h" + +/* TODO: This is another good one for having smaller private libraries that come before core. + * Here, the elevation check functions are near copy-pastes from whats in qx-common-windows.h, + * so having a way to have those in said private library for access here, while being able to + * just forward them to the public windows library would be ideal. + */ + +namespace Qx +{ +/*! @cond */ + +//=============================================================================================================== +// ProcessWaiter +//=============================================================================================================== + +//-Constructor---------------------------------------------------------------------------------------------- +//Public: +ProcessWaiter::ProcessWaiter(QObject* parent) : + AbstractProcessWaiter(parent), + mProcessHandle(nullptr), + mWaitHandle(nullptr), + mTaskKillHandle(nullptr), + mAdminCloseHandle(nullptr), + mCleaningUp(false) +{} + +//-Destructor---------------------------------------------------------------------------------------------- +//Public: +ProcessWaiter::~ProcessWaiter() { cleanup(); } + +//-Class Functions--------------------------------------------------------------------------------------------- +//Public: +void ProcessWaiter::waitCallback(void* context, BOOLEAN timedOut) +{ + Q_UNUSED(timedOut); // Will never be true + + // The waiter should never be deleted while a wait is registered so this pointer must always be valid + ProcessWaiter* pw = static_cast(context); + + /* Queue up the response slot. QueuedConnection should be automatic since this callback function is + * executed by a system thread, but we're explicit here to be sure. + */ + QMetaObject::invokeMethod(pw, &ProcessWaiter::handleProcessSignaled, Qt::ConnectionType::QueuedConnection); +} + +void ProcessWaiter::adminCloseNativeCallback(void* context, BOOLEAN timedOut) +{ + // The waiter should never be deleted while a wait is registered so this pointer must always be valid + ProcessWaiter* pw = static_cast(context); + + /* Queue up the response slot. QueuedConnection should be automatic since this callback function is + * executed by a system thread, but we're explicit here to be sure. + */ + QMetaObject::invokeMethod(pw, [pw, timedOut]{ + pw->handleAdminCloseFinsihed(timedOut); + }, Qt::ConnectionType::QueuedConnection); +} + +bool ProcessWaiter::processIsElevated(bool def) +{ + HANDLE hThisProcess = GetCurrentProcess(); // Self handle doesn't need to be closed + return processIsElevated(hThisProcess, def); +} + +bool ProcessWaiter::processIsElevated(HANDLE pHandle, bool def) +{ + // Checks if the process is elevated, if for some reason the check fails, assumes 'def' + + // Ensure handle isn't null (doesn't assure validity) + if(!pHandle) + return def; + + // Get access token for process + HANDLE hToken; + if(!OpenProcessToken(pHandle, TOKEN_QUERY, &hToken)) + return def; + + // Ensure token handle is cleaned up + QScopeGuard hTokenCleaner([&hToken]() { CloseHandle(hToken); }); + + // Get elevation information + TOKEN_ELEVATION elevationInfo = { 0 }; + DWORD infoBufferSize; // The next function fills this as a double check + if(!GetTokenInformation(hToken, TokenElevation, &elevationInfo, sizeof(elevationInfo), &infoBufferSize ) ) + return def; + assert(infoBufferSize == sizeof(elevationInfo)); + + // Return value + return elevationInfo.TokenIsElevated; +} + +//-Instance Functions--------------------------------------------------------------------------------------------- +//Private: +void ProcessWaiter::closeImpl(std::chrono::milliseconds timeout, bool force) +{ + if(mCleaningUp) + return; + + // Check if admin rights are needed + bool selfElevated = processIsElevated(false); // If check fails, we're not elevated to be safe + bool waitProcessElevated = processIsElevated(mProcessHandle, true); // If check fails, assume process is elevated to be safe + bool elevatedKill = !selfElevated && waitProcessElevated; + + if(elevatedKill) + { + closeAdmin(false, [this, force, timeout](bool cleanRan){ + if(mCleaningUp) + return; + + if(!cleanRan) + { + emit closeFailed(); + return; + } + + waitForDead(timeout, [this, force](bool dead){ + if(mCleaningUp) + return; + + if(!dead) + { + if(!force) + { + emit closeFailed(); + return; + } + + closeAdmin(true, [this](bool forceRan){ + if(mCleaningUp) + return; + + if(!forceRan) + emit closeFailed(); + }); + } + }); + }); + } + else + { + Qx::cleanKillProcess(mId); + waitForDead(timeout, [this, force](bool dead){ + if(mCleaningUp) + return; + + if(!dead && (!force || Qx::forceKillProcess(mId).isValid())) + emit closeFailed(); + }); + } +} + +void ProcessWaiter::closeAdmin(bool force, std::function callback) +{ + Q_ASSERT(!mAdminCloseCallback); // Current implementation doesn't allow multiple close waits at once + + if(!startAdminClose(force)) + callback(false); + else + mAdminCloseCallback = std::move(callback); +} + +bool ProcessWaiter::startAdminClose(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(mId); + 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 + mTaskKillHandle = tkExecInfo.hProcess; + if(!mTaskKillHandle) + return false; + + // Wait for taskkill to finish (should be fast, but may need UAC so we allow 30s) + return RegisterWaitForSingleObject(&mAdminCloseHandle, mProcessHandle, waitCallback, this, 30000, WT_EXECUTEONLYONCE); +} + +void ProcessWaiter::cleanupAdminCloseHandles() +{ + if(mAdminCloseHandle) + { + UnregisterWaitEx(mAdminCloseHandle, INVALID_HANDLE_VALUE); // INVALID_HANDLE_VALUE allows in-progress callbacks to complete + mAdminCloseHandle = nullptr; + } + if(mTaskKillHandle) + { + CloseHandle(mTaskKillHandle); + mTaskKillHandle = nullptr; + } +} + +void ProcessWaiter::cleanup() +{ + if(mCleaningUp) + return; + mCleaningUp = true; + + if(mWaitHandle) + { + UnregisterWaitEx(mWaitHandle, INVALID_HANDLE_VALUE); // INVALID_HANDLE_VALUE allows in-progress callbacks to complete + mWaitHandle = nullptr; + } + if(mProcessHandle) + { + CloseHandle(mProcessHandle); + mProcessHandle = nullptr; + } + cleanupAdminCloseHandles(); + + mCleaningUp = false; +} + +//Public: +bool ProcessWaiter::wait() +{ + // TODO: Could use Qx::getLastError() here and return Qx::SystemError instead to pass more info back to ProcessBider + + // Get process handle + if(!(mProcessHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, mId))) + return false; + + // Register wait + if(!RegisterWaitForSingleObject(&mWaitHandle, mProcessHandle, waitCallback, this, INFINITE, WT_EXECUTEONLYONCE)) + return false; + + return true; +} + +bool ProcessWaiter::isWaiting() const { return mWaitHandle; } + +//-Slots--------------------------------------------------------------------------------------------------------- +//Public: +void ProcessWaiter::handleProcessSignaled() +{ + // Ignore if the wait is being canceled + if(mCleaningUp) + return; + + // Cleanup + cleanup(); + + // Notify + emit dead(); +} + +void ProcessWaiter::handleAdminCloseFinsihed(bool timeout) +{ + bool successful = false; + if(!timeout) + { + DWORD exitCode; + if(GetExitCodeProcess(mTaskKillHandle, &exitCode)) + successful = exitCode == 0; + } + + cleanupAdminCloseHandles(); + + // Move out callback in case it replaces itself + auto cb = std::move(mAdminCloseCallback); + mAdminCloseCallback = {}; + cb(successful); +} + +/*! @endcond */ +} diff --git a/lib/core/src/__private/qx-processwaiter_win.h b/lib/core/src/__private/qx-processwaiter_win.h new file mode 100644 index 00000000..598acaa0 --- /dev/null +++ b/lib/core/src/__private/qx-processwaiter_win.h @@ -0,0 +1,70 @@ +#ifndef QX_PROCCESSBIDER_P_WIN_H +#define QX_PROCCESSBIDER_P_WIN_H + +// Qt Includes +#include + +// Inter-component Includes +#include "qx-processwaiter.h" + +/*! @cond */ + +typedef void* HANDLE; +typedef unsigned char BOOLEAN; +#define CALLBACK __stdcall + +namespace Qx +{ + +/* It's critical in the design of this that these objects are never destroyed (more specifically, never completely destroyed, so + * cleanup in destructor is fine) while they have a registered wait */ +class ProcessWaiter : public AbstractProcessWaiter +{ + Q_OBJECT +//-Instance Members------------------------------------------------------------------------------------------ +private: + HANDLE mProcessHandle; + HANDLE mWaitHandle; + HANDLE mTaskKillHandle; + HANDLE mAdminCloseHandle; + std::function mAdminCloseCallback; + bool mCleaningUp; + +//-Constructor---------------------------------------------------------------------------------------------- +public: + explicit ProcessWaiter(QObject* parent); + +//-Destructor---------------------------------------------------------------------------------------------- +public: + ~ProcessWaiter(); + +//-Class Functions---------------------------------------------------------------------------------------------- +private: + static void CALLBACK waitCallback(void* context, BOOLEAN timedOut); + static void CALLBACK adminCloseNativeCallback(void* context, BOOLEAN timedOut); + static bool processIsElevated(bool def); + static bool processIsElevated(HANDLE pHandle, bool def); + +//-Instance Functions---------------------------------------------------------------------------------------------- +private: + void closeImpl(std::chrono::milliseconds timeout, bool force) override; + void closeAdmin(bool force, std::function callback); + bool startAdminClose(bool force); + void cleanupAdminCloseHandles(); + void cleanup(); + +public: + bool wait() override; + bool isWaiting() const override; + +//-Slots------------------------------------------------------------------------------------------------------------ +public slots: + void handleProcessSignaled() override; + void handleAdminCloseFinsihed(bool timeout); +}; + +} + +/*! @endcond */ + +#endif // QX_PROCCESSBIDER_P_WIN_H diff --git a/lib/core/src/qx-processbider.cpp b/lib/core/src/qx-processbider.cpp new file mode 100644 index 00000000..7cb9b776 --- /dev/null +++ b/lib/core/src/qx-processbider.cpp @@ -0,0 +1,640 @@ +// Unit Includes +#include "qx/core/qx-processbider.h" +#include "qx-processbider_p.h" + +// Qt Includes +#include + +// Inter-component Includes +#include "qx/core/qx-system.h" + +// TODO: Add the ability to add a starting PID so that if more than one process with the same +// name are running, the user can specify which to latch to (won't matter for grace restart though) + +namespace Qx +{ +/*! @cond */ +//=============================================================================================================== +// ProcessBiderWorker +//=============================================================================================================== + +//-Constructor---------------------------------------------------------------------------------------------- +//Public: +ProcessBiderWorker::ProcessBiderWorker() : +#ifdef __linux__ + mPollRate(0), +#endif + mGrace(0), + mStartWithGrace(false), + mWaiter(this), // Ensure waiter is moved with worker + mComplete(false) +{ + connect(&mWaiter, &ProcessWaiter::dead, this, &ProcessBiderWorker::handleProcesStop); + connect(&mWaiter, &ProcessWaiter::closeFailed, this, &ProcessBiderWorker::processCloseFailed); +#ifdef __linux__ + mWaiter.setPollRate(mPollRate); +#endif +} + +//-Instance Functions--------------------------------------------------------------------------------------------- +//Private: +void ProcessBiderWorker::startWait(quint32 pid) +{ + mWaiter.setId(pid); + if(!pid || !mWaiter.wait()) + finish(Outcome::HookFail); + else + emit processHooked(); +} + +void ProcessBiderWorker::startGrace() +{ + // Start grace timer, or if no grace skip straight to end + if(mGrace != std::chrono::milliseconds::zero()) + { + emit graceStarted(); + mGraceTimer.start(mGrace, this); + } + else + handleGraceEnd(false); +} + +void ProcessBiderWorker::timerEvent(QTimerEvent* event) +{ + Q_UNUSED(event); // Only one use of the timer here + mGraceTimer.stop(); + handleGraceEnd(true); +} + +void ProcessBiderWorker::handleGraceEnd(bool retry) +{ + // Ignore queued grace timer if finished + if(mComplete) + return; + + quint32 pid = retry ? processId(mName) : 0; + if(!pid) + finish(Outcome::GraceExpired); + else + startWait(pid); +} + +void ProcessBiderWorker::finish(Outcome outcome) +{ + mComplete = true; + emit complete(outcome); +} + +//Public: +void ProcessBiderWorker::setProcessName(const QString& name) { mName = name; } +#ifdef __linux__ +void ProcessBiderWorker::setPollRate(std::chrono::milliseconds rate) { mPollRate = rate; } +#endif +void ProcessBiderWorker::setGrace(std::chrono::milliseconds grace) { mGrace = grace; } +void ProcessBiderWorker::setStartWithGrace(bool graceFirst) { mStartWithGrace = graceFirst; } + +//-Slots--------------------------------------------------------------------------------------------------------- +//Private: +void ProcessBiderWorker::handleProcesStop() +{ + // Ignore queued process end if finished + if(mComplete) + return; + + emit processStopped(); + + // Start grace + startGrace(); +} + +//Public: +void ProcessBiderWorker::handleAbort() +{ + // Ignore further aborts if finished + if(mComplete) + return; + + mGraceTimer.stop(); + finish(Outcome::Abandoned); + /* TODO: The emission of complete will result in this being deleted, which in turn will delete + * the waiter, so it does not need to be manually cleaned up, though that might be a slight + * optimization + */ +} + +void ProcessBiderWorker::handleClosure(std::chrono::milliseconds timeout, bool force) +{ + // Ignore closure if finished + if(mComplete) + return; + + // If wait is active, try close immediately + if(mWaiter.isWaiting()) + mWaiter.close(timeout, force); + else + { + /* In case a grace expiration is queued and the process might still be running, queue a closure + * for if the process is hooked again + */ + Qt::ConnectionType ct = static_cast(Qt::AutoConnection | Qt::SingleShotConnection | Qt::UniqueConnection); + connect(this, &ProcessBiderWorker::processHooked, this, [this, timeout, force]{ + mWaiter.close(timeout, force); + }, ct); + } +} + +void ProcessBiderWorker::bide() +{ + if(mStartWithGrace) + startGrace(); + else + startWait(processId(mName)); +} + +//=============================================================================================================== +// ProcessBiderManager +//=============================================================================================================== + +/* NOTE: We explicitly avoid haivng the manager be a QObject here since it may be spawned in any thread + * due to RAII and therefore we can't be certain about thread afinity. Additionally, for this reason, we + * must be sure to avoid using 'this' as a context parameter for any connections that the manager makes + * so that nothing is set to run in the managers thread. The manager could be a QObject and could be moved + * to the main thread upon creation to ensure it's thread affinity is always valid, but we don't want the + * managers operation to potentially get held up just because the main thread may block. + */ + +//-Constructor---------------------------------------------------------------------------------------------- +//private: +ProcessBiderManager::ProcessBiderManager() : + mThread(nullptr), + mWorkerCount(0) +{} + +//-Destructor---------------------------------------------------------------------------------------------- +//Public: +ProcessBiderManager::~ProcessBiderManager() +{ + // In theory this could be deleted while something else is accessing it, however unlikely that is given it's static + QMutexLocker lock(&smMutex); + stopThreadIfStarted(true); +} + +//-Class Functions---------------------------------------------------------------------------------------------- +//Private: +Qx::ExclusiveAccess ProcessBiderManager::instance() +{ + /* We don't control the ProcessBider instances, so they could be in any thread and we need + * to synchronize access to the manager (also becuase workers access this). + * An alternative is making the manager a QObject and using signals/slots, but that gets tricky since it's created + * via RAII and therefore may be created in a thread that gets destroyed (and would then have no affinity). + * This is simpler. + */ + static ProcessBiderManager m; + return Qx::ExclusiveAccess(&m, &smMutex); // Provides locked access to manager, that unlocks when destroyed +} + +//-Instance Functions--------------------------------------------------------------------------------------------- +//Private: +void ProcessBiderManager::startThreadIfStopped() +{ + if(mThread) + return; + + QThread* mainThread = QCoreApplication::instance()->thread(); + if(!mainThread) [[unlikely]] + { + // It's documented that you're not supposed to use QObjects before QCoreAppliation is created, + // but check explicitly anyway + qCritical("Cannot use ProcessBiders before QCoreApplication is created"); + } + + mThread = new QThread(); + + /* mThread is the only QObject member of this class. We need to move it to the main thread because + * the manager can be created in any thread since it's done by RAII and UB occurs if a Object (in this case + * the QThread) continues to be used if the thread it belongs to is shutdown. moveToThread() already checks + * if this is the main thread and results in a no-op if so + */ + mThread->moveToThread(mainThread); + mThread->start(); +} + +void ProcessBiderManager::stopThreadIfStarted(bool wait) +{ + if(!mThread || !mThread->isRunning()) + return; + + // Quit thread, queue it for deletion, and abandon it + mThread->quit(); + QObject::connect(mThread, &QThread::finished, mThread, &QObject::deleteLater); + if(wait) + mThread->wait(); + mThread = nullptr; +} + +//Public: +void ProcessBiderManager::registerBider(ProcessBider* bider) +{ + startThreadIfStopped(); + + ProcessBiderWorker* worker = new ProcessBiderWorker(); + worker->setGrace(bider->respawnGrace()); + worker->setProcessName(bider->processName()); + worker->setStartWithGrace(bider->initialGrace()); + worker->moveToThread(mThread); + mWorkerCount++; + + // Management + QObject::connect(bider, &QObject::destroyed, worker, &QObject::deleteLater); // If bider is unexpectedly deleted, remove bide worker + QObject::connect(mThread, &QThread::finished, worker, &QObject::deleteLater); // Kill worker if it still exists and the thread is being shutdown + QObject::connect(worker, &QObject::destroyed, worker, []{ ProcessBiderManager::instance()->notifyWorkerFinished(); }); + // ^ Have worker notify the manager when it dies to track count. Use static accessor for synchronization instead of capturing 'this' + QObject::connect(worker, &ProcessBiderWorker::complete, worker, &QObject::deleteLater); // Self-trigger cleanup of worker + QObject::connect(bider, &ProcessBider::stopped, worker, &ProcessBiderWorker::handleAbort); // Handle aborts + QObject::connect(bider, &ProcessBider::__startClose, worker, &ProcessBiderWorker::handleClosure); // Handle closes + + // Public bider signaling + QObject::connect(worker, &ProcessBiderWorker::processHooked, bider, &ProcessBider::established); + QObject::connect(worker, &ProcessBiderWorker::processStopped, bider, &ProcessBider::processStopped); + QObject::connect(worker, &ProcessBiderWorker::processCloseFailed, bider, &ProcessBider::handleCloseFailure); + QObject::connect(worker, &ProcessBiderWorker::graceStarted, bider, &ProcessBider::graceStarted); + QObject::connect(worker, &ProcessBiderWorker::complete, bider, [bider](ProcessBiderWorker::Outcome o){ + // THIS LAMBDA IS EVAULATED IN THE WORKER THREAD + // Translate to public result type + ProcessBider::ResultType r; + switch(o) + { + case ProcessBiderWorker::HookFail: + r = ProcessBider::Fail; + break; + case ProcessBiderWorker::GraceExpired: + r = ProcessBider::Expired; + break; + case ProcessBiderWorker::Abandoned: + r = ProcessBider::Abandoned; + break; + } + + // Push result asynchronously + QMetaObject::invokeMethod(bider, [r, bider]{ + bider->handleResultReady(r); + }); + }); + + // Start work asynchronously + QMetaObject::invokeMethod(worker, &ProcessBiderWorker::bide); +} + +void ProcessBiderManager::notifyWorkerFinished() +{ + if(!--mWorkerCount) + stopThreadIfStarted(); +} + +/*! @endcond */ + +//=============================================================================================================== +// ProcessBiderError +//=============================================================================================================== + +/*! + * @class ProcessBiderError qx/core/qx-processbider.h + * @ingroup qx-core + * + * @brief The ProcessBiderError class describes errors than can occur during process biding. + */ + +/*! + * @enum ProcessBiderError::Type + * + * This enum describes the type of error. + * + * @var ProcessBiderError::Type ProcessBiderError::FailedToHook + * The bider was unable to hook the process for waiting, potentially due to a permissions issue. + * + * @var ProcessBiderError::Type ProcessBiderError::FailedToClose + * The bider was unable to close the process, likely due to a permissions issue. + * + * @sa type(). + */ + +//-Constructor------------------------------------------------------------- +//Private: +ProcessBiderError::ProcessBiderError(Type t, const QString& pn) : + mType(t), + mProcessName(pn) +{} + +//-Instance Functions------------------------------------------------------------- +//Private: +quint32 ProcessBiderError::deriveValue() const { return mType; } +QString ProcessBiderError::derivePrimary() const { return ERR_STRINGS.value(mType); } +QString ProcessBiderError::deriveSecondary() const { return mProcessName; } + +//Public: +/*! + * Returns @c true if the error is valid; otherwise, returns @c false. + */ +bool ProcessBiderError::isValid() const { return mType != NoError; } + +/*! + * Returns the name of the process the bide was for. + */ +QString ProcessBiderError::processName() const { return mProcessName; } + +/*! + * Returns the type of error. + */ +ProcessBiderError::Type ProcessBiderError::type() const { return mType; } + +//=============================================================================================================== +// ProcessBider +//=============================================================================================================== + +/*! + * @class ProcessBider qx/core/qx-processbider.h + * @ingroup qx-core + * + * @brief The ProcessBider class monitors the presence of a process and signals when that process ends. + * + * Processes are specified by name in order to allow for an optional grace period in which the process + * is not considered to be finished if it restarts. The ability to specify an initial process ID may + * be added in the future. + * + * On Windows this directly requests a wait from the OS. On Linux this uses polling. + * + * @note Monitoring external processes you don't have direct control over is tricky in that they can terminate + * at any time, and potentially be replaced with a different process using the same ID, including during the + * brief setup of the bide. While generally this won't be a problem, it's something to be kept in mind, especially + * when using slower poll rates. + * + * @sa setPollRate(). + */ + +//-Class Enums----------------------------------------------------------------------------------------------- +//Public: +/*! + * @enum ProcessBider::ResultType + * + * This enum specifies the different conditions under which the finished() signal is emitted. + * + * @var ProcessBider::ResultType ProcessBider::Fail + * The bider was unable to hook the process for waiting, potentially due to a permissions issue. + * + * @var ProcessBider::ResultType ProcessBider::Expired + * The process ended and the grace period, if any, expired. + * + * @var ProcessBider::ResultType ProcessBider::Abandoned + * The bide was abandoned before the process ended. + * + * @sa finished(). + */ + +//-Constructor---------------------------------------------------------------------------------------------- +//Public: +/*! + * Constructs a process bider with @a parent, set to bide on the process with the name @a processName. + */ +ProcessBider::ProcessBider(QObject* parent, const QString& processName) : + QObject(parent), + mName(processName), + mGrace(0), +#ifdef __linux__ + mPollRate(500), +#endif + mInitialGrace(false), + mBiding(false) +{} + +//-Instance Functions--------------------------------------------------------------------------------------------- +//Public: +/*! + * Returns @c true if the bide is ongoing; otherwise, returns @c false. + * + * @sa start() and finished(). + */ +bool ProcessBider::isBiding() const { return mBiding; } + +/*! + * Returns the name of the process to bide on. + * + * @sa setProcessName(). + */ +QString ProcessBider::processName() const { return mName; } + +/*! + * Returns how long the process has to restart in order to not be considered stopped. + * + * The default is @c 0, which means there is no grace period. + * + * @sa setRespawnGrace() and initialGrace(). + */ +std::chrono::milliseconds ProcessBider::respawnGrace() const { return mGrace; } + +/*! + * Returns @c true if the bider is currently configured to start with a grace period; + * otherwise, returns @c false. + * + * The deafault is @c false. + * + * @sa setInitialGrace() and respawnGrace(). + */ +bool ProcessBider::initialGrace() const { return mInitialGrace; } + +/*! + * Sets the name of the process to bide on to @a name. + * + * @sa processName(). + */ +void ProcessBider::setProcessName(const QString& name) { mName = name; } + +/*! + * Sets how long the process has to restart in order to not be considered stopped. + * + * The grace period does not end early if the process starts sooner and always runs + * to completion before checking if it's running due to technical limitations. + * + * A value of @c 0 disables the grace period. + * + * @sa respawnGrace() and setInitialGrace(). + */ +void ProcessBider::setRespawnGrace(std::chrono::milliseconds grace) { mGrace = grace; } + +/*! + * Enables or disables the initial grace period. + * + * If enabled, the bider will begin the bide with a grace period, which is useful if + * the process may be stopped when the bide starts and you want to wait for it to + * come back up. + * + * When disabled, the bider will assume the proess is already running and try to hook + * it for waiting immediately after starting. + * + * This setting has no effect if there is no grace set. + * + * @sa initialGrace() and setRespawnGrace(). + */ +void ProcessBider::setInitialGrace(bool initialGrace) { mInitialGrace = initialGrace; } + +#ifdef __linux__ +/*! + * @note This function is only available on Linux + * + * Returns the rate at which the process is checked. + * + * The default is 500ms. + * + * @sa setPollRate(). + */ +std::chrono::milliseconds ProcessBider::pollRate() const { return mPollRate; } + +/*! + * @note This function is only available on Linux + * + * Sets the rate at which the process is checked. + * + * @sa pollRate(). + */ +void ProcessBider::setPollRate(std::chrono::milliseconds rate) { mPollRate = rate; } +#endif + +//-Slots------------------------------------------------------------------------------------------------------------ +//Private: +void ProcessBider::handleResultReady(ResultType result) +{ + mBiding = false; + if(result == ResultType::Fail) + emit errorOccurred(ProcessBiderError(ProcessBiderError::FailedToHook, mName)); + + emit finished(result); +} + +void ProcessBider::handleCloseFailure() { emit errorOccurred(ProcessBiderError(ProcessBiderError::FailedToClose, mName)); } + +//Public: +/*! + * Begins monitoring the process for closure. + * + * @sa stop() and started(). + */ +void ProcessBider::start() +{ + if(mBiding) + return; + + mBiding = true; + ProcessBiderManager::instance()->registerBider(this); + emit started(); +} + +/*! + * Abandons the bide, regardless of whether or not the process has closed. + * + * @sa start() and stopped(). + */ +void ProcessBider::stop() +{ + if(!mBiding) + return; + + emit stopped(); +} + +/*! + * Attempts to close the process. + * + * First the process is signaled to shutdown gracefully, with @a timeout controlling how long the process has to terminate. + * Then, if the process is still running and @a force is @c true, the process will be terminated forcefully. errorOccurred() + * will be emitted if the closure ultimately fails. + * + * On Windows if the process is elevated and the current process is not, this will attempt to run taskkill as an administrator + * to close the process, which may trigger a UAC prompt. Trying to close processes with greater permissions on Linux is not + * supported. + * + * @sa processIsClosing(). + */ +void ProcessBider::closeProcess(std::chrono::milliseconds timeout, bool force) +{ + if(!mBiding) + return; + + /* TODO: Reimplement to use PIMPL so that the private class emits this since + * this is a dirty implementation detail, or change the manager/worker setup + * so that ProcessBider gets a pointer to its worker and invokes its post + * setup (so close and abort) handlers directly. This can be tricky though + * since the worker dies before the bider, probably could just use QPointer. + */ + emit __startClose(timeout, force); + emit processClosing(); +} + +//-Signals------------------------------------------------------------------------------------------------------------ +/*! + * @fn void ProcessBider::started() + * + * This signal is emitted when the bide is started. + * + * @sa start() and stop(). + */ + +/*! + * @fn void ProcessBider::graceStarted() + * + * This signal is emitted when the respawn grace begins, if set, and may be emitted multiple times if the process + * restarts. + * + * It will be emitted immediately after processStopped(), and once after started() if the initial grace period + * is enabled. + * + * @sa processStopped(). + */ + +/*! + * @fn void ProcessBider::established() + * + * This signal is emitted when the process is found and hooked for waiting, so it may be emitted multiple times if + * a grace period is set. + * + * @sa processStopped(). + */ + +/*! + * @fn void ProcessBider::processStopped() + * + * This signal is emitted when the process stops, which means it may be emitted multiple times if a grace period + * is set. + * + * @sa established(). + */ + +/*! + * @fn void ProcessBider::processClosing() + * + * This signal is emitted when an attempt is made to close the process via closeProcess(). + * + * @sa closeProcess(). + */ + +/*! + * @fn void ProcessBider::stopped() + * + * This signal is emitted when the bide is abandoned via stop(). + * + * @sa stop() and start(). + */ + +/*! + * @fn void ProcessBider::errorOccurred(ProcessBiderError error) + * + * This signal is emitted when a bide error occurs, with @a error containing details. + */ + +/*! + * @fn void ProcessBider::finished(ResultType result) + * + * This signal is emitted when the bide is complete, with @a result containing the reason. + * + * @sa processStopped(). + */ + +} diff --git a/lib/core/src/qx-processbider_p.h b/lib/core/src/qx-processbider_p.h new file mode 100644 index 00000000..c963a3f1 --- /dev/null +++ b/lib/core/src/qx-processbider_p.h @@ -0,0 +1,130 @@ +#ifndef QX_PROCCESSBIDER_P_H +#define QX_PROCCESSBIDER_P_H + +// Qt Includes +#include +#include +#include +#include + +// Inter-component Includes +#include + +// Project Includes +/* The split headers like this are a touch messy as they rely on the public method's of all implementations being the same + * without a true base class, but this way avoids unnecessary virtual dispatch overhead that would be caused by + * using polymorphism when it's not needed (since the derived is known at compile time), and somewhat equally kludgey + * templates that would still rely on a fare amount of #ifdefs anyway + */ +#ifdef _WIN32 +#include "__private/qx-processwaiter_win.h" +#endif +#ifdef __linux__ +#include "__private/qx-processwaiter_linux.h" +#endif + +namespace Qx +{ +/*! @cond */ + +class ProcessBiderWorker : public QObject +{ + Q_OBJECT +//-Class Types---------------------------------------------------------------------------------------------- +public: + enum Outcome { HookFail, GraceExpired, Abandoned }; + +//-Instance Members------------------------------------------------------------------------------------------ +private: + // Data + QString mName; +#ifdef __linux__ + std::chrono::milliseconds mPollRate; +#endif + std::chrono::milliseconds mGrace; + bool mStartWithGrace; + + // Function + QBasicTimer mGraceTimer; + ProcessWaiter mWaiter; + bool mComplete; + +//-Constructor---------------------------------------------------------------------------------------------- +public: + explicit ProcessBiderWorker(); + +//-Instance Functions---------------------------------------------------------------------------------------------- +private: + void startWait(quint32 pid); + void startGrace(); + void timerEvent(QTimerEvent* event) override; + void handleGraceEnd(bool retry); + void finish(Outcome outcome); + +public: + void setProcessName(const QString& name); +#ifdef __linux__ + void setPollRate(std::chrono::milliseconds rate); +#endif + void setGrace(std::chrono::milliseconds grace); + void setStartWithGrace(bool graceFirst); + +//-Slots------------------------------------------------------------------------------------------------------------ +private slots: + void handleProcesStop(); + +public slots: + void handleAbort(); + void handleClosure(std::chrono::milliseconds timeout, bool force); + void bide(); + +//-Signals------------------------------------------------------------------------------------------------------------ +signals: + void processHooked(); + void processStopped(); + void processCloseFailed(); + void graceStarted(); + void complete(Outcome outcome); +}; + +class ProcessBider; + +class ProcessBiderManager +{ +//-Class Members--------------------------------------------------------------------------------------------- +private: + // Needs to be static so it can be locked before the the singleton is created, or else a race in instance() could occur. + static inline constinit QMutex smMutex; + +//-Instance Members------------------------------------------------------------------------------------------ +private: + QThread* mThread; + int mWorkerCount; + +//-Constructor---------------------------------------------------------------------------------------------- +private: + explicit ProcessBiderManager(); + +//-Destructor---------------------------------------------------------------------------------------------- +public: + ~ProcessBiderManager(); + +//-Class Functions---------------------------------------------------------------------------------------------- +public: + static Qx::ExclusiveAccess instance(); + +//-Instance Functions---------------------------------------------------------------------------------------------- +private: + void startThreadIfStopped(); + void stopThreadIfStarted(bool wait = false); + +public: + void registerBider(ProcessBider* bider); + void notifyWorkerFinished(); + +/*! @endcond */ +}; + +} + +#endif // QX_PROCCESSBIDER_P_H