From 590f90e3f96454dfc53566c4d8795b56c0c66943 Mon Sep 17 00:00:00 2001 From: KraoESPfan1n <136997481+KraoESPfan1n@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:13:11 -0400 Subject: [PATCH] feat(system-service): add PM2 picker and platform selection (#7114) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Frank Elsinga --- server/model/monitor.js | 16 +++ server/monitor-types/pm2.js | 36 +++++ server/monitor-types/system-service.js | 13 +- .../socket-handlers/general-socket-handler.js | 16 +++ server/uptime-kuma-server.js | 2 + server/util/pm2.js | 74 +++++++++++ src/lang/en.json | 6 +- src/pages/EditMonitor.vue | 124 +++++++++++++++++- test/backend-test/test-pm2.js | 123 +++++++++++++++++ 9 files changed, 400 insertions(+), 10 deletions(-) create mode 100644 server/monitor-types/pm2.js create mode 100644 server/util/pm2.js create mode 100644 test/backend-test/test-pm2.js diff --git a/server/model/monitor.js b/server/model/monitor.js index 96cbf99cd..fb117e661 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1692,6 +1692,22 @@ class Monitor extends BeanModel { } } + if (["system-service", "pm2"].includes(this.type)) { + this.system_service_name = (this.system_service_name || "").trim(); + + if (!this.system_service_name) { + throw new Error(this.type === "pm2" ? "PM2 process name is required." : "Service Name is required."); + } + } + + if (this.type === "system-service" && !/^[a-zA-Z0-9._\-@]+$/.test(this.system_service_name)) { + throw new Error("Invalid service name. Please use the internal Service Name (no spaces)."); + } + + if (this.type === "pm2" && /[\u0000-\u001F\u007F]/.test(this.system_service_name)) { + throw new Error("Invalid PM2 process name."); + } + if (this.type === "ping") { // ping parameters validation if (this.packetSize && (this.packetSize < PING_PACKET_SIZE_MIN || this.packetSize > PING_PACKET_SIZE_MAX)) { diff --git a/server/monitor-types/pm2.js b/server/monitor-types/pm2.js new file mode 100644 index 000000000..c212174a8 --- /dev/null +++ b/server/monitor-types/pm2.js @@ -0,0 +1,36 @@ +const { MonitorType } = require("./monitor-type"); +const { UP } = require("../../src/util"); +const { getPM2ProcessList } = require("../util/pm2"); + +class PM2MonitorType extends MonitorType { + name = "pm2"; + description = "Checks if a PM2 process is online."; + + /** + * Check the PM2 process status. + * @param {object} monitor The monitor object containing monitor.system_service_name. + * @param {object} heartbeat The heartbeat object to update. + * @returns {Promise} + */ + async check(monitor, heartbeat) { + const processName = (monitor.system_service_name || "").trim(); + const processList = await getPM2ProcessList(); + const entry = processList.find((item) => item.name === processName || item.id === processName); + + if (!entry) { + throw new Error(`PM2 process '${processName}' was not found.`); + } + + if (entry.status === "online") { + heartbeat.status = UP; + heartbeat.msg = `PM2 process '${processName}' is online.`; + return; + } + + throw new Error(`PM2 process '${processName}' is ${entry.status}.`); + } +} + +module.exports = { + PM2MonitorType, +}; diff --git a/server/monitor-types/system-service.js b/server/monitor-types/system-service.js index 05c18cd80..0b9aff0ed 100644 --- a/server/monitor-types/system-service.js +++ b/server/monitor-types/system-service.js @@ -15,17 +15,19 @@ class SystemServiceMonitorType extends MonitorType { * @returns {Promise} Resolves when check is complete. */ async check(monitor, heartbeat) { - if (!monitor.system_service_name) { + const serviceName = (monitor.system_service_name || "").trim(); + + if (!serviceName) { throw new Error("Service Name is required."); } if (process.platform === "win32") { - return this.checkWindows(monitor.system_service_name, heartbeat); + return this.checkWindows(serviceName, heartbeat); } else if (process.platform === "linux") { - return this.checkLinux(monitor.system_service_name, heartbeat); - } else { - throw new Error(`System Service monitoring is not supported on ${process.platform}`); + return this.checkLinux(serviceName, heartbeat); } + + throw new Error(`System Service monitoring is not supported on ${process.platform}`); } /** @@ -80,7 +82,6 @@ class SystemServiceMonitorType extends MonitorType { "-NoProfile", "-NonInteractive", "-Command", - // Single quotes around the service name `(Get-Service -Name '${serviceName.replaceAll("'", "''")}').Status`, ]; diff --git a/server/socket-handlers/general-socket-handler.js b/server/socket-handlers/general-socket-handler.js index e879a4a06..fbb76e656 100644 --- a/server/socket-handlers/general-socket-handler.js +++ b/server/socket-handlers/general-socket-handler.js @@ -4,6 +4,7 @@ const { sendInfo } = require("../client"); const { checkLogin } = require("../util-server"); const { games } = require("gamedig"); const { testChrome } = require("../monitor-types/real-browser-monitor-type"); +const { getPM2ProcessList } = require("../util/pm2"); const fsAsync = require("fs").promises; const path = require("path"); @@ -68,6 +69,21 @@ module.exports.generalSocketHandler = (socket, server) => { } }); + socket.on("getPM2ProcessList", async (callback) => { + try { + checkLogin(socket); + callback({ + ok: true, + processList: await getPM2ProcessList(), + }); + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); + socket.on("testChrome", (executable, callback) => { try { checkLogin(socket); diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 2d2961161..ba932b19d 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -129,6 +129,7 @@ class UptimeKumaServer { UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType(); UptimeKumaServer.monitorTypeList["globalping"] = new GlobalpingMonitorType(this.getUserAgent()); UptimeKumaServer.monitorTypeList["redis"] = new RedisMonitorType(); + UptimeKumaServer.monitorTypeList["pm2"] = new PM2MonitorType(); UptimeKumaServer.monitorTypeList["system-service"] = new SystemServiceMonitorType(); UptimeKumaServer.monitorTypeList["sqlserver"] = new MssqlMonitorType(); UptimeKumaServer.monitorTypeList["mysql"] = new MysqlMonitorType(); @@ -582,6 +583,7 @@ const { TCPMonitorType } = require("./monitor-types/tcp.js"); const { ManualMonitorType } = require("./monitor-types/manual"); const { GlobalpingMonitorType } = require("./monitor-types/globalping"); const { RedisMonitorType } = require("./monitor-types/redis"); +const { PM2MonitorType } = require("./monitor-types/pm2"); const { SystemServiceMonitorType } = require("./monitor-types/system-service"); const { MssqlMonitorType } = require("./monitor-types/mssql"); const { MysqlMonitorType } = require("./monitor-types/mysql"); diff --git a/server/util/pm2.js b/server/util/pm2.js new file mode 100644 index 000000000..e3aebe298 --- /dev/null +++ b/server/util/pm2.js @@ -0,0 +1,74 @@ +const { execFile } = require("child_process"); +const process = require("process"); + +const PM2_EXEC_OPTIONS = { + timeout: 5000, + maxBuffer: 10 * 1024 * 1024, +}; + +/** + * Truncate command output to keep error messages compact. + * @param {string | Buffer} output Command output. + * @returns {string} The truncated output text. + */ +function truncateOutput(output) { + const text = (output || "").toString().trim(); + if (text.length > 200) { + return text.substring(0, 200) + "..."; + } + return text; +} + +/** + * Query PM2 for the current process list. + * @returns {Promise<{id: string, name: string, status: string}[]>} The normalized PM2 process list. + */ +function getPM2ProcessList() { + return new Promise((resolve, reject) => { + execFile( + process.platform === "win32" ? "pm2.cmd" : "pm2", + ["jlist"], + PM2_EXEC_OPTIONS, + (error, stdout, stderr) => { + if (error) { + const details = truncateOutput(stderr) || error.code || error.message; + reject(new Error(`Unable to query PM2 process list (${details}).`)); + return; + } + + try { + const parsed = JSON.parse((stdout || "").toString()); + if (!Array.isArray(parsed)) { + reject(new Error("Unexpected PM2 process list output.")); + return; + } + + resolve( + parsed + .map((item) => { + const id = item?.pm_id != null ? String(item.pm_id) : null; + const name = item?.name || null; + + if (!id && !name) { + return null; + } + + return { + id: id || name, + name: name || id, + status: item?.pm2_env?.status?.toString().toLowerCase() || "unknown", + }; + }) + .filter(Boolean) + ); + } catch (parseError) { + reject(new Error(truncateOutput(stderr) || "Unable to parse PM2 process list output.")); + } + } + ); + }); +} + +module.exports = { + getPM2ProcessList, +}; diff --git a/src/lang/en.json b/src/lang/en.json index 6c05e9274..da681f222 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1181,6 +1181,8 @@ "systemServiceDescriptionWindows": "Checks if Windows Service Manager {service_name} is running", "systemServiceCommandHint": "Command used: {command}", "systemServiceExpectedOutput": "Expected Output: \"{0}\"", + "pm2Description": "Checks if PM2 process {service_name} is online", + "pm2ExpectedStates": "Expected state: online. States stopped and errored are treated as DOWN.", "Browser Screenshot": "Browser Screenshot", "Command": "Command", "mongodbCommandDescription": "Run a MongoDB command against the database. For information about the available commands check out the {documentation}", @@ -1513,7 +1515,9 @@ "GrafanaOncallURL": "Grafana Oncall URL", "Never": "Never", "Json Query": "Json Query", - "System Service": "System Service", + "PM2 Process": "PM2 Process", + "Refresh": "Refresh", + "Select PM2 process": "Select PM2 process", "SSL/TLS": "SSL/TLS", "playground": "playground", "Check Type": "Check Type", diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 92db0d9ac..0ab97c9e3 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -52,7 +52,10 @@ " value="system-service" > - {{ $t("System Service") }} + {{ $t("systemService") }} + +