Added Windows Service Monitor & changed local to systen

This commit is contained in:
iotux 2025-12-15 16:27:20 +01:00
parent fe50adb061
commit 0f951ef123
5 changed files with 123 additions and 68 deletions

View File

@ -1,55 +0,0 @@
const { MonitorType } = require("./monitor-type");
const { execFile } = require("child_process");
const { DOWN, UP } = require("../../src/util");
class LocalServiceMonitorType extends MonitorType {
name = "local-service";
description = "Checks if a local service is running by executing a command.";
/**
* Check a local systemd service status.
* Uses `systemctl is-running` to determine if the service is active.
* @param {object} monitor The monitor object containing monitor.local_service_name.
* @param {object} heartbeat The heartbeat object to update.
* @param {object} server The server object (unused in this specific check).
* @returns {Promise<object>} A promise that resolves with the updated heartbeat.
* @throws {Error} If the monitor.local_service_name is invalid or the command execution fails.
*/
async check(monitor, heartbeat, server) {
// Basic sanitization to prevent argument injection.
// This regex allows for standard service names, including those with instances like "sshd@.service".
if (!monitor.local_service_name || !/^[a-zA-Z0-9._\-@]+$/.test(monitor.local_service_name)) {
heartbeat.status = DOWN;
heartbeat.msg = "Invalid service name provided.";
throw new Error(heartbeat.msg);
}
return new Promise((resolve, reject) => {
execFile("systemctl", [ "is-active", monitor.local_service_name ], (error, stdout, stderr) => {
// systemctl is-active exits with 0 if the service is active,
// and a non-zero code if it is inactive, failed, or not found.
// stderr often contains useful info like "service not found"
let output = (stderr || stdout || "").toString().trim();
if (output.length > 200) {
output = output.substring(0, 200) + "...";
}
if (error) {
heartbeat.msg = stderr || stdout || `Service '${monitor.local_service_name}' is not running.`;
reject(new Error(heartbeat.msg));
return;
}
// If there's no error, the service is running.
heartbeat.status = UP;
heartbeat.msg = `Service '${monitor.local_service_name}' is running.`;
resolve();
});
});
}
}
module.exports = {
LocalServiceMonitorType,
};

View File

@ -0,0 +1,110 @@
const { MonitorType } = require("./monitor-type");
const { execFile } = require("child_process");
const process = require("process");
const { DOWN, UP } = require("../../src/util");
class SystemServiceMonitorType extends MonitorType {
name = "system-service";
description = "Checks if a system service is running (systemd on Linux, Service Manager on Windows).";
/**
* Check the system service status.
* Detects OS and dispatches to the appropriate check method.
* @param {object} monitor The monitor object containing monitor.system_service_name.
* @param {object} heartbeat The heartbeat object to update.
* @returns {Promise<void>} Resolves when check is complete.
*/
async check(monitor, heartbeat) {
// Use the new variable name 'system_service_name' to match the monitor type change
const serviceName = monitor.system_service_name;
// Basic sanitization.
// We do not allow spaces to ensure users use the "Service Name" (wuauserv) and not "Display Name".
if (!serviceName || !/^[a-zA-Z0-9._\-@]+$/.test(serviceName)) {
heartbeat.status = DOWN;
heartbeat.msg = "Invalid service name. Please use the internal Service Name (no spaces).";
throw new Error(heartbeat.msg);
}
if (process.platform === "win32") {
return this.checkWindows(serviceName, heartbeat);
} else {
return this.checkLinux(serviceName, heartbeat);
}
}
/**
* Linux Check (Systemd)
* @param {string} serviceName The name of the service to check.
* @param {object} heartbeat The heartbeat object.
* @returns {Promise<void>} Resolves on success, rejects on error.
*/
async checkLinux(serviceName, heartbeat) {
return new Promise((resolve, reject) => {
// Linter requires spaces inside array brackets: [ "arg1", "arg2" ]
execFile("systemctl", [ "is-active", serviceName ], (error, stdout, stderr) => {
// Combine output and truncate to ~200 chars to prevent DB bloat
let output = (stderr || stdout || "").toString().trim();
if (output.length > 200) {
output = output.substring(0, 200) + "...";
}
if (error) {
heartbeat.msg = output || `Service '${serviceName}' is not running.`;
reject(new Error(heartbeat.msg));
return;
}
heartbeat.status = UP;
heartbeat.msg = `Service '${serviceName}' is running.`;
resolve();
});
});
}
/**
* Windows Check (PowerShell)
* @param {string} serviceName The name of the service to check.
* @param {object} heartbeat The heartbeat object.
* @returns {Promise<void>} Resolves on success, rejects on error.
*/
async checkWindows(serviceName, heartbeat) {
return new Promise((resolve, reject) => {
const cmd = "powershell";
// -NoProfile: Faster startup, -NonInteractive: No prompts
const args = [
"-NoProfile",
"-NonInteractive",
"-Command",
`(Get-Service -Name "${serviceName}").Status`
];
execFile(cmd, args, (error, stdout, stderr) => {
let output = (stderr || stdout || "").toString().trim();
if (output.length > 200) {
output = output.substring(0, 200) + "...";
}
// PowerShell writes to stderr if the service is not found
if (error || stderr) {
heartbeat.msg = output || `Service '${serviceName}' is not running/found.`;
reject(new Error(heartbeat.msg));
return;
}
if (output === "Running") {
heartbeat.status = UP;
heartbeat.msg = `Service '${serviceName}' is running.`;
resolve();
} else {
heartbeat.msg = `Service '${serviceName}' is ${output}.`;
reject(new Error(heartbeat.msg));
}
});
});
}
}
module.exports = {
SystemServiceMonitorType,
};

