From 66bb18e150d7040c27e7294cfafc311f1f55bcad Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Tue, 3 Feb 2026 00:00:21 +0800 Subject: [PATCH] update server-control.ts --- single-scripts/server-control.ts | 431 +++++++++++++++++++++++++++++-- 1 file changed, 408 insertions(+), 23 deletions(-) diff --git a/single-scripts/server-control.ts b/single-scripts/server-control.ts index 9fac11d..7f7f6fd 100755 --- a/single-scripts/server-control.ts +++ b/single-scripts/server-control.ts @@ -1,18 +1,370 @@ -#!/usr/bin/env deno -- --allow-all +#!/usr/bin/env deno-1.6.0 run --allow-all -// !IMPORTANT! not ready +// 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 -import { - execCommand, - execCommandShell, - fetchWithTimeout, - log, - ProcessBar, - readFileToString, - sleep, -} from "https://global.hatter.ink/script/get/@24/deno-commons-mod.ts"; -import {parseProcessLine, Process,} from "https://global.hatter.ink/script/get/@0/deno-process-mod.ts"; -import {fromFileUrl} from "https://deno.land/std/path/mod.ts"; +// 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; @@ -35,12 +387,19 @@ async function listJavaProcesses( processOutput.assertSuccess(); const stdout = processOutput.getStdoutAsStringThenTrim(); const lines = stdout.split("\n"); - return lines.filter((line) => { + const processes = lines.filter((line) => { return line.includes("java") && line.includes("hatterserver") && line.includes(`-DAPPID=${appId}`); }).map((line) => { return parseProcessLine(line); - }).filter((line) => line !== null); + }); + const filterProcesses: Process[] = []; + for (const process of processes) { + if (process !== null) { + filterProcesses.push(process); + } + } + return filterProcesses; } async function checkServerStarted( @@ -55,6 +414,7 @@ async function checkServerStarted( 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 { @@ -63,7 +423,7 @@ async function checkServerStarted( timeoutMillis, ); if (response.status === 200) { - log.success("Server started!"); + startServerSuccess = true; return true; } } catch (e) { @@ -72,15 +432,37 @@ async function checkServerStarted( await sleep(intervalMillis); } }); - log.warn("Server failed!"); + 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 || - (fromFileUrl(import.meta.url).replace(".ts", ".json")); + (posixFromFileUrl(import.meta.url).replace(".ts", ".json")); + log.debug( + `Read server control config file: ${fullServerControlConfigFile}`, + ); const serverControlConfigJson = await readFileToString( fullServerControlConfigFile, ); @@ -97,12 +479,12 @@ async function handleStatus( ): Promise { const processes = await listJavaProcesses(serverControlConfig); if (processes.length === 0) { - log.info("No process are running!"); + log.warn("No process are running!"); return; } - log.info( - `Find ${processes.length} process(es), pid(s): ${ - processes.map((p) => p.pid).join(", ") + log.success( + `Find ${processes.length} process(es), pid: \n-> ${ + processes.map((p) => p.pid).join("-> ") }`, ); } @@ -162,7 +544,10 @@ server-control.js kill|stop server-control.js re|restart`); return; } - const serverControlConfig = await loadServerControlConfig(); + const serverControlConfigFile = Deno.env.get("SERVER_CONTROL_CONFIG_FILE"); + const serverControlConfig = await loadServerControlConfig( + serverControlConfigFile, + ); if (!serverControlConfig.appId) { log.error("Config appId not found!"); return;