add server-control.ts
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
// Reference:
|
// Reference:
|
||||||
// - https://docs.deno.com/runtime/fundamentals/testing/
|
// - https://docs.deno.com/runtime/fundamentals/testing/
|
||||||
|
|
||||||
import { assert } from "jsr:@std/assert/assert";
|
import {assert} from "jsr:@std/assert/assert";
|
||||||
import { assertEquals } from "jsr:@std/assert";
|
import {assertEquals} from "jsr:@std/assert";
|
||||||
import { decodeBase64, encodeBase64 } from "jsr:@std/encoding/base64";
|
import {decodeBase64, encodeBase64} from "jsr:@std/encoding/base64";
|
||||||
import { dirname } from "https://deno.land/std@0.208.0/path/mod.ts";
|
import {dirname} from "https://deno.land/std@0.208.0/path/mod.ts";
|
||||||
|
|
||||||
// reference: https://docs.deno.com/examples/hex_base64_encoding/
|
// reference: https://docs.deno.com/examples/hex_base64_encoding/
|
||||||
// import { decodeBase64, encodeBase64 } from "jsr:@std/encoding/base64";
|
// import { decodeBase64, encodeBase64 } from "jsr:@std/encoding/base64";
|
||||||
@@ -569,6 +569,26 @@ export class ProcessBar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchWithTimeout(
|
||||||
|
input: URL | Request | string,
|
||||||
|
timeout?: number,
|
||||||
|
initCallback?: (init: RequestInit) => RequestInit,
|
||||||
|
): Promise<Response> {
|
||||||
|
const fetchTimeout = timeout || 10000;
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const timeoutHandler = setTimeout(() => {
|
||||||
|
abortController.abort(`Timeout ${fetchTimeout} ms`);
|
||||||
|
}, fetchTimeout);
|
||||||
|
let init: RequestInit = {};
|
||||||
|
init.signal = abortController.signal;
|
||||||
|
if (initCallback) {
|
||||||
|
init = initCallback(init);
|
||||||
|
}
|
||||||
|
const response = await fetch(input, init);
|
||||||
|
clearTimeout(timeoutHandler);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
Deno.test("isOn", () => {
|
Deno.test("isOn", () => {
|
||||||
assertEquals(false, isOn(undefined));
|
assertEquals(false, isOn(undefined));
|
||||||
assertEquals(false, isOn(""));
|
assertEquals(false, isOn(""));
|
||||||
|
|||||||
302
single-scripts/server-control.ts
Executable file
302
single-scripts/server-control.ts
Executable file
@@ -0,0 +1,302 @@
|
|||||||
|
#!/usr/bin/env deno -- --allow-all
|
||||||
|
|
||||||
|
// !IMPORTANT! not ready
|
||||||
|
|
||||||
|
import {
|
||||||
|
execCommand,
|
||||||
|
fetchWithTimeout,
|
||||||
|
log,
|
||||||
|
ProcessBar,
|
||||||
|
sleep,
|
||||||
|
} from "https://global.hatter.ink/script/get/@24/deno-commons-mod.ts";
|
||||||
|
import {assertEquals} from "jsr:@std/assert";
|
||||||
|
import {assert} from "jsr:@std/assert/assert";
|
||||||
|
import {fromFileUrl} from "https://deno.land/std/path/mod.ts";
|
||||||
|
import {execCommandShell, readFileToString,} from "../libraries/deno-commons-mod.ts";
|
||||||
|
|
||||||
|
interface HealthCheck {
|
||||||
|
url?: string;
|
||||||
|
count?: number; // default 30
|
||||||
|
timeoutMillis?: number; // default 1000
|
||||||
|
intervalMillis?: number; // default 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServerControlConfig {
|
||||||
|
appId?: string;
|
||||||
|
startup?: string;
|
||||||
|
healthCheck?: HealthCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Process {
|
||||||
|
user: string;
|
||||||
|
pid: number;
|
||||||
|
cpu: number;
|
||||||
|
mem: number;
|
||||||
|
vsz: number;
|
||||||
|
rss: number;
|
||||||
|
tty: string;
|
||||||
|
stat: string;
|
||||||
|
start: string;
|
||||||
|
time: string;
|
||||||
|
command: string;
|
||||||
|
constructor(
|
||||||
|
user: string,
|
||||||
|
pid: number,
|
||||||
|
cpu: number,
|
||||||
|
mem: number,
|
||||||
|
vsz: number,
|
||||||
|
rss: number,
|
||||||
|
tty: string,
|
||||||
|
stat: string,
|
||||||
|
start: string,
|
||||||
|
time: string,
|
||||||
|
command: string,
|
||||||
|
) {
|
||||||
|
this.user = user;
|
||||||
|
this.pid = pid;
|
||||||
|
this.cpu = cpu;
|
||||||
|
this.mem = mem;
|
||||||
|
this.vsz = vsz;
|
||||||
|
this.rss = rss;
|
||||||
|
this.tty = tty;
|
||||||
|
this.stat = stat;
|
||||||
|
this.start = start;
|
||||||
|
this.time = time;
|
||||||
|
this.command = command;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listJavaProcesses(
|
||||||
|
serverControlConfig: ServerControlConfig,
|
||||||
|
): Promise<Process[]> {
|
||||||
|
const appId = serverControlConfig.appId;
|
||||||
|
const processOutput = await execCommand("ps", ["aux"]);
|
||||||
|
processOutput.assertSuccess();
|
||||||
|
const stdout = processOutput.getStdoutAsStringThenTrim();
|
||||||
|
const lines = stdout.split("\n");
|
||||||
|
return lines.filter((line) => {
|
||||||
|
return line.includes("java") && line.includes("hatterserver") &&
|
||||||
|
line.includes(`-DAPPID=${appId}`);
|
||||||
|
}).map((line) => {
|
||||||
|
return parseProcessLine(line);
|
||||||
|
}).filter((line) => line !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseProcessLine(line: string): Process | null {
|
||||||
|
const processMatchRegex =
|
||||||
|
/^\s*([\w+\-_]+)\s+(\d+)\s+([\d.]+)\s+([\d.]+)\s+(\d+)\s+(\d+)\s+([\w\/\\?]+)\s+([\w+<>]+)\s+([\w:]+)\s+([\d:]+)\s+(.*)$/;
|
||||||
|
// "app 3622 0.2 24.0 2932504 453004 ? Sl Jan25 23:04 /usr/lib/jvm/jdk-25/bin/java -Dfastjson.parser.safeMode=true......";
|
||||||
|
// USER PID CPU MEM VSZ RSS TTY STAT START TIME COMMAND
|
||||||
|
const matcher = line.match(processMatchRegex);
|
||||||
|
if (!matcher) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const user = matcher[1];
|
||||||
|
const pid = parseInt(matcher[2]);
|
||||||
|
const cpu = Number(matcher[3]);
|
||||||
|
const mem = Number(matcher[4]);
|
||||||
|
const vsz = parseInt(matcher[5]);
|
||||||
|
const rss = parseInt(matcher[6]);
|
||||||
|
const tty = matcher[7];
|
||||||
|
const stat = matcher[8];
|
||||||
|
const start = matcher[9];
|
||||||
|
const time = matcher[10];
|
||||||
|
const command = matcher[11];
|
||||||
|
return new Process(
|
||||||
|
user,
|
||||||
|
pid,
|
||||||
|
cpu,
|
||||||
|
mem,
|
||||||
|
vsz,
|
||||||
|
rss,
|
||||||
|
tty,
|
||||||
|
stat,
|
||||||
|
start,
|
||||||
|
time,
|
||||||
|
command,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkServerStarted(
|
||||||
|
serverControlConfig: ServerControlConfig,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const healthCheck = serverControlConfig.healthCheck;
|
||||||
|
const healthCheckUrl = healthCheck?.url;
|
||||||
|
if (!healthCheck || !healthCheckUrl) {
|
||||||
|
log.error("Health check URL is not configured!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const count = healthCheck.count || 30;
|
||||||
|
const intervalMillis = healthCheck.intervalMillis || 1000;
|
||||||
|
const timeoutMillis = healthCheck.timeoutMillis || 1000;
|
||||||
|
await new ProcessBar("Starting server").call(async () => {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
try {
|
||||||
|
const response = await fetchWithTimeout(
|
||||||
|
healthCheckUrl,
|
||||||
|
timeoutMillis,
|
||||||
|
);
|
||||||
|
if (response.status === 200) {
|
||||||
|
log.success("Server started!");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// IGNORE
|
||||||
|
}
|
||||||
|
await sleep(intervalMillis);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log.warn("Server failed!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadServerControlConfig(
|
||||||
|
serverControlConfigFile?: string,
|
||||||
|
): Promise<ServerControlConfig> {
|
||||||
|
const fullServerControlConfigFile = serverControlConfigFile ||
|
||||||
|
(fromFileUrl(import.meta.url).replace(".ts", ".json"));
|
||||||
|
const serverControlConfigJson = await readFileToString(
|
||||||
|
fullServerControlConfigFile,
|
||||||
|
);
|
||||||
|
if (serverControlConfigJson === null) {
|
||||||
|
throw new Error(`Read file ${fullServerControlConfigFile} failed.`);
|
||||||
|
}
|
||||||
|
return JSON.parse(
|
||||||
|
serverControlConfigJson,
|
||||||
|
) as ServerControlConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStatus(
|
||||||
|
serverControlConfig: ServerControlConfig,
|
||||||
|
): Promise<void> {
|
||||||
|
const processes = await listJavaProcesses(serverControlConfig);
|
||||||
|
if (processes.length === 0) {
|
||||||
|
log.info("No process are running!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.info(
|
||||||
|
`Find ${processes.length} process(es), pid(s): ${
|
||||||
|
processes.map((p) => p.pid).join(", ")
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStop(serverControlConfig: ServerControlConfig) {
|
||||||
|
const processes = await listJavaProcesses(serverControlConfig);
|
||||||
|
if (processes.length === 0) {
|
||||||
|
log.info("No process are running!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (processes.length > 1) {
|
||||||
|
log.warn(
|
||||||
|
`Too many processes are running, pid(s): ${
|
||||||
|
processes.map((p) => p.pid).join(", ")
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pid = processes[0].pid;
|
||||||
|
log.warn(`Kill pid: ${pid}`);
|
||||||
|
await execCommandShell("kill", [`${pid}`]);
|
||||||
|
await sleep(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRestart(serverControlConfig: ServerControlConfig) {
|
||||||
|
let processes = await listJavaProcesses(serverControlConfig);
|
||||||
|
if (processes.length > 1) {
|
||||||
|
log.warn(
|
||||||
|
`Too many processes are running, pid(s): ${
|
||||||
|
processes.map((p) => p.pid).join(", ")
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!serverControlConfig.startup) {
|
||||||
|
log.error("Startup command is not configured!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (processes.length > 0) {
|
||||||
|
await execCommandShell("kill", [`${processes[0].pid}`]);
|
||||||
|
await sleep(500);
|
||||||
|
processes = await listJavaProcesses(serverControlConfig);
|
||||||
|
}
|
||||||
|
log.info(`Run command: ${serverControlConfig.startup} &`);
|
||||||
|
await execCommandShell("sh", ["-c", `${serverControlConfig.startup} &`]);
|
||||||
|
log.success("Start server, checking ...");
|
||||||
|
await checkServerStarted(serverControlConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = Deno.args;
|
||||||
|
if (args.length === 0) {
|
||||||
|
log.error("No args.");
|
||||||
|
console.log("");
|
||||||
|
console.log("server-control.js status");
|
||||||
|
console.log("server-control.js kill|stop");
|
||||||
|
console.log("server-control.js re|restart");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const serverControlConfig = await loadServerControlConfig();
|
||||||
|
if (!serverControlConfig.appId) {
|
||||||
|
log.error("Config appId not found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (args[0]) {
|
||||||
|
case "status":
|
||||||
|
await handleStatus(serverControlConfig);
|
||||||
|
return;
|
||||||
|
case "kill":
|
||||||
|
case "stop":
|
||||||
|
await handleStop(serverControlConfig);
|
||||||
|
return;
|
||||||
|
case "re":
|
||||||
|
case "restart":
|
||||||
|
await handleRestart(serverControlConfig);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
log.warn("Argument error!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => log.error(e));
|
||||||
|
|
||||||
|
Deno.test("parseProcessLine", () => {
|
||||||
|
const p1 = parseProcessLine(
|
||||||
|
"app 3622 0.2 24.0 2932504 453004 ? Sl Jan25 23:04 /usr/lib/jvm/jdk-25/bin/java -Dfastjson.parser.safeMode=true......",
|
||||||
|
);
|
||||||
|
if (p1 !== null) {
|
||||||
|
assertEquals("app", p1.user);
|
||||||
|
assertEquals(3622, p1.pid);
|
||||||
|
} else {
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
const p2 = parseProcessLine(
|
||||||
|
"root 10880 0.0 0.0 151104 1820 pts/2 R+ 23:17 0:00 ps aux",
|
||||||
|
);
|
||||||
|
if (p2 !== null) {
|
||||||
|
assertEquals("root", p2.user);
|
||||||
|
assertEquals(10880, p2.pid);
|
||||||
|
} else {
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
const p3 = parseProcessLine(
|
||||||
|
"root 18 0.0 0.0 0 0 ? S< 2020 0:00 [kblockd]",
|
||||||
|
);
|
||||||
|
if (p3 !== null) {
|
||||||
|
assertEquals("root", p3.user);
|
||||||
|
assertEquals(18, p3.pid);
|
||||||
|
} else {
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
const p4 = parseProcessLine(
|
||||||
|
"filebro+ 10377 0.0 0.6 1901492 12432 ? Sl 2024 25:15 /home/filebrowser/filebrowser",
|
||||||
|
);
|
||||||
|
if (p4 !== null) {
|
||||||
|
assertEquals("filebro+", p4.user);
|
||||||
|
assertEquals(10377, p4.pid);
|
||||||
|
} else {
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user