mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-14 15:36:59 +08:00
Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
37666bf35f | ||
|
90badfabee | ||
|
e3396251a8 | ||
|
9c9a086788 | ||
|
9fb95fe95e | ||
|
1e75d81bcf | ||
|
cb3a104dc0 | ||
|
57a18958d6 | ||
|
1708b67949 | ||
|
73239d441d | ||
|
4ceeb304f1 | ||
|
711380bbbe | ||
|
9536c6aa6a | ||
|
4255496b11 | ||
|
f28dccf4e1 | ||
|
b689733d59 | ||
|
afaa7bb2f0 | ||
|
121d1a11af | ||
|
8e61158758 | ||
|
bf58838b89 | ||
|
33ce0ef02c |
@@ -27,7 +27,7 @@ RUN apt-get update && \
|
|||||||
ca-certificates \
|
ca-certificates \
|
||||||
sudo \
|
sudo \
|
||||||
nscd && \
|
nscd && \
|
||||||
pip3 --no-cache-dir install apprise==1.4.5 && \
|
pip3 --no-cache-dir install apprise==1.6.0 && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
apt --yes autoremove
|
apt --yes autoremove
|
||||||
|
|
||||||
|
44
extra/reformat-changelog.js
Normal file
44
extra/reformat-changelog.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Generate on GitHub
|
||||||
|
const input = `
|
||||||
|
* Add Korean translation by @Alanimdeo in https://github.com/louislam/dockge/pull/86
|
||||||
|
`;
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
### 🆕 New Features
|
||||||
|
|
||||||
|
### 💇♀️ Improvements
|
||||||
|
|
||||||
|
### 🐞 Bug Fixes
|
||||||
|
|
||||||
|
### ⬆️ Security Fixes
|
||||||
|
|
||||||
|
### 🦎 Translation Contributions
|
||||||
|
|
||||||
|
### Others
|
||||||
|
- Other small changes, code refactoring and comment/doc updates in this repo:
|
||||||
|
`;
|
||||||
|
|
||||||
|
const lines = input.split("\n").filter((line) => line.trim() !== "");
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Split the last " by "
|
||||||
|
const usernamePullRequesURL = line.split(" by ").pop();
|
||||||
|
|
||||||
|
if (!usernamePullRequesURL) {
|
||||||
|
console.log("Unable to parse", line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ username, pullRequestURL ] = usernamePullRequesURL.split(" in ");
|
||||||
|
const pullRequestID = "#" + pullRequestURL.split("/").pop();
|
||||||
|
let message = line.split(" by ").shift();
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
console.log("Unable to parse", line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
message = message.split("* ").pop();
|
||||||
|
console.log("-", pullRequestID, message, `(Thanks ${username})`);
|
||||||
|
}
|
||||||
|
console.log(template);
|
1499
package-lock.json
generated
1499
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uptime-kuma",
|
"name": "uptime-kuma",
|
||||||
"version": "1.23.6",
|
"version": "1.23.8",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
"build-docker-nightly-amd64": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:nightly-amd64 --target nightly . --push --progress plain",
|
||||||
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
|
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test --target pr-test . --push",
|
||||||
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
"upload-artifacts": "docker buildx build -f docker/dockerfile --platform linux/amd64 -t louislam/uptime-kuma:upload-artifact --build-arg VERSION --build-arg GITHUB_TOKEN --target upload-artifact . --progress plain",
|
||||||
"setup": "git checkout 1.23.6 && npm ci --production && npm run download-dist",
|
"setup": "git checkout 1.23.8 && npm ci --production && npm run download-dist",
|
||||||
"download-dist": "node extra/download-dist.js",
|
"download-dist": "node extra/download-dist.js",
|
||||||
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
"mark-as-nightly": "node extra/mark-as-nightly.js",
|
||||||
"reset-password": "node extra/reset-password.js",
|
"reset-password": "node extra/reset-password.js",
|
||||||
@@ -99,7 +99,8 @@
|
|||||||
"express-basic-auth": "~1.2.1",
|
"express-basic-auth": "~1.2.1",
|
||||||
"express-static-gzip": "~2.1.7",
|
"express-static-gzip": "~2.1.7",
|
||||||
"form-data": "~4.0.0",
|
"form-data": "~4.0.0",
|
||||||
"gamedig": "~4.1.0",
|
"gamedig": "^4.2.0",
|
||||||
|
"html-escaper": "^3.0.3",
|
||||||
"http-graceful-shutdown": "~3.1.7",
|
"http-graceful-shutdown": "~3.1.7",
|
||||||
"http-proxy-agent": "~5.0.0",
|
"http-proxy-agent": "~5.0.0",
|
||||||
"https-proxy-agent": "~5.0.1",
|
"https-proxy-agent": "~5.0.1",
|
||||||
@@ -129,6 +130,7 @@
|
|||||||
"playwright-core": "~1.35.1",
|
"playwright-core": "~1.35.1",
|
||||||
"prom-client": "~13.2.0",
|
"prom-client": "~13.2.0",
|
||||||
"prometheus-api-metrics": "~3.2.1",
|
"prometheus-api-metrics": "~3.2.1",
|
||||||
|
"promisify-child-process": "~4.1.2",
|
||||||
"protobufjs": "~7.2.4",
|
"protobufjs": "~7.2.4",
|
||||||
"qs": "~6.10.4",
|
"qs": "~6.10.4",
|
||||||
"redbean-node": "~0.3.0",
|
"redbean-node": "~0.3.0",
|
||||||
@@ -202,7 +204,7 @@
|
|||||||
"vue-router": "~4.0.14",
|
"vue-router": "~4.0.14",
|
||||||
"vue-toastification": "~2.0.0-rc.5",
|
"vue-toastification": "~2.0.0-rc.5",
|
||||||
"vuedraggable": "~4.1.0",
|
"vuedraggable": "~4.1.0",
|
||||||
"wait-on": "^6.0.1",
|
"wait-on": "^7.2.0",
|
||||||
"whatwg-url": "~12.0.1"
|
"whatwg-url": "~12.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
const jsesc = require("jsesc");
|
const jsesc = require("jsesc");
|
||||||
|
const { escape } = require("html-escaper");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a string that represents the javascript that is required to insert the Google Analytics scripts
|
* Returns a string that represents the javascript that is required to insert the Google Analytics scripts
|
||||||
@@ -7,15 +8,18 @@ const jsesc = require("jsesc");
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function getGoogleAnalyticsScript(tagId) {
|
function getGoogleAnalyticsScript(tagId) {
|
||||||
let escapedTagId = jsesc(tagId, { isScriptContext: true });
|
let escapedTagIdJS = jsesc(tagId, { isScriptContext: true });
|
||||||
|
|
||||||
if (escapedTagId) {
|
if (escapedTagIdJS) {
|
||||||
escapedTagId = escapedTagId.trim();
|
escapedTagIdJS = escapedTagIdJS.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escape the tag ID for use in an HTML attribute.
|
||||||
|
let escapedTagIdHTMLAttribute = escape(tagId);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagId}"></script>
|
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedTagIdHTMLAttribute}"></script>
|
||||||
<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagId}'); </script>
|
<script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', '${escapedTagIdJS}'); </script>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@ const dayjs = require("dayjs");
|
|||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
||||||
SQL_DATETIME_FORMAT, isDev, sleep, getRandomInt
|
SQL_DATETIME_FORMAT
|
||||||
} = require("../../src/util");
|
} = require("../../src/util");
|
||||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
||||||
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
||||||
@@ -329,16 +329,6 @@ class Monitor extends BeanModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evil
|
|
||||||
if (isDev) {
|
|
||||||
if (process.env.EVIL_RANDOM_MONITOR_SLEEP === "SURE") {
|
|
||||||
if (getRandomInt(0, 100) === 0) {
|
|
||||||
log.debug("evil", `[${this.name}] Evil mode: Random sleep: ` + beatInterval * 10000);
|
|
||||||
await sleep(beatInterval * 10000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expose here for prometheus update
|
// Expose here for prometheus update
|
||||||
// undefined if not https
|
// undefined if not https
|
||||||
let tlsInfo = undefined;
|
let tlsInfo = undefined;
|
||||||
@@ -483,7 +473,7 @@ class Monitor extends BeanModel {
|
|||||||
validateStatus: (status) => {
|
validateStatus: (status) => {
|
||||||
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
return checkStatusCode(status, this.getAcceptedStatuscodes());
|
||||||
},
|
},
|
||||||
signal: axiosAbortSignal(this.timeout * 1000),
|
signal: axiosAbortSignal((this.timeout + 10) * 1000),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (bodyValue) {
|
if (bodyValue) {
|
||||||
@@ -938,7 +928,11 @@ class Monitor extends BeanModel {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
bean.msg = error.message;
|
if (error?.name === "CanceledError") {
|
||||||
|
bean.msg = `timeout by AbortSignal (${this.timeout}s)`;
|
||||||
|
} else {
|
||||||
|
bean.msg = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
// If UP come in here, it must be upside down mode
|
// If UP come in here, it must be upside down mode
|
||||||
// Just reset the retries
|
// Just reset the retries
|
||||||
@@ -1020,7 +1014,6 @@ class Monitor extends BeanModel {
|
|||||||
if (! this.isStop) {
|
if (! this.isStop) {
|
||||||
log.debug("monitor", `[${this.name}] SetTimeout for next check.`);
|
log.debug("monitor", `[${this.name}] SetTimeout for next check.`);
|
||||||
this.heartbeatInterval = setTimeout(safeBeat, beatInterval * 1000);
|
this.heartbeatInterval = setTimeout(safeBeat, beatInterval * 1000);
|
||||||
this.lastScheduleBeatTime = dayjs();
|
|
||||||
} else {
|
} else {
|
||||||
log.info("monitor", `[${this.name}] isStop = true, no next check.`);
|
log.info("monitor", `[${this.name}] isStop = true, no next check.`);
|
||||||
}
|
}
|
||||||
@@ -1030,9 +1023,7 @@ class Monitor extends BeanModel {
|
|||||||
/** Get a heartbeat and handle errors */
|
/** Get a heartbeat and handle errors */
|
||||||
const safeBeat = async () => {
|
const safeBeat = async () => {
|
||||||
try {
|
try {
|
||||||
this.lastStartBeatTime = dayjs();
|
|
||||||
await beat();
|
await beat();
|
||||||
this.lastEndBeatTime = dayjs();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.trace(e);
|
console.trace(e);
|
||||||
UptimeKumaServer.errorLog(e, false);
|
UptimeKumaServer.errorLog(e, false);
|
||||||
@@ -1041,9 +1032,6 @@ class Monitor extends BeanModel {
|
|||||||
if (! this.isStop) {
|
if (! this.isStop) {
|
||||||
log.info("monitor", "Try to restart the monitor");
|
log.info("monitor", "Try to restart the monitor");
|
||||||
this.heartbeatInterval = setTimeout(safeBeat, this.interval * 1000);
|
this.heartbeatInterval = setTimeout(safeBeat, this.interval * 1000);
|
||||||
this.lastScheduleBeatTime = dayjs();
|
|
||||||
} else {
|
|
||||||
log.info("monitor", "isStop = true, no next check.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -40,6 +40,7 @@ if (process.platform === "win32") {
|
|||||||
"/usr/bin/chromium",
|
"/usr/bin/chromium",
|
||||||
"/usr/bin/chromium-browser",
|
"/usr/bin/chromium-browser",
|
||||||
"/usr/bin/google-chrome",
|
"/usr/bin/google-chrome",
|
||||||
|
"/snap/bin/chromium", // Ubuntu
|
||||||
];
|
];
|
||||||
} else if (process.platform === "darwin") {
|
} else if (process.platform === "darwin") {
|
||||||
// TODO: Generated by GitHub Copilot, but not sure if it's correct
|
// TODO: Generated by GitHub Copilot, but not sure if it's correct
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
const { MonitorType } = require("./monitor-type");
|
const { MonitorType } = require("./monitor-type");
|
||||||
const { UP, log } = require("../../src/util");
|
const { UP } = require("../../src/util");
|
||||||
const exec = require("child_process").exec;
|
const childProcessAsync = require("promisify-child-process");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A TailscalePing class extends the MonitorType.
|
* A TailscalePing class extends the MonitorType.
|
||||||
@@ -23,7 +23,6 @@ class TailscalePing extends MonitorType {
|
|||||||
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
|
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
|
||||||
this.parseTailscaleOutput(tailscaleOutput, heartbeat);
|
this.parseTailscaleOutput(tailscaleOutput, heartbeat);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.debug("Tailscale", err);
|
|
||||||
// trigger log function somewhere to display a notification or alert to the user (but how?)
|
// trigger log function somewhere to display a notification or alert to the user (but how?)
|
||||||
throw new Error(`Error checking Tailscale ping: ${err}`);
|
throw new Error(`Error checking Tailscale ping: ${err}`);
|
||||||
}
|
}
|
||||||
@@ -33,30 +32,23 @@ class TailscalePing extends MonitorType {
|
|||||||
* Runs the Tailscale ping command to the given URL.
|
* Runs the Tailscale ping command to the given URL.
|
||||||
*
|
*
|
||||||
* @param {string} hostname - The hostname to ping.
|
* @param {string} hostname - The hostname to ping.
|
||||||
|
* @param {number} interval
|
||||||
* @returns {Promise<string>} - A Promise that resolves to the output of the Tailscale ping command
|
* @returns {Promise<string>} - A Promise that resolves to the output of the Tailscale ping command
|
||||||
* @throws Will throw an error if the command execution encounters any error.
|
* @throws Will throw an error if the command execution encounters any error.
|
||||||
*/
|
*/
|
||||||
async runTailscalePing(hostname, interval) {
|
async runTailscalePing(hostname, interval) {
|
||||||
let cmd = `tailscale ping ${hostname}`;
|
let timeout = interval * 1000 * 0.8;
|
||||||
|
let res = await childProcessAsync.spawn("tailscale", [ "ping", "--c", "1", hostname ], {
|
||||||
log.debug("Tailscale", cmd);
|
timeout: timeout
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let timeout = interval * 1000 * 0.8;
|
|
||||||
exec(cmd, { timeout: timeout }, (error, stdout, stderr) => {
|
|
||||||
// we may need to handle more cases if tailscale reports an error that isn't necessarily an error (such as not-logged in or DERP health-related issues)
|
|
||||||
if (error) {
|
|
||||||
reject(`Execution error: ${error.message}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (stderr) {
|
|
||||||
reject(`Error in output: ${stderr}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(stdout);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
if (res.stderr && res.stderr.toString()) {
|
||||||
|
throw new Error(`Error in output: ${res.stderr.toString()}`);
|
||||||
|
}
|
||||||
|
if (res.stdout && res.stdout.toString()) {
|
||||||
|
return res.stdout.toString();
|
||||||
|
} else {
|
||||||
|
throw new Error("No output from Tailscale ping");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,7 +66,7 @@ class TailscalePing extends MonitorType {
|
|||||||
heartbeat.status = UP;
|
heartbeat.status = UP;
|
||||||
let time = line.split(" in ")[1].split(" ")[0];
|
let time = line.split(" in ")[1].split(" ")[0];
|
||||||
heartbeat.ping = parseInt(time);
|
heartbeat.ping = parseInt(time);
|
||||||
heartbeat.msg = line;
|
heartbeat.msg = "OK";
|
||||||
break;
|
break;
|
||||||
} else if (line.includes("timed out")) {
|
} else if (line.includes("timed out")) {
|
||||||
throw new Error(`Ping timed out: "${line}"`);
|
throw new Error(`Ping timed out: "${line}"`);
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
const NotificationProvider = require("./notification-provider");
|
const NotificationProvider = require("./notification-provider");
|
||||||
const childProcess = require("child_process");
|
const childProcessAsync = require("promisify-child-process");
|
||||||
|
|
||||||
class Apprise extends NotificationProvider {
|
class Apprise extends NotificationProvider {
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ class Apprise extends NotificationProvider {
|
|||||||
args.push("-t");
|
args.push("-t");
|
||||||
args.push(notification.title);
|
args.push(notification.title);
|
||||||
}
|
}
|
||||||
const s = childProcess.spawnSync("apprise", args);
|
const s = await childProcessAsync.spawn("apprise", args);
|
||||||
|
|
||||||
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||||
|
|
||||||
|
@@ -1223,9 +1223,9 @@ let needSetup = false;
|
|||||||
// Update nscd status
|
// Update nscd status
|
||||||
if (previousNSCDStatus !== data.nscd) {
|
if (previousNSCDStatus !== data.nscd) {
|
||||||
if (data.nscd) {
|
if (data.nscd) {
|
||||||
server.startNSCDServices();
|
await server.startNSCDServices();
|
||||||
} else {
|
} else {
|
||||||
server.stopNSCDServices();
|
await server.stopNSCDServices();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -42,24 +42,40 @@ module.exports.generalSocketHandler = (socket, server) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("getGameList", async (callback) => {
|
socket.on("getGameList", async (callback) => {
|
||||||
callback({
|
try {
|
||||||
ok: true,
|
checkLogin(socket);
|
||||||
gameList: getGameList(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("testChrome", (executable, callback) => {
|
|
||||||
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
|
|
||||||
testChrome(executable).then((version) => {
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
msg: "Found Chromium/Chrome. Version: " + version,
|
gameList: getGameList(),
|
||||||
});
|
});
|
||||||
}).catch((e) => {
|
} catch (e) {
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: e.message,
|
msg: e.message,
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("testChrome", (executable, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
|
||||||
|
testChrome(executable).then((version) => {
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "Found Chromium/Chrome. Version: " + version,
|
||||||
|
});
|
||||||
|
}).catch((e) => {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -10,9 +10,8 @@ const util = require("util");
|
|||||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||||
const { Settings } = require("./settings");
|
const { Settings } = require("./settings");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const childProcess = require("child_process");
|
const childProcessAsync = require("promisify-child-process");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const axios = require("axios");
|
|
||||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
|
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,8 +62,6 @@ class UptimeKumaServer {
|
|||||||
*/
|
*/
|
||||||
jwtSecret = null;
|
jwtSecret = null;
|
||||||
|
|
||||||
checkMonitorsInterval = null;
|
|
||||||
|
|
||||||
static getInstance(args) {
|
static getInstance(args) {
|
||||||
if (UptimeKumaServer.instance == null) {
|
if (UptimeKumaServer.instance == null) {
|
||||||
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
||||||
@@ -78,9 +75,6 @@ class UptimeKumaServer {
|
|||||||
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
|
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
|
||||||
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
|
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
|
||||||
|
|
||||||
// Set default axios timeout to 5 minutes instead of infinity
|
|
||||||
axios.defaults.timeout = 300 * 1000;
|
|
||||||
|
|
||||||
log.info("server", "Creating express and socket.io instance");
|
log.info("server", "Creating express and socket.io instance");
|
||||||
this.app = express();
|
this.app = express();
|
||||||
if (sslKey && sslCert) {
|
if (sslKey && sslCert) {
|
||||||
@@ -350,12 +344,8 @@ class UptimeKumaServer {
|
|||||||
let enable = await Settings.get("nscd");
|
let enable = await Settings.get("nscd");
|
||||||
|
|
||||||
if (enable || enable === null) {
|
if (enable || enable === null) {
|
||||||
this.startNSCDServices();
|
await this.startNSCDServices();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkMonitorsInterval = setInterval(() => {
|
|
||||||
this.checkMonitors();
|
|
||||||
}, 60 * 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -366,21 +356,19 @@ class UptimeKumaServer {
|
|||||||
let enable = await Settings.get("nscd");
|
let enable = await Settings.get("nscd");
|
||||||
|
|
||||||
if (enable || enable === null) {
|
if (enable || enable === null) {
|
||||||
this.stopNSCDServices();
|
await this.stopNSCDServices();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearInterval(this.checkMonitorsInterval);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start all system services (e.g. nscd)
|
* Start all system services (e.g. nscd)
|
||||||
* For now, only used in Docker
|
* For now, only used in Docker
|
||||||
*/
|
*/
|
||||||
startNSCDServices() {
|
async startNSCDServices() {
|
||||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||||
try {
|
try {
|
||||||
log.info("services", "Starting nscd");
|
log.info("services", "Starting nscd");
|
||||||
childProcess.execSync("sudo service nscd start", { stdio: "pipe" });
|
await childProcessAsync.exec("sudo service nscd start");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.info("services", "Failed to start nscd");
|
log.info("services", "Failed to start nscd");
|
||||||
}
|
}
|
||||||
@@ -390,93 +378,16 @@ class UptimeKumaServer {
|
|||||||
/**
|
/**
|
||||||
* Stop all system services
|
* Stop all system services
|
||||||
*/
|
*/
|
||||||
stopNSCDServices() {
|
async stopNSCDServices() {
|
||||||
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
|
||||||
try {
|
try {
|
||||||
log.info("services", "Stopping nscd");
|
log.info("services", "Stopping nscd");
|
||||||
childProcess.execSync("sudo service nscd stop");
|
await childProcessAsync.exec("sudo service nscd stop");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.info("services", "Failed to stop nscd");
|
log.info("services", "Failed to stop nscd");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the specified monitor
|
|
||||||
* @param {number} monitorID ID of monitor to start
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async startMonitor(monitorID) {
|
|
||||||
log.info("manage", `Resume Monitor: ${monitorID} by server`);
|
|
||||||
|
|
||||||
await R.exec("UPDATE monitor SET active = 1 WHERE id = ?", [
|
|
||||||
monitorID,
|
|
||||||
]);
|
|
||||||
|
|
||||||
let monitor = await R.findOne("monitor", " id = ? ", [
|
|
||||||
monitorID,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (monitor.id in this.monitorList) {
|
|
||||||
this.monitorList[monitor.id].stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.monitorList[monitor.id] = monitor;
|
|
||||||
monitor.start(this.io);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restart a given monitor
|
|
||||||
* @param {number} monitorID ID of monitor to start
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async restartMonitor(monitorID) {
|
|
||||||
return await this.startMonitor(monitorID);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if monitors are running properly
|
|
||||||
*/
|
|
||||||
async checkMonitors() {
|
|
||||||
log.debug("monitor_checker", "Checking monitors");
|
|
||||||
|
|
||||||
for (let monitorID in this.monitorList) {
|
|
||||||
let monitor = this.monitorList[monitorID];
|
|
||||||
|
|
||||||
// Not for push monitor
|
|
||||||
if (monitor.type === "push") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!monitor.active) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the lastStartBeatTime, if it is too long, then restart
|
|
||||||
if (monitor.lastScheduleBeatTime ) {
|
|
||||||
let diff = dayjs().diff(monitor.lastStartBeatTime, "second");
|
|
||||||
|
|
||||||
if (diff > monitor.interval * 1.5) {
|
|
||||||
log.error("monitor_checker", `Monitor Interval: ${monitor.interval} Monitor ` + monitorID + " lastStartBeatTime diff: " + diff);
|
|
||||||
log.error("monitor_checker", "Unexpected error: Monitor " + monitorID + " is struck for unknown reason");
|
|
||||||
log.error("monitor_checker", "Last start beat time: " + R.isoDateTime(monitor.lastStartBeatTime));
|
|
||||||
log.error("monitor_checker", "Last end beat time: " + R.isoDateTime(monitor.lastEndBeatTime));
|
|
||||||
log.error("monitor_checker", "Last ScheduleBeatTime: " + R.isoDateTime(monitor.lastScheduleBeatTime));
|
|
||||||
|
|
||||||
// Restart
|
|
||||||
log.error("monitor_checker", `Restarting monitor ${monitorID} automatically now`);
|
|
||||||
this.restartMonitor(monitorID);
|
|
||||||
} else {
|
|
||||||
//log.debug("monitor_checker", "Monitor " + monitorID + " is running normally");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//log.debug("monitor_checker", "Monitor " + monitorID + " is not started yet, skipp");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("monitor_checker", "Checking monitors end");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@@ -458,6 +458,7 @@ exports.postgresQuery = function (connectionString, query) {
|
|||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
|
client.end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1141,7 +1142,6 @@ module.exports.axiosAbortSignal = (timeoutMs) => {
|
|||||||
// v16-: AbortSignal.timeout is not supported
|
// v16-: AbortSignal.timeout is not supported
|
||||||
try {
|
try {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
|
|
||||||
setTimeout(() => abortController.abort(), timeoutMs);
|
setTimeout(() => abortController.abort(), timeoutMs);
|
||||||
|
|
||||||
return abortController.signal;
|
return abortController.signal;
|
||||||
|
@@ -8,9 +8,9 @@
|
|||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:disabled="!enabled"
|
:disabled="!enabled"
|
||||||
>
|
>
|
||||||
<a class="btn btn-outline-primary" @click="action()">
|
<button class="btn btn-outline-primary" @click="action()" :aria-label="actionAriaLabel">
|
||||||
<font-awesome-icon :icon="icon" />
|
<font-awesome-icon :icon="icon" />
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -66,6 +66,13 @@ export default {
|
|||||||
action: {
|
action: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The aria-label of the action button
|
||||||
|
*/
|
||||||
|
actionAriaLabel: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: [ "update:modelValue" ],
|
emits: [ "update:modelValue" ],
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<select ref="select" v-model="model" class="form-select" :disabled="disabled" :required="required">
|
<select :id="id" ref="select" v-model="model" class="form-select" :disabled="disabled" :required="required">
|
||||||
<option v-for="option in options" :key="option" :value="option.value" :disabled="option.disabled">{{ option.label }}</option>
|
<option v-for="option in options" :key="option" :value="option.value" :disabled="option.disabled">{{ option.label }}</option>
|
||||||
</select>
|
</select>
|
||||||
<a class="btn btn-outline-primary" :class="{ disabled: actionDisabled }" @click="action()">
|
<button class="btn btn-outline-primary" :class="{ disabled: actionDisabled }" :aria-label="actionAriaLabel" @click="action()">
|
||||||
<font-awesome-icon :icon="icon" />
|
<font-awesome-icon :icon="icon" aria-hidden="true" />
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -20,6 +20,13 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* The id of the form which will be targeted by a <label for=..
|
||||||
|
*/
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* The value of the select field.
|
* The value of the select field.
|
||||||
*/
|
*/
|
||||||
@@ -51,6 +58,13 @@ export default {
|
|||||||
type: Function,
|
type: Function,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* The aria-label of the action button
|
||||||
|
*/
|
||||||
|
actionAriaLabel: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Whether the action button is disabled.
|
* Whether the action button is disabled.
|
||||||
* @example true
|
* @example true
|
||||||
|
@@ -27,13 +27,13 @@
|
|||||||
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
|
<div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6">
|
||||||
<div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
|
<div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2">
|
||||||
<span>{{ day }} {{ $tc("day", day) }}</span>
|
<span>{{ day }} {{ $tc("day", day) }}</span>
|
||||||
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" @click="removeExpiryNotifDay(day)">
|
<button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" @click="removeExpiryNotifDay(day)" :aria-label="$t('Remove the expiry notification')">
|
||||||
<font-awesome-icon class="" icon="times" />
|
<font-awesome-icon icon="times" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-xl-6">
|
<div class="col-12 col-xl-6">
|
||||||
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" />
|
<ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" :action-aria-label="$t('Add a new expiry notification day')" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-primary" type="button" @click="saveSettings()">
|
<button class="btn btn-primary" type="button" @click="saveSettings()">
|
||||||
|
@@ -334,6 +334,8 @@
|
|||||||
"Fingerprint:": "Fingerprint:",
|
"Fingerprint:": "Fingerprint:",
|
||||||
"No status pages": "No status pages",
|
"No status pages": "No status pages",
|
||||||
"Domain Name Expiry Notification": "Domain Name Expiry Notification",
|
"Domain Name Expiry Notification": "Domain Name Expiry Notification",
|
||||||
|
"Add a new expiry notification day": "Add a new expiry notification day",
|
||||||
|
"Remove the expiry notification": "Remove the expiry notification day",
|
||||||
"Proxy": "Proxy",
|
"Proxy": "Proxy",
|
||||||
"Date Created": "Date Created",
|
"Date Created": "Date Created",
|
||||||
"Footer Text": "Footer Text",
|
"Footer Text": "Footer Text",
|
||||||
@@ -656,6 +658,10 @@
|
|||||||
"Notify Channel": "Notify Channel",
|
"Notify Channel": "Notify Channel",
|
||||||
"aboutNotifyChannel": "Notify channel will trigger a desktop or mobile notification for all members of the channel, whether their availability is set to active or away.",
|
"aboutNotifyChannel": "Notify channel will trigger a desktop or mobile notification for all members of the channel, whether their availability is set to active or away.",
|
||||||
"Uptime Kuma URL": "Uptime Kuma URL",
|
"Uptime Kuma URL": "Uptime Kuma URL",
|
||||||
|
"setup a new monitor group": "setup a new monitor group",
|
||||||
|
"openModalTo": "open modal to {0}",
|
||||||
|
"Add a domain": "Add a domain",
|
||||||
|
"Remove domain": "Remove domain '{0}'",
|
||||||
"Icon Emoji": "Icon Emoji",
|
"Icon Emoji": "Icon Emoji",
|
||||||
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
|
"signalImportant": "IMPORTANT: You cannot mix groups and numbers in recipients!",
|
||||||
"aboutWebhooks": "More info about Webhooks on: {0}",
|
"aboutWebhooks": "More info about Webhooks on: {0}",
|
||||||
|
@@ -288,7 +288,9 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="docker-host" class="form-label">{{ $t("Docker Host") }}</label>
|
<label for="docker-host" class="form-label">{{ $t("Docker Host") }}</label>
|
||||||
<ActionSelect
|
<ActionSelect
|
||||||
|
id="docker-host"
|
||||||
v-model="monitor.docker_host"
|
v-model="monitor.docker_host"
|
||||||
|
:action-aria-label="$t('openModalTo', $t('Setup Docker Host'))"
|
||||||
:options="dockerHostOptionsList"
|
:options="dockerHostOptionsList"
|
||||||
:disabled="$root.dockerHostList == null || $root.dockerHostList.length === 0"
|
:disabled="$root.dockerHostList == null || $root.dockerHostList.length === 0"
|
||||||
:icon="'plus'"
|
:icon="'plus'"
|
||||||
@@ -498,9 +500,11 @@
|
|||||||
|
|
||||||
<!-- Parent Monitor -->
|
<!-- Parent Monitor -->
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="parent" class="form-label">{{ $t("Monitor Group") }}</label>
|
<label for="monitorGroupSelector" class="form-label">{{ $t("Monitor Group") }}</label>
|
||||||
<ActionSelect
|
<ActionSelect
|
||||||
|
id="monitorGroupSelector"
|
||||||
v-model="monitor.parent"
|
v-model="monitor.parent"
|
||||||
|
:action-aria-label="$t('openModalTo', 'setup a new monitor group')"
|
||||||
:options="parentMonitorOptionsList"
|
:options="parentMonitorOptionsList"
|
||||||
:disabled="sortedGroupMonitorList.length === 0 && draftGroupName == null"
|
:disabled="sortedGroupMonitorList.length === 0 && draftGroupName == null"
|
||||||
:icon="'plus'"
|
:icon="'plus'"
|
||||||
@@ -860,7 +864,7 @@ const monitorDefaults = {
|
|||||||
interval: 60,
|
interval: 60,
|
||||||
retryInterval: 60,
|
retryInterval: 60,
|
||||||
resendInterval: 0,
|
resendInterval: 0,
|
||||||
maxretries: 1,
|
maxretries: 0,
|
||||||
timeout: 48,
|
timeout: 48,
|
||||||
notificationIDList: {},
|
notificationIDList: {},
|
||||||
ignoreTls: false,
|
ignoreTls: false,
|
||||||
|
@@ -69,13 +69,17 @@
|
|||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
{{ $t("Domain Names") }}
|
{{ $t("Domain Names") }}
|
||||||
<font-awesome-icon icon="plus-circle" class="btn-add-domain action text-primary" @click="addDomainField" />
|
<button class="p-0 bg-transparent border-0" :aria-label="$t('Add a domain')" @click="addDomainField">
|
||||||
|
<font-awesome-icon icon="plus-circle" class="action text-primary" />
|
||||||
|
</button>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<ul class="list-group domain-name-list">
|
<ul class="list-group domain-name-list">
|
||||||
<li v-for="(domain, index) in config.domainNameList" :key="index" class="list-group-item">
|
<li v-for="(domain, index) in config.domainNameList" :key="index" class="list-group-item">
|
||||||
<input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" placeholder="example.com" />
|
<input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" placeholder="example.com" />
|
||||||
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="removeDomain(index)" />
|
<button class="p-0 bg-transparent border-0" :aria-label="$t('Remove domain', [ domain ])" @click="removeDomain(index)">
|
||||||
|
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" />
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -101,6 +101,9 @@ class Logger {
|
|||||||
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
|
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
|
||||||
*/
|
*/
|
||||||
log(module, msg, level) {
|
log(module, msg, level) {
|
||||||
|
if (level === "DEBUG" && !exports.isDev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
|
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -115,6 +115,10 @@ class Logger {
|
|||||||
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
|
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
|
||||||
*/
|
*/
|
||||||
log(module: string, msg: any, level: string) {
|
log(module: string, msg: any, level: string) {
|
||||||
|
if (level === "DEBUG" && !isDev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
|
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user