UNPKG

@the-stations-project/sdk

Version:

SDK for developing Station Services.

439 lines (341 loc) 11.4 kB
import Child from "child_process"; import Fs from "fs/promises"; import Path from "path"; /* BASIC */ export enum ExitCodes { Ok = 0, ErrUnknown = 1, ErrNoCommand = 2, ErrMissingParameter = 3, } export class Result<C, V> { code: C; value: V; private initial_code: C; private initial_value: V; log_message: undefined | (() => string); panic_message: () => string = () => "Unknown: invalid result."; constructor(code: C, value: V) { this.initial_code = this.code = code; this.initial_value = this.value = value; } err(cb: (result: Result<C, V>) => any) { if (this.has_failed) { cb(this); } return this; } ok(cb: (result: Result<C, V>) => any) { if (!this.has_failed) { cb(this); } return this; } or_panic(msg?: string) { if (this.has_failed) { console.trace(this); const message = msg ?? this.panic_message(); log("PANIC", message); throw message; } return this; } or_log_error(msg?: string) { if (this.has_failed) { log("ERROR", msg ?? this.panic_message()); } return this; } to_string(): string { return `${this.code}|${this.value}`; } finalize(code: C, value: V) { this.code = code; this.value = value; if (this.log_message) log("ACTIVITY", this.log_message()); return this; } finalize_with_value(value: V) { this.value = value; return this.finalize(this.code, value); } finalize_with_code(code: C) { this.code = code; return this.finalize(code, this.value); } revert() { return this.finalize(this.initial_code, this.initial_value); } get has_failed(): boolean { return this.code > 0; } } export function contains_undefined_arguments(args: IArguments) { return [...args].filter(x => x == undefined).length != 0; } /* CLI */ export async function start_module(main: (subcommand: string, args: string[]) => Promise<Result<any, any>>, cb: (result: Result<any, any>) => void) { const args = process.argv; //remove first two args args.splice(0, 2); //get subcommand const subcommand = args.splice(0, 1)[0]; //run const result = await main(subcommand, args); cb(result); } /* LOGS */ export type LogType = "ACTIVITY" | "ERROR" | "PANIC" | "OTHER" | "STATUS"; const LOG_DIR = "logs/current"; export async function log(type: LogType, msg: string) { msg = `TYPE ${type}\nPID ${process.pid}\n${msg}\n\n`; /* get filename */ const timestamp = new Date().toISOString(); const filename = `log-${timestamp}`; const path = Path.join(LOG_DIR, filename); (await Registry.append(path, msg)).or_panic("failed to log"); } /* REGISTRY */ export enum RegistryExitCodes { OkUnchanged = -1, Ok = 0, ErrUnknown = 1, ErrRead = 2, ErrWrite = 3, ErrDel = 4, } class RegistryResult<T> extends Result<RegistryExitCodes, T|undefined> { constructor() { super(RegistryExitCodes.ErrUnknown, undefined); } } export const Registry = { base_path: "registry", get_full_path: (path: string) => Registry.join_paths(Registry.base_path, path), get_panic_message: (msg: string) => `Registry: ${msg}.`, join_paths(...args: string[]): string { return Path.join(...args .filter(x => x) .map(x => x.split("/")) .flat() ); }, async mkdir(path: string): Promise<RegistryResult<undefined>> { const full_path = Registry.get_full_path(path); const result = new RegistryResult<undefined>(); result.panic_message = () => Registry.get_panic_message(`failed to create directory "${path}"`); try { await Fs.mkdir(full_path, { recursive: true }); result.finalize_with_code(RegistryExitCodes.Ok); } catch { (await Registry.test(full_path)) .ok(() => result.finalize_with_code(RegistryExitCodes.OkUnchanged)) .err(() => result.finalize_with_code(RegistryExitCodes.ErrUnknown)); } return result; }, async write(path: string, content: string): Promise<RegistryResult<undefined>> { const full_path = Registry.get_full_path(path); const result = new RegistryResult<undefined>(); result.panic_message = () => Registry.get_panic_message(`failed to write file "${path}"`); try { await Fs.writeFile(full_path, content); result.code = RegistryExitCodes.Ok; } catch { result.code = RegistryExitCodes.ErrWrite; } return result; }, async append(path: string, content: string): Promise<RegistryResult<undefined>> { const full_path = Registry.get_full_path(path); const result = new RegistryResult<undefined>(); result.panic_message = () => Registry.get_panic_message(`failed to append to file "${path}"`); try { await Fs.appendFile(full_path, content); result.finalize_with_code(RegistryExitCodes.Ok); } catch { result.finalize_with_code(RegistryExitCodes.ErrWrite); } return result; }, async read(path: string): Promise<RegistryResult<string|undefined>> { const full_path = Registry.get_full_path(path); const result: RegistryResult<string|undefined> = new RegistryResult<undefined>(); result.panic_message = () => Registry.get_panic_message(`failed to read file "${path}"`); try { const text = await Fs.readFile(full_path, { encoding: "utf8" }); result.code = RegistryExitCodes.Ok; result.value = text; } catch { result.code = RegistryExitCodes.ErrRead; } return result; }, async ls(path: string): Promise<RegistryResult<string[]|undefined>> { const full_path = Registry.get_full_path(path); const result = new RegistryResult<string[]|undefined>(); result.panic_message = () => Registry.get_panic_message(`failed to read directory "${path}"`); try { const items = await Fs.readdir(full_path); result.finalize(RegistryExitCodes.Ok, items); } catch { result.finalize_with_code(RegistryExitCodes.ErrRead); } return result; }, async delete(path: string): Promise<RegistryResult<undefined>> { const full_path = Registry.get_full_path(path); const result = new RegistryResult<undefined>(); result.panic_message = () => Registry.get_panic_message(`failed to delete item "${path}"`); try { await Fs.rm(full_path, { recursive: true }); result.finalize_with_code(RegistryExitCodes.Ok); } catch { result.finalize_with_code(RegistryExitCodes.ErrDel); } return result; }, async read_or_create(path: string, default_value: string): Promise<RegistryResult<string|undefined>> { const read_result = await Registry.read(path); if (!read_result.has_failed) return read_result; const write_result = await Registry.write(path, default_value); return write_result; }, async test(path: string): Promise<RegistryResult<undefined>> { const full_path = Registry.get_full_path(path); const result = new RegistryResult<undefined>(); try { await Fs.stat(full_path); result.finalize_with_code(RegistryExitCodes.Ok); } catch {} return result; }, async copy(src: string, dest: string): Promise<RegistryResult<undefined>> { const full_src = Registry.get_full_path(src); const full_dest = Registry.get_full_path(dest); const result = new RegistryResult<undefined>(); try { await Fs.cp(full_src, full_dest, { recursive: true }); result.finalize_with_code(RegistryExitCodes.Ok); } catch {} return result; }, async move(src: string, dest: string): Promise<RegistryResult<undefined>> { const full_src = Registry.get_full_path(src); const full_dest = Registry.get_full_path(dest); const result = new RegistryResult<undefined>(); try { await Fs.rename(full_src, full_dest); result.finalize_with_code(RegistryExitCodes.Ok); } catch {} return result; }, } /* MEMORY */ export const Memory = { base_path: "tmp", get_full_path: (path: string) => Registry.join_paths(Memory.base_path, path), async init(): Promise<RegistryResult<undefined>> { await Registry.delete(Memory.base_path); return await Registry.mkdir(Memory.base_path); }, mkdir: async (path: string) => await Registry.mkdir(Memory.get_full_path(path)), ls: async (path: string) => await Registry.read(Memory.get_full_path(path)), remember: async (path: string, content: string) => await Registry.write(Memory.get_full_path(path), content), recall: async (path: string) => await Registry.read(Memory.get_full_path(path)), forget: async (path: string) => await Registry.delete(Memory.get_full_path(path)), } /* SHELL */ class ShellResult extends Result<ExitCodes, Child.ChildProcess|undefined> { cmd: string = ""; panic_message = () => `Shell: an error occured while trying to run "${this.cmd}".`; constructor(cmd: string) { super(ExitCodes.ErrUnknown, undefined); this.cmd = cmd; } } const PROCESS_TRACKING_DIR = "processes" export const Shell = { async exec(station_command: string): Promise<ShellResult> { let result = new ShellResult(station_command); //remove leading whitespaces station_command = station_command.replace(/^ /g, ""); const separator_index = station_command.indexOf(" "); let module, args; if (separator_index > -1) { module = station_command.substring(0, separator_index); args = station_command.substring(separator_index); } else { module = station_command; } /* get module command */ const cmd_result = await Registry.read(Path.join("modules", module)); if (cmd_result.has_failed) { log("ERROR", `Shell: failed to get command for "${module}".`); return result; } /* process args */ args = (args ?? "") .replace(/--/g, "\\ "); /* get full command */ const module_cmd = cmd_result.value!.split("\n")[0]; const sys_cmd = `${module_cmd}${args}`; /* spawn process */ const cp = Child.spawn(sys_cmd, { shell: true, detached: true, }); /* track */ Shell.track(sys_cmd, cp); result.code = ExitCodes.Ok; result.value = cp; return result; }, exec_sync(station_command: string): Promise<Result<ExitCodes, string>> { return new Promise(async (res) => { const result = new Result(ExitCodes.Ok, ""); /* execute */ const shell_result = (await Shell.exec(station_command)).or_log_error(); if (shell_result.has_failed) return res(result.finalize_with_code(ExitCodes.ErrUnknown)); let stdout = ""; const cp = shell_result.value!; cp.stdout?.on("data", (data) => stdout += data.toString()); cp.on("exit", () => res(result.finalize_with_value(stdout))); }); }, async track(cmd: string, cp: Child.ChildProcess) { const pid = cp.pid; /* safety */ if (pid == undefined) { cp.kill(); return; } const path = Path.join(PROCESS_TRACKING_DIR, pid.toString()); const abort = async (type: LogType) => { if (cp.killed == false) cp.kill(); log(type, `Shell: ${pid} dead`); (await Memory.forget(path)).or_log_error(); } /* create tracking directory if needed */ (await Memory.mkdir(PROCESS_TRACKING_DIR)) .err(() => abort("ERROR")); /* track process */ (await Memory.remember(path, "")) .err(() => abort("ERROR")) .ok(() => log("ACTIVITY", `Shell: started tracking process using module "${cmd.split(" ")[1]}" (${pid})`)); /* handle killing */ cp.on("exit", () => abort("STATUS")); //in case it already died if (cp.exitCode != null) abort("STATUS"); }, async kill(pid: number) { //check if process exists if ((await Memory.recall(Path.join(PROCESS_TRACKING_DIR, pid.toString()))).has_failed) return; process.kill(pid); }, parse_output(output: string) { let [code, value] = output.split("|"); value = value.split("\n")[0]; return [code, value]; } }