diff --git a/libraries/deno-commons-1.6.0-mod.ts b/libraries/deno-commons-1.6.0-mod.ts new file mode 100644 index 0000000..0fe6b36 --- /dev/null +++ b/libraries/deno-commons-1.6.0-mod.ts @@ -0,0 +1,382 @@ +// 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 + +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`; + } +} + +export 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); + } +} + +export const log = new Logger(); + +export 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; +} + +export 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; +} + +export 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; +} + +export 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); + } +} + +export 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), + ); +} + +export 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; +} + +export async function sleep(timeoutMillis: number): Promise { + await new Promise((resolve) => setTimeout(resolve, timeoutMillis)); +} + +export 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; + } +} + +export 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"); + } + } +} + +export 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; + } +} + +export 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, + ); +} + +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"), + ); +} + +export function fromFileUrl(url: URL | string): string { + return posixFromFileUrl(url); +} \ No newline at end of file diff --git a/libraries/deno-server-control-1.6.0-mod.ts b/libraries/deno-server-control-1.6.0-mod.ts new file mode 100644 index 0000000..fff15f7 --- /dev/null +++ b/libraries/deno-server-control-1.6.0-mod.ts @@ -0,0 +1,210 @@ +// 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, + fromFileUrl, + log, + parseProcessLine, + Process, + ProcessBar, + readFileToString, + sleep, +} from "https://global.hatter.ink/script/get/@1/deno-commons-1.6.0-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; +} + +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; +} + +async function loadServerControlConfig( + metaUrl: string, + serverControlConfigFile?: string, +): Promise { + const fullServerControlConfigFile = serverControlConfigFile || + (fromFileUrl(metaUrl).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); +} + +export async function serverControlMain(metaUrl: string) { + const args = Deno.args; + if (args.length === 0) { + log.error(`No args. + +server-control.ts status +server-control.ts kill|stop +server-control.ts re|restart`); + return; + } + const serverControlConfigFile = Deno.env.get("SERVER_CONTROL_CONFIG_FILE"); + const serverControlConfig = await loadServerControlConfig( + metaUrl, + 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; + } +} diff --git a/single-scripts/server-control.ts b/single-scripts/server-control.ts index 7f7f6fd..0e13d78 100755 --- a/single-scripts/server-control.ts +++ b/single-scripts/server-control.ts @@ -8,567 +8,7 @@ // deno-1.6.0 run --allow-all server-control.ts ?? -class Term { - constructor() { - } +import {log} from "https://global.hatter.ink/script/get/@1/deno-commons-1.6.0-mod.ts"; +import {serverControlMain} from "https://global.hatter.ink/script/get/@3/deno-server-control-1.6.0-mod.ts"; - 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)); +serverControlMain(import.meta.url).catch((e) => log.error(e));