#!/usr/bin/env deno -- --allow-all // !IMPORTANT! not ready import { execCommand, execCommandShell, fetchWithTimeout, log, ProcessBar, readFileToString, 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"; 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 { 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 { 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 { 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 { 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); } });