#!/usr/bin/env deno-1.6.0 run --allow-all // Compatible with Deno 1.6.0 (works on CentOS 7) // https://play.hatter.me/doc/showDocDetail.jssp?id=8879 // https://github.com/denoland/deno/releases/tag/v1.6.0 // https://play.hatter.me/doc/showDocDetail.jssp?id=8881 // https://play.hatter.me/doc/showDocDetail.jssp?id=8882 // deno-1.6.0 run --allow-all server-control.ts ?? class Term { constructor() { } blink(message: string): string { return `\x1b[5m${message}\x1b[0m`; } bold(message: string): string { return `\x1b[1m${message}\x1b[0m`; } under(message: string): string { return `\x1b[4m${message}\x1b[0m`; } red(message: string): string { return `\x1b[31m${message}\x1b[0m`; } green(message: string): string { return `\x1b[32m${message}\x1b[0m`; } yellow(message: string): string { return `\x1b[33m${message}\x1b[0m`; } blue(message: string): string { return `\x1b[34m${message}\x1b[0m`; } pink(message: string): string { return `\x1b[35m${message}\x1b[0m`; } cyan(message: string): string { return `\x1b[36m${message}\x1b[0m`; } } const term = new Term(); function pad(message: string, length: number): string { if (message.length >= length) { return message; } return message + " ".repeat(length - message.length); } const LOGGER_PREFIX_LEN: number = 8; class Logger { constructor() { } // deno-lint-ignore no-explicit-any success(...data: any[]) { this.log( term.bold(term.green(`[${pad("SUCCESS", LOGGER_PREFIX_LEN)}]`)), data, ); } // deno-lint-ignore no-explicit-any error(...data: any[]) { this.log( term.bold(term.red(`[${pad("ERROR", LOGGER_PREFIX_LEN)}]`)), data, ); } // deno-lint-ignore no-explicit-any warn(...data: any[]) { this.log( term.bold(term.yellow(`[${pad("WARN", LOGGER_PREFIX_LEN)}]`)), data, ); } // deno-lint-ignore no-explicit-any warning(...data: any[]) { this.log( term.blink( term.bold(term.yellow(`[${pad("WARN", LOGGER_PREFIX_LEN)}]`)), ), data, ); } // deno-lint-ignore no-explicit-any info(...data: any[]) { this.log(term.bold(`[${pad("INFO", LOGGER_PREFIX_LEN)}]`), data); } // deno-lint-ignore no-explicit-any debug(...data: any[]) { this.log(`[${pad("DEBUG", LOGGER_PREFIX_LEN)}]`, data); } // deno-lint-ignore no-explicit-any log(prefix: string, data: any[]) { const args = [prefix]; for (let i = 0; i < data.length; i++) { args.push(data[i]); } console.log.apply(console, args); } } const log = new Logger(); function getHomeDir(): string | null { if (Deno.build.os === "windows") { const userProfile = Deno.env.get("USERPROFILE"); if (userProfile) { return userProfile; } const homeDrive = Deno.env.get("HOMEDRIVE"); const homePath = Deno.env.get("HOMEPATH"); if (homeDrive && homePath) { return homeDrive + homePath; } return null; } return Deno.env.get("HOME") || null; } function resolveFilename(filename: string): string { if (filename.startsWith("~/")) { return getHomeDir() + filename.substring(1); } return filename; } function toCmdArray(command: string, args?: string[]): string[] { const cmdArray = [command]; if (args) { for (const arg of args) { cmdArray.push(arg); } } log.debug("Command: ", cmdArray); return cmdArray; } async function execCommandShell( command: string, args?: string[], ): Promise { const process = Deno.run({ cmd: toCmdArray(command, args), stdout: "inherit", stderr: "inherit", }); const status = await process.status(); process.close(); return status.code; } class ProcessOutput { code: number; stdout: string; stderr: string; constructor(code: number, stdout: string, stderr: string) { this.code = code; this.stdout = stdout; this.stderr = stderr; } assertSuccess(): ProcessOutput { if (this.code !== 0) { throw new Error( `Failed to execute command, exit code: ${this.code}\n- stdout: ${this.stdout}\n- stderr: ${this.stderr}\n`, ); } return this; } getStdoutAsStringThenTrim(): string { return this.stdout.trim(); } getStdoutAsJson(): any { return JSON.parse(this.stdout); } getStderrAsJson(): any { return JSON.parse(this.stderr); } } async function execCommand( command: string, args?: string[], ): Promise { const process = Deno.run({ cmd: toCmdArray(command, args), stdout: "piped", stderr: "piped", }); const { code } = await process.status(); const stdout = await process.output(); const stderr = await process.stderrOutput(); return new ProcessOutput( code, new TextDecoder().decode(stdout), new TextDecoder().decode(stderr), ); } async function fetchWithTimeout( input: URL | Request | string, timeout?: number, initCallback?: (init: RequestInit) => RequestInit, ): Promise { const fetchTimeout = timeout || 10000; const abortController = new AbortController(); const timeoutHandler = setTimeout(() => { abortController.abort(); }, fetchTimeout); let init: RequestInit = {}; init.signal = abortController.signal; if (initCallback) { init = initCallback(init); } const response = await fetch(input, init); clearTimeout(timeoutHandler); return response; } async function sleep(timeoutMillis: number): Promise { await new Promise((resolve) => setTimeout(resolve, timeoutMillis)); } async function readFileToString( filename: string, ): Promise { try { return await Deno.readTextFile(resolveFilename(filename)); } catch (e) { if (e instanceof Error && e.name == "NotFound") { return null; } throw e; } } class ProcessBar { interval?: number; message: string; constructor(message?: string) { this.message = message || "Processing"; } async call(cb: () => Promise, clearLine?: boolean): Promise { this.start(); try { return await cb(); } finally { this.stop(clearLine); } } write(message: string): void { Deno.stdout.writeSync(new TextEncoder().encode(message)); } start(): void { const startMs = new Date().getTime(); let count = 0; this.interval = setInterval(() => { const dots = ".".repeat(((count++) % 10) + 1); const costMs = new Date().getTime() - startMs; const time = `${Math.floor(costMs / 1000)}s`; this.write(`\r${this.message} ${time} ${dots}\x1b[K`); }, 500); } stop(clearLine?: boolean): void { if (this.interval) { clearInterval(this.interval); this.write(clearLine ? "\r\x1b[K" : "\n"); } } } 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; } } 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, ); } interface HealthCheck { url?: string; count?: number; // default 30 timeoutMillis?: number; // default 1000 intervalMillis?: number; // default 1000 } interface ServerControlConfig { appId?: string; startup?: string; healthCheck?: HealthCheck; } 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"); const processes = lines.filter((line) => { return line.includes("java") && line.includes("hatterserver") && line.includes(`-DAPPID=${appId}`); }).map((line) => { return parseProcessLine(line); }); const filterProcesses: Process[] = []; for (const process of processes) { if (process !== null) { filterProcesses.push(process); } } return filterProcesses; } 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; let startServerSuccess = false; 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) { startServerSuccess = true; return true; } } catch (e) { // IGNORE } await sleep(intervalMillis); } }); if (startServerSuccess) { log.success("Server started!"); } else { log.warn("Server failed!"); } return false; } function assertArg(url: URL | string) { url = url instanceof URL ? url : new URL(url); if (url.protocol !== "file:") { throw new TypeError("Must be a file URL."); } return url; } function posixFromFileUrl(url: URL | string): string { url = assertArg(url); return decodeURIComponent( url.pathname.replace(/%(?![0-9A-Fa-f]{2})/g, "%25"), ); } async function loadServerControlConfig( serverControlConfigFile?: string, ): Promise { const fullServerControlConfigFile = serverControlConfigFile || (posixFromFileUrl(import.meta.url).replace(".ts", ".json")); log.debug( `Read server control config file: ${fullServerControlConfigFile}`, ); 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.warn("No process are running!"); return; } log.success( `Find ${processes.length} process(es), pid: \n-> ${ 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. server-control.js status server-control.js kill|stop server-control.js re|restart`); return; } const serverControlConfigFile = Deno.env.get("SERVER_CONTROL_CONFIG_FILE"); const serverControlConfig = await loadServerControlConfig( serverControlConfigFile, ); 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));