mirror of
https://github.com/louislam/uptime-kuma.git
synced 2026-08-07 10:14:59 +00:00
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") {
|
if (this.type === "ping") {
|
||||||
// ping parameters validation
|
// ping parameters validation
|
||||||
if (this.packetSize && (this.packetSize < PING_PACKET_SIZE_MIN || this.packetSize > PING_PACKET_SIZE_MAX)) {
|
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.
|
* @returns {Promise<void>} Resolves when check is complete.
|
||||||
*/
|
*/
|
||||||
async check(monitor, heartbeat) {
|
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.");
|
throw new Error("Service Name is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
return this.checkWindows(monitor.system_service_name, heartbeat);
|
return this.checkWindows(serviceName, heartbeat);
|
||||||
} else if (process.platform === "linux") {
|
} else if (process.platform === "linux") {
|
||||||
return this.checkLinux(monitor.system_service_name, heartbeat);
|
return this.checkLinux(serviceName, heartbeat);
|
||||||
} else {
|
|
||||||
throw new Error(`System Service monitoring is not supported on ${process.platform}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error(`System Service monitoring is not supported on ${process.platform}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,7 +82,6 @@ class SystemServiceMonitorType extends MonitorType {
|
|||||||
"-NoProfile",
|
"-NoProfile",
|
||||||
"-NonInteractive",
|
"-NonInteractive",
|
||||||
"-Command",
|
"-Command",
|
||||||
// Single quotes around the service name
|
|
||||||
`(Get-Service -Name '${serviceName.replaceAll("'", "''")}').Status`,
|
`(Get-Service -Name '${serviceName.replaceAll("'", "''")}').Status`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const { sendInfo } = require("../client");
|
|||||||
const { checkLogin } = require("../util-server");
|
const { checkLogin } = require("../util-server");
|
||||||
const { games } = require("gamedig");
|
const { games } = require("gamedig");
|
||||||
const { testChrome } = require("../monitor-types/real-browser-monitor-type");
|
const { testChrome } = require("../monitor-types/real-browser-monitor-type");
|
||||||
|
const { getPM2ProcessList } = require("../util/pm2");
|
||||||
const fsAsync = require("fs").promises;
|
const fsAsync = require("fs").promises;
|
||||||
const path = require("path");
|
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) => {
|
socket.on("testChrome", (executable, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ class UptimeKumaServer {
|
|||||||
UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType();
|
UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["globalping"] = new GlobalpingMonitorType(this.getUserAgent());
|
UptimeKumaServer.monitorTypeList["globalping"] = new GlobalpingMonitorType(this.getUserAgent());
|
||||||
UptimeKumaServer.monitorTypeList["redis"] = new RedisMonitorType();
|
UptimeKumaServer.monitorTypeList["redis"] = new RedisMonitorType();
|
||||||
|
UptimeKumaServer.monitorTypeList["pm2"] = new PM2MonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["system-service"] = new SystemServiceMonitorType();
|
UptimeKumaServer.monitorTypeList["system-service"] = new SystemServiceMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["sqlserver"] = new MssqlMonitorType();
|
UptimeKumaServer.monitorTypeList["sqlserver"] = new MssqlMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["mysql"] = new MysqlMonitorType();
|
UptimeKumaServer.monitorTypeList["mysql"] = new MysqlMonitorType();
|
||||||
@@ -582,6 +583,7 @@ const { TCPMonitorType } = require("./monitor-types/tcp.js");
|
|||||||
const { ManualMonitorType } = require("./monitor-types/manual");
|
const { ManualMonitorType } = require("./monitor-types/manual");
|
||||||
const { GlobalpingMonitorType } = require("./monitor-types/globalping");
|
const { GlobalpingMonitorType } = require("./monitor-types/globalping");
|
||||||
const { RedisMonitorType } = require("./monitor-types/redis");
|
const { RedisMonitorType } = require("./monitor-types/redis");
|
||||||
|
const { PM2MonitorType } = require("./monitor-types/pm2");
|
||||||
const { SystemServiceMonitorType } = require("./monitor-types/system-service");
|
const { SystemServiceMonitorType } = require("./monitor-types/system-service");
|
||||||
const { MssqlMonitorType } = require("./monitor-types/mssql");
|
const { MssqlMonitorType } = require("./monitor-types/mssql");
|
||||||
const { MysqlMonitorType } = require("./monitor-types/mysql");
|
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,
|
||||||
|
};
|
||||||
+5
-1
@@ -1181,6 +1181,8 @@
|
|||||||
"systemServiceDescriptionWindows": "Checks if Windows Service Manager {service_name} is running",
|
"systemServiceDescriptionWindows": "Checks if Windows Service Manager {service_name} is running",
|
||||||
"systemServiceCommandHint": "Command used: {command}",
|
"systemServiceCommandHint": "Command used: {command}",
|
||||||
"systemServiceExpectedOutput": "Expected Output: \"{0}\"",
|
"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",
|
"Browser Screenshot": "Browser Screenshot",
|
||||||
"Command": "Command",
|
"Command": "Command",
|
||||||
"mongodbCommandDescription": "Run a MongoDB command against the database. For information about the available commands check out the {documentation}",
|
"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",
|
"GrafanaOncallURL": "Grafana Oncall URL",
|
||||||
"Never": "Never",
|
"Never": "Never",
|
||||||
"Json Query": "Json Query",
|
"Json Query": "Json Query",
|
||||||
"System Service": "System Service",
|
"PM2 Process": "PM2 Process",
|
||||||
|
"Refresh": "Refresh",
|
||||||
|
"Select PM2 process": "Select PM2 process",
|
||||||
"SSL/TLS": "SSL/TLS",
|
"SSL/TLS": "SSL/TLS",
|
||||||
"playground": "playground",
|
"playground": "playground",
|
||||||
"Check Type": "Check Type",
|
"Check Type": "Check Type",
|
||||||
|
|||||||
+121
-3
@@ -52,7 +52,10 @@
|
|||||||
"
|
"
|
||||||
value="system-service"
|
value="system-service"
|
||||||
>
|
>
|
||||||
{{ $t("System Service") }}
|
{{ $t("systemService") }}
|
||||||
|
</option>
|
||||||
|
<option value="pm2">
|
||||||
|
{{ $t("PM2 Process") }}
|
||||||
</option>
|
</option>
|
||||||
<option value="real-browser">
|
<option value="real-browser">
|
||||||
HTTP(s) - Browser Engine (Chrome/Chromium) (Beta)
|
HTTP(s) - Browser Engine (Chrome/Chromium) (Beta)
|
||||||
@@ -1242,14 +1245,16 @@
|
|||||||
|
|
||||||
<template v-if="monitor.type === 'system-service'">
|
<template v-if="monitor.type === 'system-service'">
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="system-service-name" class="form-label">{{ $t("Service Name") }}</label>
|
<label for="system-service-name" class="form-label">
|
||||||
|
{{ $t("systemServiceName") }}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
id="system-service-name"
|
id="system-service-name"
|
||||||
v-model="monitor.system_service_name"
|
v-model="monitor.system_service_name"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
required
|
required
|
||||||
placeholder="nginx"
|
:placeholder="$root.info.runtime.platform === 'win32' ? 'Dnscache' : 'nginx'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
@@ -1319,6 +1324,71 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="monitor.type === 'pm2'">
|
||||||
|
<div class="my-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<label for="pm2-process-name" class="form-label mb-0">
|
||||||
|
{{ $t("PM2 Process") }}
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary btn-sm"
|
||||||
|
type="button"
|
||||||
|
:disabled="pm2ProcessLoading"
|
||||||
|
@click="loadPM2ProcessList"
|
||||||
|
>
|
||||||
|
{{ pm2ProcessLoading ? $t("Loading...") : $t("Refresh") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
v-if="pm2ProcessOptions.length > 0"
|
||||||
|
id="pm2-process-name"
|
||||||
|
v-model="monitor.system_service_name"
|
||||||
|
class="form-select mt-2"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option disabled value="">{{ $t("Select PM2 process") }}</option>
|
||||||
|
<!-- Save the PM2 name because PM2 can reassign numeric IDs after delete/recreate. -->
|
||||||
|
<option
|
||||||
|
v-for="item in pm2ProcessOptions"
|
||||||
|
:key="`${item.name}-${item.id}`"
|
||||||
|
:value="item.name"
|
||||||
|
>
|
||||||
|
{{ item.name }} (#{{ item.id }}) - {{ item.status }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
id="pm2-process-name"
|
||||||
|
v-model="monitor.system_service_name"
|
||||||
|
type="text"
|
||||||
|
class="form-control mt-2"
|
||||||
|
placeholder="api"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div v-if="pm2ProcessError" class="text-danger small mt-2">
|
||||||
|
{{ pm2ProcessError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-text">
|
||||||
|
{{
|
||||||
|
$t("pm2Description", {
|
||||||
|
service_name: monitor.system_service_name || "api",
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
<div class="mt-2">
|
||||||
|
<i18n-t keypath="systemServiceCommandHint" tag="span">
|
||||||
|
<template #command>
|
||||||
|
<code>pm2 jlist</code>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
<div class="text-secondary small">
|
||||||
|
{{ $t("pm2ExpectedStates") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-if="monitor.type === 'mysql'">
|
<template v-if="monitor.type === 'mysql'">
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="mysql-password" class="form-label">{{ $t("Password") }}</label>
|
<label for="mysql-password" class="form-label">{{ $t("Password") }}</label>
|
||||||
@@ -3246,6 +3316,9 @@ export default {
|
|||||||
confirmed: false,
|
confirmed: false,
|
||||||
editedValue: false,
|
editedValue: false,
|
||||||
},
|
},
|
||||||
|
pm2ProcessOptions: [],
|
||||||
|
pm2ProcessLoading: false,
|
||||||
|
pm2ProcessError: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -3596,6 +3669,10 @@ message HealthCheckResponse {
|
|||||||
"monitor.type"(newType, oldType) {
|
"monitor.type"(newType, oldType) {
|
||||||
this.checkDomain();
|
this.checkDomain();
|
||||||
|
|
||||||
|
if (newType === "pm2") {
|
||||||
|
this.loadPM2ProcessList();
|
||||||
|
}
|
||||||
|
|
||||||
if (newType === "globalping" && !this.monitor.subtype) {
|
if (newType === "globalping" && !this.monitor.subtype) {
|
||||||
this.monitor.subtype = "ping";
|
this.monitor.subtype = "ping";
|
||||||
}
|
}
|
||||||
@@ -3828,6 +3905,39 @@ message HealthCheckResponse {
|
|||||||
this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions;
|
this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
loadPM2ProcessList() {
|
||||||
|
this.pm2ProcessLoading = true;
|
||||||
|
this.pm2ProcessError = "";
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("getPM2ProcessList", (res) => {
|
||||||
|
this.pm2ProcessLoading = false;
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
this.pm2ProcessOptions = [];
|
||||||
|
this.pm2ProcessError = res.msg || "Unable to query PM2 process list.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pm2ProcessOptions = (res.processList || []).map((item) => {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
status: item.status,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedProcess = this.pm2ProcessOptions.find(
|
||||||
|
(item) =>
|
||||||
|
item.id === this.monitor.system_service_name || item.name === this.monitor.system_service_name
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert a legacy numeric id to the stable process name without replacing a missing saved target.
|
||||||
|
if (selectedProcess) {
|
||||||
|
this.monitor.system_service_name = selectedProcess.name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the edit monitor form
|
* Initialize the edit monitor form
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
@@ -3918,6 +4028,10 @@ message HealthCheckResponse {
|
|||||||
this.monitor.timeout = ~~(this.monitor.interval * 8) / 10;
|
this.monitor.timeout = ~~(this.monitor.interval * 8) / 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.monitor.type === "pm2") {
|
||||||
|
this.loadPM2ProcessList();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.$root.toastError(res.msg);
|
this.$root.toastError(res.msg);
|
||||||
}
|
}
|
||||||
@@ -4137,6 +4251,10 @@ message HealthCheckResponse {
|
|||||||
this.monitor.url = this.monitor.url.trim();
|
this.monitor.url = this.monitor.url.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (["system-service", "pm2"].includes(this.monitor.type) && this.monitor.system_service_name) {
|
||||||
|
this.monitor.system_service_name = this.monitor.system_service_name.trim();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.monitor.databaseConnectionString) {
|
if (this.monitor.databaseConnectionString) {
|
||||||
this.monitor.databaseConnectionString = this.monitor.databaseConnectionString.trim();
|
this.monitor.databaseConnectionString = this.monitor.databaseConnectionString.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
const { describe, test, beforeEach, afterEach } = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const childProcess = require("child_process");
|
||||||
|
const { DOWN, UP } = require("../../src/util");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the PM2 monitor after swapping child_process.execFile.
|
||||||
|
* @param {typeof childProcess.execFile} execFileStub Stub implementation.
|
||||||
|
* @returns {import("../../server/monitor-types/pm2").PM2MonitorType} A PM2 monitor instance wired to the stub.
|
||||||
|
*/
|
||||||
|
function createMonitorType(execFileStub) {
|
||||||
|
childProcess.execFile = execFileStub;
|
||||||
|
delete require.cache[require.resolve("../../server/util/pm2")];
|
||||||
|
delete require.cache[require.resolve("../../server/monitor-types/pm2")];
|
||||||
|
const { PM2MonitorType } = require("../../server/monitor-types/pm2");
|
||||||
|
return new PM2MonitorType();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("PM2MonitorType", () => {
|
||||||
|
let monitorType;
|
||||||
|
let heartbeat;
|
||||||
|
let originalExecFile;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
monitorType = null;
|
||||||
|
heartbeat = {
|
||||||
|
status: DOWN,
|
||||||
|
msg: "",
|
||||||
|
};
|
||||||
|
originalExecFile = childProcess.execFile;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
childProcess.execFile = originalExecFile;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("check() returns UP for an online PM2 process", async () => {
|
||||||
|
monitorType = createMonitorType((command, args, options, callback) => {
|
||||||
|
callback(
|
||||||
|
null,
|
||||||
|
JSON.stringify([
|
||||||
|
{
|
||||||
|
pm_id: 0,
|
||||||
|
name: "api",
|
||||||
|
pm2_env: {
|
||||||
|
status: "online",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
""
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await monitorType.check(
|
||||||
|
{
|
||||||
|
system_service_name: "api",
|
||||||
|
},
|
||||||
|
heartbeat
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(heartbeat.status, UP);
|
||||||
|
assert.ok(heartbeat.msg.includes("online"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("check() matches PM2 process by name when pm_id changes", async () => {
|
||||||
|
monitorType = createMonitorType((command, args, options, callback) => {
|
||||||
|
callback(
|
||||||
|
null,
|
||||||
|
JSON.stringify([
|
||||||
|
{
|
||||||
|
// PM2 may assign a different id when a process is deleted and recreated.
|
||||||
|
pm_id: 7,
|
||||||
|
name: "api",
|
||||||
|
pm2_env: {
|
||||||
|
status: "online",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
""
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await monitorType.check(
|
||||||
|
{
|
||||||
|
system_service_name: "api",
|
||||||
|
},
|
||||||
|
heartbeat
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(heartbeat.status, UP);
|
||||||
|
assert.ok(heartbeat.msg.includes("online"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("check() returns DOWN for a stopped PM2 process", async () => {
|
||||||
|
monitorType = createMonitorType((command, args, options, callback) => {
|
||||||
|
callback(
|
||||||
|
null,
|
||||||
|
JSON.stringify([
|
||||||
|
{
|
||||||
|
pm_id: 0,
|
||||||
|
name: "api",
|
||||||
|
pm2_env: {
|
||||||
|
status: "stopped",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
""
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
monitorType.check(
|
||||||
|
{
|
||||||
|
system_service_name: "api",
|
||||||
|
},
|
||||||
|
heartbeat
|
||||||
|
),
|
||||||
|
/stopped/
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(heartbeat.status, DOWN);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user