add server-control.ts

This commit is contained in:
2026-02-01 01:04:35 +08:00
parent 3c6fd6eb9e
commit 015a7d1581
2 changed files with 326 additions and 4 deletions

View File

@@ -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
View 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);
}
});