some improvements

This commit is contained in:
Radek Davidek 2026-03-27 22:33:13 +01:00
parent 3a14e79738
commit 38bfed7c5b
4 changed files with 354 additions and 26 deletions

View File

@ -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

View File

@ -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

View File

@ -3,6 +3,7 @@
#include <winhttp.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cctype>
#include <ctime>
@ -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<bool> 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<DWORD>(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<LPWSTR>(&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<DWORD>(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<DWORD>(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<std::mutex> 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<std::string> 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<DWORD>(intervalSeconds * 1000));
}
bool postHeartbeat(const Config& config, const ParsedUrl& url, const std::vector<std::string>& 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<LPWSTR>(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;

View File

@ -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