initial commit
This commit is contained in:
commit
212216b101
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
# Maven
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
.vscode/
|
||||
.classpath
|
||||
.project
|
||||
.settings/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
build
|
||||
58
java-api/README.md
Normal file
58
java-api/README.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Process Monitor API (Java 11)
|
||||
|
||||
Small Java 11 application that exposes `POST /hb/api` using `com.sun.net.httpserver.HttpServer`
|
||||
and stores received heartbeat records into MariaDB. JSON parsing uses `Gson`
|
||||
and the app logs incoming requests plus insert results to stdout.
|
||||
|
||||
## Request shape
|
||||
|
||||
Expected JSON body:
|
||||
|
||||
```json
|
||||
{
|
||||
"machine_name": "PC-01",
|
||||
"status": "running",
|
||||
"detected_at": "2026-03-27T21:00:00Z",
|
||||
"processes": ["chrome.exe", "discord.exe"]
|
||||
}
|
||||
```
|
||||
|
||||
If `processes` is omitted, the application still stores one row with `process_name = NULL`.
|
||||
|
||||
## Table DDL
|
||||
|
||||
```sql
|
||||
CREATE DATABASE IF NOT EXISTS process_monitor
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE process_monitor;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS process_heartbeat (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
machine_name VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
detected_at DATETIME(3) NOT NULL,
|
||||
process_name VARCHAR(255) NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_process_heartbeat_machine_detected (machine_name, detected_at),
|
||||
KEY idx_process_heartbeat_process_detected (process_name, detected_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
## Configure
|
||||
|
||||
Edit `src/main/resources/application.properties`.
|
||||
|
||||
## Run
|
||||
|
||||
```powershell
|
||||
mvn exec:java
|
||||
```
|
||||
|
||||
## Package
|
||||
|
||||
```powershell
|
||||
mvn package
|
||||
```
|
||||
65
java-api/pom.xml
Normal file
65
java-api/pom.xml
Normal file
@ -0,0 +1,65 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>cz.kamma.processmonitor</groupId>
|
||||
<artifactId>process-monitor-api</artifactId>
|
||||
<version>0.1.0</version>
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.13.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
<version>3.5.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<configuration>
|
||||
<mainClass>cz.kamma.processmonitor.Main</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<finalName>process-monitor-api</finalName>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>cz.kamma.processmonitor.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
17
java-api/schema.sql
Normal file
17
java-api/schema.sql
Normal file
@ -0,0 +1,17 @@
|
||||
CREATE DATABASE IF NOT EXISTS process_monitor
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE process_monitor;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS process_heartbeat (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
machine_name VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
detected_at DATETIME(3) NOT NULL,
|
||||
process_name VARCHAR(255) NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_process_heartbeat_machine_detected (machine_name, detected_at),
|
||||
KEY idx_process_heartbeat_process_detected (process_name, detected_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
212
java-api/src/main/java/cz/kamma/processmonitor/Main.java
Normal file
212
java-api/src/main/java/cz/kamma/processmonitor/Main.java
Normal file
@ -0,0 +1,212 @@
|
||||
package cz.kamma.processmonitor;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.sun.net.httpserver.Headers;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
public class Main {
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
AppConfig config = AppConfig.load();
|
||||
Database database = new Database(config);
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress(config.listenHost, config.listenPort), 0);
|
||||
server.createContext(config.apiPath, new HeartbeatHandler(database));
|
||||
server.setExecutor(null);
|
||||
server.start();
|
||||
|
||||
log("Listening on http://" + config.listenHost + ":" + config.listenPort + config.apiPath);
|
||||
}
|
||||
|
||||
private static final class HeartbeatHandler implements HttpHandler {
|
||||
private final Database database;
|
||||
|
||||
private HeartbeatHandler(Database database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
sendJson(exchange, 405, "{\"error\":\"method_not_allowed\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
Headers headers = exchange.getRequestHeaders();
|
||||
String contentType = headers.getFirst("Content-Type");
|
||||
if (contentType == null || !contentType.toLowerCase().contains("application/json")) {
|
||||
sendJson(exchange, 415, "{\"error\":\"unsupported_media_type\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
String body = readBody(exchange.getRequestBody());
|
||||
log("Incoming heartbeat request: " + body);
|
||||
HeartbeatRequest request = HeartbeatRequest.parse(body);
|
||||
int insertedRows = database.saveHeartbeat(request);
|
||||
log("Stored heartbeat for machine " + request.machineName + ", inserted rows: " + insertedRows);
|
||||
sendJson(exchange, 200, String.format("{\"ok\":true,\"inserted\":%d}", insertedRows));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
log("Request validation failed: " + ex.getMessage());
|
||||
sendJson(exchange, 400, "{\"error\":\"" + escapeJson(ex.getMessage()) + "\"}");
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
log("Internal server error: " + ex.getMessage());
|
||||
sendJson(exchange, 500, "{\"error\":\"internal_error\"}");
|
||||
}
|
||||
}
|
||||
|
||||
private static String readBody(InputStream inputStream) throws IOException {
|
||||
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static void sendJson(HttpExchange exchange, int statusCode, String body) throws IOException {
|
||||
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||
exchange.sendResponseHeaders(statusCode, bytes.length);
|
||||
try (OutputStream outputStream = exchange.getResponseBody()) {
|
||||
outputStream.write(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AppConfig {
|
||||
private final String listenHost;
|
||||
private final int listenPort;
|
||||
private final String apiPath;
|
||||
private final String jdbcUrl;
|
||||
private final String dbUser;
|
||||
private final String dbPassword;
|
||||
|
||||
private AppConfig(String listenHost, int listenPort, String apiPath, String jdbcUrl, String dbUser, String dbPassword) {
|
||||
this.listenHost = listenHost;
|
||||
this.listenPort = listenPort;
|
||||
this.apiPath = apiPath;
|
||||
this.jdbcUrl = jdbcUrl;
|
||||
this.dbUser = dbUser;
|
||||
this.dbPassword = dbPassword;
|
||||
}
|
||||
|
||||
private static AppConfig load() throws IOException {
|
||||
Properties properties = new Properties();
|
||||
try (InputStream inputStream = Main.class.getClassLoader().getResourceAsStream("application.properties")) {
|
||||
if (inputStream == null) {
|
||||
throw new IOException("Missing application.properties");
|
||||
}
|
||||
properties.load(inputStream);
|
||||
}
|
||||
|
||||
String listenHost = properties.getProperty("server.host", "0.0.0.0");
|
||||
int listenPort = Integer.parseInt(properties.getProperty("server.port", "8080"));
|
||||
String apiPath = properties.getProperty("server.path", "/hb/api");
|
||||
String jdbcUrl = required(properties, "db.url");
|
||||
String dbUser = required(properties, "db.user");
|
||||
String dbPassword = required(properties, "db.password");
|
||||
return new AppConfig(listenHost, listenPort, apiPath, jdbcUrl, dbUser, dbPassword);
|
||||
}
|
||||
|
||||
private static String required(Properties properties, String key) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing config key: " + key);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Database {
|
||||
private final AppConfig config;
|
||||
|
||||
private Database(AppConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
private int saveHeartbeat(HeartbeatRequest request) throws SQLException {
|
||||
List<String> processes = request.processes.isEmpty() ? Collections.singletonList(null) : request.processes;
|
||||
String sql = "INSERT INTO process_heartbeat (machine_name, status, detected_at, process_name) VALUES (?, ?, ?, ?)";
|
||||
|
||||
try (Connection connection = DriverManager.getConnection(config.jdbcUrl, config.dbUser, config.dbPassword);
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
int inserted = 0;
|
||||
for (String processName : processes) {
|
||||
statement.setString(1, request.machineName);
|
||||
statement.setString(2, request.status);
|
||||
statement.setTimestamp(3, Timestamp.from(request.detectedAt));
|
||||
statement.setString(4, processName);
|
||||
inserted += statement.executeUpdate();
|
||||
}
|
||||
return inserted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class HeartbeatRequest {
|
||||
private String machine_name;
|
||||
private String status;
|
||||
private String detected_at;
|
||||
private List<String> processes;
|
||||
|
||||
private transient String machineName;
|
||||
private transient Instant detectedAt;
|
||||
|
||||
private static HeartbeatRequest parse(String json) {
|
||||
try {
|
||||
HeartbeatRequest request = GSON.fromJson(json, HeartbeatRequest.class);
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("Empty request body");
|
||||
}
|
||||
request.validate();
|
||||
return request;
|
||||
} catch (JsonSyntaxException ex) {
|
||||
throw new IllegalArgumentException("Invalid JSON");
|
||||
}
|
||||
}
|
||||
|
||||
private void validate() {
|
||||
machineName = required(machine_name, "machine_name");
|
||||
status = required(status, "status");
|
||||
try {
|
||||
detectedAt = Instant.parse(required(detected_at, "detected_at"));
|
||||
} catch (DateTimeParseException ex) {
|
||||
throw new IllegalArgumentException("Invalid detected_at");
|
||||
}
|
||||
if (processes == null) {
|
||||
processes = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private static String required(String value, String fieldName) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing field: " + fieldName);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
private static void log(String message) {
|
||||
System.out.printf("[%s] %s%n", Instant.now(), message);
|
||||
}
|
||||
|
||||
private static String escapeJson(String value) {
|
||||
return value.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
}
|
||||
212
java-api/src/main/java/local/processmonitor/api/Main.java
Normal file
212
java-api/src/main/java/local/processmonitor/api/Main.java
Normal file
@ -0,0 +1,212 @@
|
||||
package local.processmonitor.api;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.sun.net.httpserver.Headers;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
public class Main {
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
AppConfig config = AppConfig.load();
|
||||
Database database = new Database(config);
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress(config.listenHost, config.listenPort), 0);
|
||||
server.createContext(config.apiPath, new HeartbeatHandler(database));
|
||||
server.setExecutor(null);
|
||||
server.start();
|
||||
|
||||
log("Listening on http://" + config.listenHost + ":" + config.listenPort + config.apiPath);
|
||||
}
|
||||
|
||||
private static final class HeartbeatHandler implements HttpHandler {
|
||||
private final Database database;
|
||||
|
||||
private HeartbeatHandler(Database database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
sendJson(exchange, 405, "{\"error\":\"method_not_allowed\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
Headers headers = exchange.getRequestHeaders();
|
||||
String contentType = headers.getFirst("Content-Type");
|
||||
if (contentType == null || !contentType.toLowerCase().contains("application/json")) {
|
||||
sendJson(exchange, 415, "{\"error\":\"unsupported_media_type\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
String body = readBody(exchange.getRequestBody());
|
||||
log("Incoming heartbeat request: " + body);
|
||||
HeartbeatRequest request = HeartbeatRequest.parse(body);
|
||||
int insertedRows = database.saveHeartbeat(request);
|
||||
log("Stored heartbeat for machine " + request.machineName + ", inserted rows: " + insertedRows);
|
||||
sendJson(exchange, 200, String.format("{\"ok\":true,\"inserted\":%d}", insertedRows));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
log("Request validation failed: " + ex.getMessage());
|
||||
sendJson(exchange, 400, "{\"error\":\"" + escapeJson(ex.getMessage()) + "\"}");
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
log("Internal server error: " + ex.getMessage());
|
||||
sendJson(exchange, 500, "{\"error\":\"internal_error\"}");
|
||||
}
|
||||
}
|
||||
|
||||
private static String readBody(InputStream inputStream) throws IOException {
|
||||
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static void sendJson(HttpExchange exchange, int statusCode, String body) throws IOException {
|
||||
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||
exchange.sendResponseHeaders(statusCode, bytes.length);
|
||||
try (OutputStream outputStream = exchange.getResponseBody()) {
|
||||
outputStream.write(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AppConfig {
|
||||
private final String listenHost;
|
||||
private final int listenPort;
|
||||
private final String apiPath;
|
||||
private final String jdbcUrl;
|
||||
private final String dbUser;
|
||||
private final String dbPassword;
|
||||
|
||||
private AppConfig(String listenHost, int listenPort, String apiPath, String jdbcUrl, String dbUser, String dbPassword) {
|
||||
this.listenHost = listenHost;
|
||||
this.listenPort = listenPort;
|
||||
this.apiPath = apiPath;
|
||||
this.jdbcUrl = jdbcUrl;
|
||||
this.dbUser = dbUser;
|
||||
this.dbPassword = dbPassword;
|
||||
}
|
||||
|
||||
private static AppConfig load() throws IOException {
|
||||
Properties properties = new Properties();
|
||||
try (InputStream inputStream = Main.class.getClassLoader().getResourceAsStream("application.properties")) {
|
||||
if (inputStream == null) {
|
||||
throw new IOException("Missing application.properties");
|
||||
}
|
||||
properties.load(inputStream);
|
||||
}
|
||||
|
||||
String listenHost = properties.getProperty("server.host", "0.0.0.0");
|
||||
int listenPort = Integer.parseInt(properties.getProperty("server.port", "8080"));
|
||||
String apiPath = properties.getProperty("server.path", "/hb/api");
|
||||
String jdbcUrl = required(properties, "db.url");
|
||||
String dbUser = required(properties, "db.user");
|
||||
String dbPassword = required(properties, "db.password");
|
||||
return new AppConfig(listenHost, listenPort, apiPath, jdbcUrl, dbUser, dbPassword);
|
||||
}
|
||||
|
||||
private static String required(Properties properties, String key) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing config key: " + key);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Database {
|
||||
private final AppConfig config;
|
||||
|
||||
private Database(AppConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
private int saveHeartbeat(HeartbeatRequest request) throws SQLException {
|
||||
List<String> processes = request.processes.isEmpty() ? Collections.singletonList(null) : request.processes;
|
||||
String sql = "INSERT INTO process_heartbeat (machine_name, status, detected_at, process_name) VALUES (?, ?, ?, ?)";
|
||||
|
||||
try (Connection connection = DriverManager.getConnection(config.jdbcUrl, config.dbUser, config.dbPassword);
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
int inserted = 0;
|
||||
for (String processName : processes) {
|
||||
statement.setString(1, request.machineName);
|
||||
statement.setString(2, request.status);
|
||||
statement.setTimestamp(3, Timestamp.from(request.detectedAt));
|
||||
statement.setString(4, processName);
|
||||
inserted += statement.executeUpdate();
|
||||
}
|
||||
return inserted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class HeartbeatRequest {
|
||||
private String machine_name;
|
||||
private String status;
|
||||
private String detected_at;
|
||||
private List<String> processes;
|
||||
|
||||
private transient String machineName;
|
||||
private transient Instant detectedAt;
|
||||
|
||||
private static HeartbeatRequest parse(String json) {
|
||||
try {
|
||||
HeartbeatRequest request = GSON.fromJson(json, HeartbeatRequest.class);
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("Empty request body");
|
||||
}
|
||||
request.validate();
|
||||
return request;
|
||||
} catch (JsonSyntaxException ex) {
|
||||
throw new IllegalArgumentException("Invalid JSON");
|
||||
}
|
||||
}
|
||||
|
||||
private void validate() {
|
||||
machineName = required(machine_name, "machine_name");
|
||||
status = required(status, "status");
|
||||
try {
|
||||
detectedAt = Instant.parse(required(detected_at, "detected_at"));
|
||||
} catch (DateTimeParseException ex) {
|
||||
throw new IllegalArgumentException("Invalid detected_at");
|
||||
}
|
||||
if (processes == null) {
|
||||
processes = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private static String required(String value, String fieldName) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing field: " + fieldName);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
private static void log(String message) {
|
||||
System.out.printf("[%s] %s%n", Instant.now(), message);
|
||||
}
|
||||
|
||||
private static String escapeJson(String value) {
|
||||
return value.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
}
|
||||
8
java-api/src/main/resources/application.properties
Normal file
8
java-api/src/main/resources/application.properties
Normal file
@ -0,0 +1,8 @@
|
||||
server.host=0.0.0.0
|
||||
server.port=80
|
||||
server.path=/hb/api
|
||||
|
||||
db.url=jdbc:mariadb://127.0.0.1:3306/process_monitor?useUnicode=true&characterEncoding=utf8
|
||||
# Change these credentials before running.
|
||||
db.user=process_monitor
|
||||
db.password=process_monitor_secret
|
||||
19
service/CMakeLists.txt
Normal file
19
service/CMakeLists.txt
Normal file
@ -0,0 +1,19 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
project(process_monitor VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
add_executable(process-monitor
|
||||
src/main.cpp
|
||||
)
|
||||
|
||||
target_compile_features(process-monitor PRIVATE cxx_std_17)
|
||||
target_compile_definitions(process-monitor PRIVATE UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX)
|
||||
target_link_libraries(process-monitor PRIVATE winhttp)
|
||||
|
||||
if(MSVC)
|
||||
set_property(TARGET process-monitor PROPERTY
|
||||
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||
target_compile_options(process-monitor PRIVATE /W4 /permissive-)
|
||||
else()
|
||||
target_compile_options(process-monitor PRIVATE -Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
91
service/README.md
Normal file
91
service/README.md
Normal file
@ -0,0 +1,91 @@
|
||||
# Process Monitor
|
||||
|
||||
Simple Windows C++ application that checks running processes every 30 seconds
|
||||
and sends one HTTP heartbeat with all matching processes.
|
||||
|
||||
## What it does
|
||||
|
||||
- Enumerates running Windows processes via ToolHelp API
|
||||
- Finds processes by partial name match
|
||||
- Sends one JSON payload with all currently matched processes
|
||||
- Builds with CMake without external runtime dependencies
|
||||
|
||||
## Expected payload
|
||||
|
||||
The application sends HTTP `POST` with `Content-Type: application/json`.
|
||||
|
||||
```json
|
||||
{
|
||||
"machine_name": "PC-01",
|
||||
"status": "running",
|
||||
"detected_at": "2026-03-27T12:34:56Z",
|
||||
"processes": ["notepad.exe", "notepad++.exe"]
|
||||
}
|
||||
```
|
||||
|
||||
If `api_token` is set, request header `Authorization: Bearer <token>` is added.
|
||||
|
||||
If no process matches in a cycle, the application still sends a heartbeat, but without the `processes` field:
|
||||
|
||||
```json
|
||||
{
|
||||
"machine_name": "PC-01",
|
||||
"status": "running",
|
||||
"detected_at": "2026-03-27T12:34:56Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `process-monitor.conf`.
|
||||
|
||||
```ini
|
||||
api_url=http://10.0.0.147/hb/api
|
||||
api_token=
|
||||
machine_name=
|
||||
interval_seconds=30
|
||||
request_timeout_seconds=2
|
||||
process_names=fortnite,chrome,discord,steam
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `machine_name` is optional; if empty, Windows computer name is used
|
||||
- `process_names` is a comma-separated list of substrings to search in executable names
|
||||
- `interval_seconds` can be changed from the default `30`
|
||||
- `request_timeout_seconds` sets WinHTTP connect/send/receive timeout in seconds
|
||||
|
||||
## Build
|
||||
|
||||
Developer Command Prompt for Visual Studio:
|
||||
|
||||
```powershell
|
||||
cmake -S . -B build
|
||||
cmake --build build --config Release
|
||||
```
|
||||
|
||||
Or with Ninja if you have a compiler environment ready:
|
||||
|
||||
```powershell
|
||||
cmake -S . -B build -G Ninja
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```powershell
|
||||
.\build\Release\process-monitor.exe
|
||||
```
|
||||
|
||||
Or specify custom config path:
|
||||
|
||||
```powershell
|
||||
.\build\Release\process-monitor.exe .\my-config.conf
|
||||
```
|
||||
|
||||
## 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
|
||||
8
service/process-monitor.conf
Normal file
8
service/process-monitor.conf
Normal file
@ -0,0 +1,8 @@
|
||||
# Copy this file to process-monitor.conf and adjust values.
|
||||
|
||||
api_url=http://10.0.0.147/hb/api
|
||||
api_token=
|
||||
machine_name=
|
||||
interval_seconds=30
|
||||
request_timeout_seconds=2
|
||||
process_names=fortnite,chrome,discord,steam
|
||||
465
service/src/main.cpp
Normal file
465
service/src/main.cpp
Normal file
@ -0,0 +1,465 @@
|
||||
#include <windows.h>
|
||||
#include <tlhelp32.h>
|
||||
#include <winhttp.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cctype>
|
||||
#include <ctime>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
std::mutex g_logMutex;
|
||||
const char* kLogFilePath = "process-monitor.log";
|
||||
|
||||
struct Config {
|
||||
std::string apiUrl;
|
||||
std::string apiToken;
|
||||
std::string machineName;
|
||||
int intervalSeconds = 30;
|
||||
int requestTimeoutSeconds = 2;
|
||||
std::vector<std::string> processNames;
|
||||
};
|
||||
|
||||
std::string trim(const std::string& value) {
|
||||
const auto begin = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) {
|
||||
return std::isspace(ch) != 0;
|
||||
});
|
||||
const auto end = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) {
|
||||
return std::isspace(ch) != 0;
|
||||
}).base();
|
||||
|
||||
if (begin >= end) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::string(begin, end);
|
||||
}
|
||||
|
||||
std::string toLower(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
std::vector<std::string> splitList(const std::string& value) {
|
||||
std::vector<std::string> items;
|
||||
std::stringstream stream(value);
|
||||
std::string item;
|
||||
|
||||
while (std::getline(stream, item, ',')) {
|
||||
const auto normalized = toLower(trim(item));
|
||||
if (!normalized.empty()) {
|
||||
items.push_back(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
std::wstring toWide(const std::string& value) {
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const int sizeNeeded = MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, nullptr, 0);
|
||||
if (sizeNeeded <= 0) {
|
||||
throw std::runtime_error("Failed to convert UTF-8 to UTF-16.");
|
||||
}
|
||||
|
||||
std::wstring wide(static_cast<std::size_t>(sizeNeeded), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, wide.data(), sizeNeeded);
|
||||
wide.pop_back();
|
||||
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 {};
|
||||
}
|
||||
|
||||
const int sizeNeeded = WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, nullptr, 0, nullptr, nullptr);
|
||||
if (sizeNeeded <= 0) {
|
||||
throw std::runtime_error("Failed to convert UTF-16 to UTF-8.");
|
||||
}
|
||||
|
||||
std::string narrow(static_cast<std::size_t>(sizeNeeded), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, narrow.data(), sizeNeeded, nullptr, nullptr);
|
||||
narrow.pop_back();
|
||||
return narrow;
|
||||
}
|
||||
|
||||
Config loadConfig(const std::string& path) {
|
||||
std::ifstream input(path);
|
||||
if (!input) {
|
||||
throw std::runtime_error("Cannot open config file: " + path);
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> values;
|
||||
std::string line;
|
||||
|
||||
while (std::getline(input, line)) {
|
||||
const auto cleaned = trim(line);
|
||||
if (cleaned.empty() || cleaned[0] == '#' || cleaned[0] == ';') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto separator = cleaned.find('=');
|
||||
if (separator == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto key = toLower(trim(cleaned.substr(0, separator)));
|
||||
const auto value = trim(cleaned.substr(separator + 1));
|
||||
values[key] = value;
|
||||
}
|
||||
|
||||
Config config;
|
||||
config.apiUrl = values["api_url"];
|
||||
config.apiToken = values["api_token"];
|
||||
config.machineName = values["machine_name"].empty() ? getComputerNameUtf8() : values["machine_name"];
|
||||
config.processNames = splitList(values["process_names"]);
|
||||
|
||||
if (values.count("interval_seconds") > 0) {
|
||||
config.intervalSeconds = std::max(1, std::atoi(values["interval_seconds"].c_str()));
|
||||
}
|
||||
|
||||
if (values.count("request_timeout_seconds") > 0) {
|
||||
config.requestTimeoutSeconds = std::max(1, std::atoi(values["request_timeout_seconds"].c_str()));
|
||||
}
|
||||
|
||||
if (config.apiUrl.empty()) {
|
||||
throw std::runtime_error("Missing required config key: api_url");
|
||||
}
|
||||
|
||||
if (config.processNames.empty()) {
|
||||
throw std::runtime_error("Missing required config key: process_names");
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
struct ParsedUrl {
|
||||
bool secure = false;
|
||||
INTERNET_PORT port = 0;
|
||||
std::wstring host;
|
||||
std::wstring path;
|
||||
};
|
||||
|
||||
ParsedUrl parseUrl(const std::string& rawUrl) {
|
||||
const auto wideUrl = toWide(rawUrl);
|
||||
|
||||
URL_COMPONENTS components = {};
|
||||
components.dwStructSize = sizeof(components);
|
||||
components.dwSchemeLength = static_cast<DWORD>(-1);
|
||||
components.dwHostNameLength = static_cast<DWORD>(-1);
|
||||
components.dwUrlPathLength = static_cast<DWORD>(-1);
|
||||
components.dwExtraInfoLength = static_cast<DWORD>(-1);
|
||||
|
||||
if (!WinHttpCrackUrl(wideUrl.c_str(), 0, 0, &components)) {
|
||||
throw std::runtime_error("Invalid api_url: " + rawUrl);
|
||||
}
|
||||
|
||||
ParsedUrl parsed;
|
||||
parsed.secure = (components.nScheme == INTERNET_SCHEME_HTTPS);
|
||||
parsed.port = components.nPort;
|
||||
parsed.host.assign(components.lpszHostName, components.dwHostNameLength);
|
||||
parsed.path.assign(components.lpszUrlPath, components.dwUrlPathLength);
|
||||
|
||||
if (components.dwExtraInfoLength > 0) {
|
||||
parsed.path.append(components.lpszExtraInfo, components.dwExtraInfoLength);
|
||||
}
|
||||
|
||||
if (parsed.path.empty()) {
|
||||
parsed.path = L"/";
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
std::string iso8601NowUtc() {
|
||||
using clock = std::chrono::system_clock;
|
||||
const auto now = clock::now();
|
||||
const std::time_t current = clock::to_time_t(now);
|
||||
|
||||
std::tm utcTime {};
|
||||
gmtime_s(&utcTime, ¤t);
|
||||
|
||||
std::ostringstream output;
|
||||
output << std::put_time(&utcTime, "%Y-%m-%dT%H:%M:%SZ");
|
||||
return output.str();
|
||||
}
|
||||
|
||||
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);
|
||||
if (logFile) {
|
||||
logFile << line << std::endl;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
std::cerr << line << std::endl;
|
||||
} else {
|
||||
std::cout << line << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
std::string escapeJson(const std::string& value) {
|
||||
std::ostringstream escaped;
|
||||
|
||||
for (const unsigned char ch : value) {
|
||||
switch (ch) {
|
||||
case '\\':
|
||||
escaped << "\\\\";
|
||||
break;
|
||||
case '"':
|
||||
escaped << "\\\"";
|
||||
break;
|
||||
case '\b':
|
||||
escaped << "\\b";
|
||||
break;
|
||||
case '\f':
|
||||
escaped << "\\f";
|
||||
break;
|
||||
case '\n':
|
||||
escaped << "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
escaped << "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
escaped << "\\t";
|
||||
break;
|
||||
default:
|
||||
if (ch < 0x20) {
|
||||
escaped << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast<int>(ch);
|
||||
} else {
|
||||
escaped << static_cast<char>(ch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return escaped.str();
|
||||
}
|
||||
|
||||
std::vector<std::string> findMatchingProcesses(
|
||||
const std::set<std::string>& runningProcesses,
|
||||
const std::vector<std::string>& configuredPatterns) {
|
||||
std::vector<std::string> matches;
|
||||
|
||||
for (const auto& runningProcess : runningProcesses) {
|
||||
for (const auto& pattern : configuredPatterns) {
|
||||
if (runningProcess.find(pattern) != std::string::npos) {
|
||||
matches.push_back(runningProcess);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
std::string buildPayload(const Config& config, const std::vector<std::string>& processNames) {
|
||||
std::ostringstream payload;
|
||||
payload
|
||||
<< "{"
|
||||
<< "\"machine_name\":\"" << escapeJson(config.machineName) << "\","
|
||||
<< "\"status\":\"running\","
|
||||
<< "\"detected_at\":\"" << escapeJson(iso8601NowUtc()) << "\"";
|
||||
|
||||
if (!processNames.empty()) {
|
||||
payload << ",\"processes\":[";
|
||||
for (std::size_t index = 0; index < processNames.size(); ++index) {
|
||||
if (index > 0) {
|
||||
payload << ",";
|
||||
}
|
||||
payload << "\"" << escapeJson(processNames[index]) << "\"";
|
||||
}
|
||||
payload << "]";
|
||||
}
|
||||
|
||||
payload << "}";
|
||||
return payload.str();
|
||||
}
|
||||
|
||||
std::string narrowUrlPath(const ParsedUrl& url) {
|
||||
return toUtf8(url.path);
|
||||
}
|
||||
|
||||
std::set<std::string> enumerateRunningProcesses() {
|
||||
std::set<std::string> processNames;
|
||||
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snapshot == INVALID_HANDLE_VALUE) {
|
||||
throw std::runtime_error("CreateToolhelp32Snapshot failed.");
|
||||
}
|
||||
|
||||
PROCESSENTRY32W entry = {};
|
||||
entry.dwSize = sizeof(entry);
|
||||
|
||||
if (Process32FirstW(snapshot, &entry)) {
|
||||
do {
|
||||
processNames.insert(toLower(toUtf8(entry.szExeFile)));
|
||||
} while (Process32NextW(snapshot, &entry));
|
||||
}
|
||||
|
||||
CloseHandle(snapshot);
|
||||
return processNames;
|
||||
}
|
||||
|
||||
bool postHeartbeat(const Config& config, const ParsedUrl& url, const std::vector<std::string>& processNames) {
|
||||
try {
|
||||
const auto userAgent = L"process-monitor/0.1";
|
||||
const auto payload = buildPayload(config, processNames);
|
||||
const auto urlForLog = toUtf8(url.host) + narrowUrlPath(url);
|
||||
const auto wideHeaders = [&config]() {
|
||||
std::wstring headers = L"Content-Type: application/json\r\n";
|
||||
if (!config.apiToken.empty()) {
|
||||
headers += L"Authorization: Bearer ";
|
||||
headers += toWide(config.apiToken);
|
||||
headers += L"\r\n";
|
||||
}
|
||||
return headers;
|
||||
}();
|
||||
|
||||
logMessage("API request -> POST " + urlForLog + " payload: " + payload);
|
||||
|
||||
HINTERNET session = WinHttpOpen(userAgent, WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
|
||||
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
|
||||
if (!session) {
|
||||
logMessage("API request failed before connect: WinHttpOpen failed", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
const int requestTimeoutMs = config.requestTimeoutSeconds * 1000;
|
||||
WinHttpSetTimeouts(
|
||||
session,
|
||||
requestTimeoutMs,
|
||||
requestTimeoutMs,
|
||||
requestTimeoutMs,
|
||||
requestTimeoutMs);
|
||||
|
||||
HINTERNET connection = WinHttpConnect(session, url.host.c_str(), url.port, 0);
|
||||
if (!connection) {
|
||||
WinHttpCloseHandle(session);
|
||||
logMessage("API request failed before send: WinHttpConnect failed", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
const DWORD flags = url.secure ? WINHTTP_FLAG_SECURE : 0;
|
||||
HINTERNET request = WinHttpOpenRequest(connection, L"POST", url.path.c_str(),
|
||||
nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, flags);
|
||||
if (!request) {
|
||||
WinHttpCloseHandle(connection);
|
||||
WinHttpCloseHandle(session);
|
||||
logMessage("API request failed before send: WinHttpOpenRequest failed", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
const BOOL sendResult = WinHttpSendRequest(
|
||||
request,
|
||||
wideHeaders.c_str(),
|
||||
static_cast<DWORD>(wideHeaders.size()),
|
||||
const_cast<char*>(payload.data()),
|
||||
static_cast<DWORD>(payload.size()),
|
||||
static_cast<DWORD>(payload.size()),
|
||||
0);
|
||||
|
||||
bool success = false;
|
||||
if (sendResult && WinHttpReceiveResponse(request, nullptr)) {
|
||||
DWORD statusCode = 0;
|
||||
DWORD statusCodeSize = sizeof(statusCode);
|
||||
if (WinHttpQueryHeaders(request,
|
||||
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
WINHTTP_HEADER_NAME_BY_INDEX,
|
||||
&statusCode,
|
||||
&statusCodeSize,
|
||||
WINHTTP_NO_HEADER_INDEX)) {
|
||||
success = (statusCode >= 200 && statusCode < 300);
|
||||
logMessage("API response <- HTTP " + std::to_string(statusCode)
|
||||
+ (success ? " success" : " failure"));
|
||||
}
|
||||
} else {
|
||||
logMessage("API request failed during send/receive", true);
|
||||
}
|
||||
|
||||
WinHttpCloseHandle(request);
|
||||
WinHttpCloseHandle(connection);
|
||||
WinHttpCloseHandle(session);
|
||||
return success;
|
||||
} catch (const std::exception& ex) {
|
||||
logMessage(std::string("Heartbeat request failed: ") + ex.what(), true);
|
||||
return false;
|
||||
}
|
||||
catch (...) {
|
||||
logMessage("Heartbeat request failed: unknown error", true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void monitorProcesses(const Config& config) {
|
||||
const auto url = parseUrl(config.apiUrl);
|
||||
logMessage("Monitoring " + std::to_string(config.processNames.size()) + " patterns every "
|
||||
+ std::to_string(config.intervalSeconds) + "s");
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const auto running = enumerateRunningProcesses();
|
||||
const auto matches = findMatchingProcesses(running, config.processNames);
|
||||
const bool sent = postHeartbeat(config, url, matches);
|
||||
logMessage(std::to_string(matches.size()) + " matching process(es) -> "
|
||||
+ (sent ? "reported" : "failed"));
|
||||
} catch (const std::exception& ex) {
|
||||
logMessage(std::string("Monitoring cycle failed: ") + ex.what(), true);
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::seconds(config.intervalSeconds));
|
||||
}
|
||||
}
|
||||
|
||||
} // 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;
|
||||
} catch (const std::exception& ex) {
|
||||
logMessage(std::string("Startup failed: ") + ex.what(), true);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user