diff --git a/server/monitor-types/local-service.js b/server/monitor-types/local-service.js deleted file mode 100644 index aeb5e319c..000000000 --- a/server/monitor-types/local-service.js +++ /dev/null @@ -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} 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, -}; diff --git a/server/monitor-types/system-service.js b/server/monitor-types/system-service.js new file mode 100644 index 000000000..ec31c58bd --- /dev/null +++ b/server/monitor-types/system-service.js @@ -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} 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} 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} 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, +}; diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 33bdb099e..72238c1d8 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -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"); diff --git a/src/lang/en.json b/src/lang/en.json index c2d80a3e6..e58db0916 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -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", diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index b5459ae78..162f874b5 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -45,8 +45,8 @@ -