View File

@ -124,7 +124,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["port"] = new TCPMonitorType();
UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType();
UptimeKumaServer.monitorTypeList["redis"] = new RedisMonitorType();
UptimeKumaServer.monitorTypeList["local-service"] = new LocalServiceMonitorType();
UptimeKumaServer.monitorTypeList["system-service"] = new SystemServiceMonitorType();
// Allow all CORS origins (polling) in development
let cors = undefined;
@ -571,6 +571,6 @@ const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq");
const { TCPMonitorType } = require("./monitor-types/tcp.js");
const { ManualMonitorType } = require("./monitor-types/manual");
const { RedisMonitorType } = require("./monitor-types/redis");
const { LocalServiceMonitorType } = require("./monitor-types/local-service");
const { SystemServiceMonitorType } = require("./monitor-types/system-service");
const Monitor = require("./model/monitor");

View File

@ -1024,10 +1024,10 @@
"useRemoteBrowser": "Use a Remote Browser",
"deleteRemoteBrowserMessage": "Are you sure want to delete this Remote Browser for all monitors?",
"GrafanaOncallUrl": "Grafana Oncall URL",
"Local Service": "Local Service",
"System Service": "System Service",
"Service Name": "Service Name",
"localServiceDescription": "The internal service name used by the OS (systemd for Linux, Service Name for Windows).",
"localServiceDebugHelp": "To debug, run this on your server: {linuxCommand} (Linux) or {windowsCommand} (Windows PowerShell). The command must return 'active' or 'Running'.",
"systemServiceDescription": "The internal service name used by the OS (systemd for Linux, Service Name for Windows).",
"systemServiceDebugHelp": "To debug, run this on your server: {linuxCommand} (Linux) or {windowsCommand} (Windows PowerShell). The command must return 'active' or 'Running'.",
"Browser Screenshot": "Browser Screenshot",
"Command": "Command",
"Expected Output": "Expected Output",

View File

@ -45,8 +45,8 @@
<option value="docker">
{{ $t("Docker Container") }}
</option>
<option value="local-service">
{{ $t("Local Service") }}
<option value="system-service">
{{ $t("System Service") }}
</option>
<option value="real-browser">
HTTP(s) - Browser Engine (Chrome/Chromium) (Beta)
@ -665,15 +665,15 @@
</div>
</template>
<template v-if="monitor.type === 'local-service'">
<template v-if="monitor.type === 'system-service'">
<div class="my-3">
<label for="local-service-name" class="form-label">{{ $t("Service Name") }}</label>
<input id="local-service-name" v-model="monitor.local_service_name" type="text" class="form-control" required placeholder="nginx.service">
<label for="system-service-name" class="form-label">{{ $t("Service Name") }}</label>
<input id="system-service-name" v-model="monitor.system_service_name" type="text" class="form-control" required placeholder="nginx.service">
<div class="form-text">
{{ $t("localServiceDescription") }}
{{ $t("systemServiceDescription") }}
<div class="mt-2">
<i18n-t keypath="localServiceDebugHelp" tag="span">
<i18n-t keypath="systemServiceDebugHelp" tag="span">
<template #linuxCommand>
<code>systemctl is-active &lt;service&gt;</code>
</template>
@ -1383,7 +1383,7 @@ const monitorDefaults = {
rabbitmqUsername: "",
rabbitmqPassword: "",
conditions: [],
local_service_name: "",
system_service_name: "",
};
export default {