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 <frank@elsinga.de>
This commit is contained in:
KraoESPfan1n
2026-07-13 17:13:11 -04:00
committed by GitHub
parent c673d17102
commit 590f90e3f9
9 changed files with 400 additions and 10 deletions
+16
View File
@@ -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)) {
+36
View File
@@ -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<void>}
*/
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,
};
+7 -6
View File
@@ -15,17 +15,19 @@ class SystemServiceMonitorType extends MonitorType {
* @returns {Promise<void>} 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`,
];
@@ -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);
+2
View File
@@ -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");
+74
View File
@@ -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,
};