some improvements
This commit is contained in:
parent
3a14e79738
commit
38bfed7c5b
@ -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
|
||||
|
||||
47
service/install-service.bat
Normal file
47
service/install-service.bat
Normal 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
|
||||
@ -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;
|
||||
|
||||
23
service/uninstall-service.bat
Normal file
23
service/uninstall-service.bat
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user