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:
@@ -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)) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user