From 38bfed7c5b6dbd6c79ad357d5202803886821f78 Mon Sep 17 00:00:00 2001 From: Radek Davidek Date: Fri, 27 Mar 2026 22:33:13 +0100 Subject: [PATCH] some improvements --- service/README.md | 53 ++++++- service/install-service.bat | 47 +++++++ service/src/main.cpp | 257 ++++++++++++++++++++++++++++++---- service/uninstall-service.bat | 23 +++ 4 files changed, 354 insertions(+), 26 deletions(-) create mode 100644 service/install-service.bat create mode 100644 service/uninstall-service.bat diff --git a/service/README.md b/service/README.md index e7beca5..e94b99a 100644 --- a/service/README.md +++ b/service/README.md @@ -73,6 +73,8 @@ cmake --build build ## Run +When started manually, the executable runs in console mode: + ```powershell .\build\Release\process-monitor.exe ``` @@ -83,9 +85,58 @@ Or specify custom config path: .\build\Release\process-monitor.exe .\my-config.conf ``` +To force console mode explicitly: + +```powershell +.\build\Release\process-monitor.exe --console .\my-config.conf +``` + +## Windows Service + +The executable can now run directly as a native Windows service through +`StartServiceCtrlDispatcher`. If started by the Service Control Manager, it +registers itself under the actual service name assigned in SCM and handles +stop/shutdown requests gracefully. + +Important notes: + +- Default config file is resolved relative to the executable directory, not the current working directory +- Log file is always written next to the executable as `process-monitor.log` +- Optional service argument after the executable path is treated as custom config path + +Create the service with default config: + +```cmd +sc create ProcessMonitorService binPath= "D:\path\to\process-monitor.exe" start= auto +``` + +Create the service with custom config: + +```cmd +sc create ProcessMonitorService binPath= "\"D:\path\to\process-monitor.exe\" \"D:\path\to\custom.conf\"" start= auto +``` + +Start and stop: + +```cmd +sc start ProcessMonitorService +sc stop ProcessMonitorService +``` + +Or install it with the bundled script: + +```cmd +install-service.bat +``` + +Remove the service with: + +```cmd +uninstall-service.bat +``` + ## Next useful improvements -- Run as Windows service - Add retry/backoff for failed API calls - Add richer payload items if your API needs both matched pattern and actual process name - Load config from JSON/YAML if richer metadata is needed diff --git a/service/install-service.bat b/service/install-service.bat new file mode 100644 index 0000000..73d532d --- /dev/null +++ b/service/install-service.bat @@ -0,0 +1,47 @@ +@echo off +setlocal + +set "SERVICE_NAME=ProcessMonitorService" +set "BASE_DIR=%~dp0" +set "EXE_PATH=%BASE_DIR%build\Release\process-monitor.exe" +set "CONFIG_PATH=%BASE_DIR%build\Release\process-monitor.conf" + +if not exist "%EXE_PATH%" ( + echo EXE not found: "%EXE_PATH%" + echo Build the project first. + exit /b 1 +) + +if not exist "%CONFIG_PATH%" ( + echo Config not found: "%CONFIG_PATH%" + echo Expected config next to the EXE. + exit /b 1 +) + +sc query "%SERVICE_NAME%" >nul 2>&1 +if %errorlevel% equ 0 ( + echo Service "%SERVICE_NAME%" already exists. Removing old service... + sc stop "%SERVICE_NAME%" >nul 2>&1 + sc delete "%SERVICE_NAME%" + timeout /t 2 /nobreak >nul +) + +echo Installing service "%SERVICE_NAME%"... +sc create "%SERVICE_NAME%" binPath= "\"%EXE_PATH%\" \"%CONFIG_PATH%\"" start= auto +if errorlevel 1 ( + echo Service installation failed. + exit /b 1 +) + +echo Setting service description... +sc description "%SERVICE_NAME%" "Process Monitor service" + +echo Starting service "%SERVICE_NAME%"... +sc start "%SERVICE_NAME%" +if errorlevel 1 ( + echo Service was installed, but start failed. + exit /b 1 +) + +echo Service "%SERVICE_NAME%" installed and started successfully. +exit /b 0 diff --git a/service/src/main.cpp b/service/src/main.cpp index 80e364e..7d12b42 100644 --- a/service/src/main.cpp +++ b/service/src/main.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -22,7 +23,12 @@ namespace { std::mutex g_logMutex; -const char* kLogFilePath = "process-monitor.log"; +std::wstring g_baseDirectory; +std::string g_logFilePath; +SERVICE_STATUS_HANDLE g_serviceStatusHandle = nullptr; +SERVICE_STATUS g_serviceStatus = {}; +HANDLE g_stopEvent = nullptr; +std::atomic g_runningAsService = false; struct Config { std::string apiUrl; @@ -86,24 +92,6 @@ std::wstring toWide(const std::string& value) { return wide; } -std::string getComputerNameUtf8() { - if (const char* envComputerName = std::getenv("COMPUTERNAME")) { - const std::string value = trim(envComputerName); - if (!value.empty()) { - return value; - } - } - - char buffer[MAX_COMPUTERNAME_LENGTH + 1] = {}; - DWORD size = static_cast(std::size(buffer)); - - if (!GetComputerNameA(buffer, &size)) { - return "unknown-host"; - } - - return std::string(buffer, size); -} - std::string toUtf8(const std::wstring& value) { if (value.empty()) { return {}; @@ -120,6 +108,107 @@ std::string toUtf8(const std::wstring& value) { return narrow; } +std::string getLastErrorMessage(DWORD errorCode) { + LPWSTR buffer = nullptr; + const DWORD size = FormatMessageW( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + errorCode, + 0, + reinterpret_cast(&buffer), + 0, + nullptr); + + if (size == 0 || buffer == nullptr) { + return "Windows error " + std::to_string(errorCode); + } + + std::wstring message(buffer, size); + LocalFree(buffer); + return toUtf8(message); +} + +std::wstring getExecutablePath() { + std::wstring path(MAX_PATH, L'\0'); + + while (true) { + const DWORD copied = GetModuleFileNameW(nullptr, path.data(), static_cast(path.size())); + if (copied == 0) { + throw std::runtime_error("GetModuleFileNameW failed."); + } + + if (copied < path.size() - 1) { + path.resize(copied); + return path; + } + + path.resize(path.size() * 2); + } +} + +std::wstring directoryName(const std::wstring& path) { + const auto separator = path.find_last_of(L"\\/"); + if (separator == std::wstring::npos) { + return L"."; + } + + return path.substr(0, separator); +} + +std::wstring joinPath(const std::wstring& base, const std::wstring& leaf) { + if (base.empty()) { + return leaf; + } + + if (base.back() == L'\\' || base.back() == L'/') { + return base + leaf; + } + + return base + L'\\' + leaf; +} + +bool isAbsolutePath(const std::wstring& path) { + if (path.size() >= 2 && path[1] == L':') { + return true; + } + + return path.size() >= 2 && path[0] == L'\\' && path[1] == L'\\'; +} + +std::string resolvePath(const std::string& path) { + if (path.empty()) { + return path; + } + + const auto widePath = toWide(path); + if (isAbsolutePath(widePath)) { + return path; + } + + return toUtf8(joinPath(g_baseDirectory, widePath)); +} + +std::string getComputerNameUtf8() { + char* envComputerName = nullptr; + std::size_t envLength = 0; + if (_dupenv_s(&envComputerName, &envLength, "COMPUTERNAME") == 0 && envComputerName != nullptr) { + const std::string value = trim(envComputerName); + free(envComputerName); + if (!value.empty()) { + return value; + } + } + + char buffer[MAX_COMPUTERNAME_LENGTH + 1] = {}; + DWORD size = static_cast(std::size(buffer)); + + if (!GetComputerNameA(buffer, &size)) { + return "unknown-host"; + } + + return std::string(buffer, size); +} + Config loadConfig(const std::string& path) { std::ifstream input(path); if (!input) { @@ -225,7 +314,7 @@ void logMessage(const std::string& message, bool isError = false) { const std::string line = "[" + iso8601NowUtc() + "] " + message; std::lock_guard lock(g_logMutex); - std::ofstream logFile(kLogFilePath, std::ios::app); + std::ofstream logFile(g_logFilePath, std::ios::app); if (logFile) { logFile << line << std::endl; } @@ -340,6 +429,33 @@ std::set enumerateRunningProcesses() { return processNames; } +void setServiceStatus(DWORD currentState, DWORD win32ExitCode = NO_ERROR, DWORD waitHint = 0) { + if (!g_runningAsService || g_serviceStatusHandle == nullptr) { + return; + } + + g_serviceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; + g_serviceStatus.dwCurrentState = currentState; + g_serviceStatus.dwWin32ExitCode = win32ExitCode; + g_serviceStatus.dwWaitHint = waitHint; + g_serviceStatus.dwControlsAccepted = (currentState == SERVICE_START_PENDING) + ? 0 + : SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN; + g_serviceStatus.dwCheckPoint = + (currentState == SERVICE_RUNNING || currentState == SERVICE_STOPPED) ? 0 : g_serviceStatus.dwCheckPoint + 1; + + SetServiceStatus(g_serviceStatusHandle, &g_serviceStatus); +} + +DWORD waitForStopOrTimeout(int intervalSeconds) { + if (g_stopEvent == nullptr) { + std::this_thread::sleep_for(std::chrono::seconds(intervalSeconds)); + return WAIT_TIMEOUT; + } + + return WaitForSingleObject(g_stopEvent, static_cast(intervalSeconds * 1000)); +} + bool postHeartbeat(const Config& config, const ParsedUrl& url, const std::vector& processNames) { try { const auto userAgent = L"process-monitor/0.1"; @@ -436,6 +552,10 @@ void monitorProcesses(const Config& config) { + std::to_string(config.intervalSeconds) + "s"); while (true) { + if (g_stopEvent != nullptr && WaitForSingleObject(g_stopEvent, 0) == WAIT_OBJECT_0) { + break; + } + try { const auto running = enumerateRunningProcesses(); const auto matches = findMatchingProcesses(running, config.processNames); @@ -446,18 +566,105 @@ void monitorProcesses(const Config& config) { logMessage(std::string("Monitoring cycle failed: ") + ex.what(), true); } - std::this_thread::sleep_for(std::chrono::seconds(config.intervalSeconds)); + if (waitForStopOrTimeout(config.intervalSeconds) == WAIT_OBJECT_0) { + break; + } } + + logMessage("Monitoring stopped"); +} + +void WINAPI serviceControlHandler(DWORD controlCode) { + switch (controlCode) { + case SERVICE_CONTROL_STOP: + case SERVICE_CONTROL_SHUTDOWN: + logMessage("Stop requested by Service Control Manager"); + setServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 10000); + if (g_stopEvent != nullptr) { + SetEvent(g_stopEvent); + } + break; + default: + break; + } + + setServiceStatus(g_serviceStatus.dwCurrentState, g_serviceStatus.dwWin32ExitCode, g_serviceStatus.dwWaitHint); +} + +void WINAPI serviceMain(DWORD argc, LPWSTR* argv) { + g_runningAsService = true; + const wchar_t* serviceName = (argc > 0 && argv != nullptr && argv[0] != nullptr && argv[0][0] != L'\0') + ? argv[0] + : L""; + g_serviceStatusHandle = RegisterServiceCtrlHandlerW(serviceName, serviceControlHandler); + if (g_serviceStatusHandle == nullptr) { + logMessage("RegisterServiceCtrlHandlerW failed: " + getLastErrorMessage(GetLastError()), true); + return; + } + + setServiceStatus(SERVICE_START_PENDING, NO_ERROR, 10000); + g_stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (g_stopEvent == nullptr) { + const DWORD error = GetLastError(); + logMessage("CreateEventW failed: " + getLastErrorMessage(error), true); + setServiceStatus(SERVICE_STOPPED, error); + return; + } + + try { + std::string configPath = "process-monitor.conf"; + if (argc > 1 && argv[1] != nullptr && argv[1][0] != L'\0') { + configPath = toUtf8(argv[1]); + } + + const auto config = loadConfig(resolvePath(configPath)); + setServiceStatus(SERVICE_RUNNING); + monitorProcesses(config); + setServiceStatus(SERVICE_STOPPED); + } catch (const std::exception& ex) { + logMessage(std::string("Service startup failed: ") + ex.what(), true); + setServiceStatus(SERVICE_STOPPED, ERROR_SERVICE_SPECIFIC_ERROR); + } + + if (g_stopEvent != nullptr) { + CloseHandle(g_stopEvent); + g_stopEvent = nullptr; + } +} + +int runConsole(int argc, char* argv[]) { + const std::string configPath = (argc > 1) ? argv[1] : "process-monitor.conf"; + const auto config = loadConfig(resolvePath(configPath)); + monitorProcesses(config); + return 0; } } // namespace int main(int argc, char* argv[]) { try { - const std::string configPath = (argc > 1) ? argv[1] : "process-monitor.conf"; - const auto config = loadConfig(configPath); - monitorProcesses(config); - return 0; + g_baseDirectory = directoryName(getExecutablePath()); + g_logFilePath = toUtf8(joinPath(g_baseDirectory, L"process-monitor.log")); + + if (argc > 1 && std::string(argv[1]) == "--console") { + return runConsole(argc - 1, argv + 1); + } + + SERVICE_TABLE_ENTRYW serviceTable[] = { + { const_cast(L""), serviceMain }, + { nullptr, nullptr } + }; + + if (StartServiceCtrlDispatcherW(serviceTable)) { + return 0; + } + + const DWORD error = GetLastError(); + if (error == ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) { + return runConsole(argc, argv); + } + + throw std::runtime_error("StartServiceCtrlDispatcherW failed: " + getLastErrorMessage(error)); } catch (const std::exception& ex) { logMessage(std::string("Startup failed: ") + ex.what(), true); return 1; diff --git a/service/uninstall-service.bat b/service/uninstall-service.bat new file mode 100644 index 0000000..043dbdf --- /dev/null +++ b/service/uninstall-service.bat @@ -0,0 +1,23 @@ +@echo off +setlocal + +set "SERVICE_NAME=ProcessMonitorService" + +sc query "%SERVICE_NAME%" >nul 2>&1 +if errorlevel 1 ( + echo Service "%SERVICE_NAME%" does not exist. + exit /b 0 +) + +echo Stopping service "%SERVICE_NAME%"... +sc stop "%SERVICE_NAME%" >nul 2>&1 + +echo Removing service "%SERVICE_NAME%"... +sc delete "%SERVICE_NAME%" +if errorlevel 1 ( + echo Service removal failed. + exit /b 1 +) + +echo Service "%SERVICE_NAME%" removed successfully. +exit /b 0