some improvements
This commit is contained in:
parent
3a14e79738
commit
38bfed7c5b
@ -73,6 +73,8 @@ cmake --build build
|
|||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
|
When started manually, the executable runs in console mode:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\build\Release\process-monitor.exe
|
.\build\Release\process-monitor.exe
|
||||||
```
|
```
|
||||||
@ -83,9 +85,58 @@ Or specify custom config path:
|
|||||||
.\build\Release\process-monitor.exe .\my-config.conf
|
.\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
|
## Next useful improvements
|
||||||
|
|
||||||
- Run as Windows service
|
|
||||||
- Add retry/backoff for failed API calls
|
- Add retry/backoff for failed API calls
|
||||||
- Add richer payload items if your API needs both matched pattern and actual process name
|
- 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
|
- 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 <winhttp.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
@ -22,7 +23,12 @@
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
std::mutex g_logMutex;
|
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 {
|
struct Config {
|
||||||
std::string apiUrl;
|
std::string apiUrl;
|
||||||
@ -86,24 +92,6 @@ std::wstring toWide(const std::string& value) {
|
|||||||
return wide;
|
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) {
|
std::string toUtf8(const std::wstring& value) {
|
||||||
if (value.empty()) {
|
if (value.empty()) {
|
||||||
return {};
|
return {};
|
||||||
@ -120,6 +108,107 @@ std::string toUtf8(const std::wstring& value) {
|
|||||||
return narrow;
|
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) {
|
Config loadConfig(const std::string& path) {
|
||||||
std::ifstream input(path);
|
std::ifstream input(path);
|
||||||
if (!input) {
|
if (!input) {
|
||||||
@ -225,7 +314,7 @@ void logMessage(const std::string& message, bool isError = false) {
|
|||||||
const std::string line = "[" + iso8601NowUtc() + "] " + message;
|
const std::string line = "[" + iso8601NowUtc() + "] " + message;
|
||||||
std::lock_guard<std::mutex> lock(g_logMutex);
|
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) {
|
if (logFile) {
|
||||||
logFile << line << std::endl;
|
logFile << line << std::endl;
|
||||||
}
|
}
|
||||||
@ -340,6 +429,33 @@ std::set<std::string> enumerateRunningProcesses() {
|
|||||||
return processNames;
|
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) {
|
bool postHeartbeat(const Config& config, const ParsedUrl& url, const std::vector<std::string>& processNames) {
|
||||||
try {
|
try {
|
||||||
const auto userAgent = L"process-monitor/0.1";
|
const auto userAgent = L"process-monitor/0.1";
|
||||||
@ -436,6 +552,10 @@ void monitorProcesses(const Config& config) {
|
|||||||
+ std::to_string(config.intervalSeconds) + "s");
|
+ std::to_string(config.intervalSeconds) + "s");
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (g_stopEvent != nullptr && WaitForSingleObject(g_stopEvent, 0) == WAIT_OBJECT_0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const auto running = enumerateRunningProcesses();
|
const auto running = enumerateRunningProcesses();
|
||||||
const auto matches = findMatchingProcesses(running, config.processNames);
|
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);
|
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
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
try {
|
try {
|
||||||
const std::string configPath = (argc > 1) ? argv[1] : "process-monitor.conf";
|
g_baseDirectory = directoryName(getExecutablePath());
|
||||||
const auto config = loadConfig(configPath);
|
g_logFilePath = toUtf8(joinPath(g_baseDirectory, L"process-monitor.log"));
|
||||||
monitorProcesses(config);
|
|
||||||
|
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;
|
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) {
|
} catch (const std::exception& ex) {
|
||||||
logMessage(std::string("Startup failed: ") + ex.what(), true);
|
logMessage(std::string("Startup failed: ") + ex.what(), true);
|
||||||
return 1;
|
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