UNPKG

@the-stations-project/sdk

Version:

SDK for developing Station Services.

367 lines (366 loc) 13.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Shell = exports.Memory = exports.Registry = exports.RegistryExitCodes = exports.log = exports.start_module = exports.contains_undefined_arguments = exports.Result = exports.ExitCodes = void 0; const child_process_1 = __importDefault(require("child_process")); const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); /* BASIC */ var ExitCodes; (function (ExitCodes) { ExitCodes[ExitCodes["Ok"] = 0] = "Ok"; ExitCodes[ExitCodes["ErrUnknown"] = 1] = "ErrUnknown"; ExitCodes[ExitCodes["ErrNoCommand"] = 2] = "ErrNoCommand"; ExitCodes[ExitCodes["ErrMissingParameter"] = 3] = "ErrMissingParameter"; })(ExitCodes = exports.ExitCodes || (exports.ExitCodes = {})); class Result { code; value; initial_code; initial_value; log_message; panic_message = () => "Unknown: invalid result."; constructor(code, value) { this.initial_code = this.code = code; this.initial_value = this.value = value; } err(cb) { if (this.has_failed) { cb(this); } return this; } ok(cb) { if (!this.has_failed) { cb(this); } return this; } or_panic(msg) { if (this.has_failed) { console.trace(this); const message = msg ?? this.panic_message(); log("PANIC", message); throw message; } return this; } or_log_error(msg) { if (this.has_failed) { log("ERROR", msg ?? this.panic_message()); } return this; } to_string() { return `${this.code}|${this.value}`; } finalize(code, value) { this.code = code; this.value = value; if (this.log_message) log("ACTIVITY", this.log_message()); return this; } finalize_with_value(value) { this.value = value; return this.finalize(this.code, value); } finalize_with_code(code) { this.code = code; return this.finalize(code, this.value); } revert() { return this.finalize(this.initial_code, this.initial_value); } get has_failed() { return this.code > 0; } } exports.Result = Result; function contains_undefined_arguments(args) { return [...args].filter(x => x == undefined).length != 0; } exports.contains_undefined_arguments = contains_undefined_arguments; /* CLI */ async function start_module(main, cb) { 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); } exports.start_module = start_module; const LOG_DIR = "logs/current"; async function log(type, msg) { msg = `TYPE ${type}\nPID ${process.pid}\n${msg}\n\n`; /* get filename */ const timestamp = new Date().toISOString(); const filename = `log-${timestamp}`; const path = path_1.default.join(LOG_DIR, filename); (await exports.Registry.append(path, msg)).or_panic("failed to log"); } exports.log = log; /* REGISTRY */ var RegistryExitCodes; (function (RegistryExitCodes) { RegistryExitCodes[RegistryExitCodes["OkUnchanged"] = -1] = "OkUnchanged"; RegistryExitCodes[RegistryExitCodes["Ok"] = 0] = "Ok"; RegistryExitCodes[RegistryExitCodes["ErrUnknown"] = 1] = "ErrUnknown"; RegistryExitCodes[RegistryExitCodes["ErrRead"] = 2] = "ErrRead"; RegistryExitCodes[RegistryExitCodes["ErrWrite"] = 3] = "ErrWrite"; RegistryExitCodes[RegistryExitCodes["ErrDel"] = 4] = "ErrDel"; })(RegistryExitCodes = exports.RegistryExitCodes || (exports.RegistryExitCodes = {})); class RegistryResult extends Result { constructor() { super(RegistryExitCodes.ErrUnknown, undefined); } } exports.Registry = { base_path: "registry", get_full_path: (path) => exports.Registry.join_paths(exports.Registry.base_path, path), get_panic_message: (msg) => `Registry: ${msg}.`, join_paths(...args) { return path_1.default.join(...args .filter(x => x) .map(x => x.split("/")) .flat()); }, async mkdir(path) { const full_path = exports.Registry.get_full_path(path); const result = new RegistryResult(); result.panic_message = () => exports.Registry.get_panic_message(`failed to create directory "${path}"`); try { await promises_1.default.mkdir(full_path, { recursive: true }); result.finalize_with_code(RegistryExitCodes.Ok); } catch { (await exports.Registry.test(full_path)) .ok(() => result.finalize_with_code(RegistryExitCodes.OkUnchanged)) .err(() => result.finalize_with_code(RegistryExitCodes.ErrUnknown)); } return result; }, async write(path, content) { const full_path = exports.Registry.get_full_path(path); const result = new RegistryResult(); result.panic_message = () => exports.Registry.get_panic_message(`failed to write file "${path}"`); try { await promises_1.default.writeFile(full_path, content); result.code = RegistryExitCodes.Ok; } catch { result.code = RegistryExitCodes.ErrWrite; } return result; }, async append(path, content) { const full_path = exports.Registry.get_full_path(path); const result = new RegistryResult(); result.panic_message = () => exports.Registry.get_panic_message(`failed to append to file "${path}"`); try { await promises_1.default.appendFile(full_path, content); result.finalize_with_code(RegistryExitCodes.Ok); } catch { result.finalize_with_code(RegistryExitCodes.ErrWrite); } return result; }, async read(path) { const full_path = exports.Registry.get_full_path(path); const result = new RegistryResult(); result.panic_message = () => exports.Registry.get_panic_message(`failed to read file "${path}"`); try { const text = await promises_1.default.readFile(full_path, { encoding: "utf8" }); result.code = RegistryExitCodes.Ok; result.value = text; } catch { result.code = RegistryExitCodes.ErrRead; } return result; }, async ls(path) { const full_path = exports.Registry.get_full_path(path); const result = new RegistryResult(); result.panic_message = () => exports.Registry.get_panic_message(`failed to read directory "${path}"`); try { const items = await promises_1.default.readdir(full_path); result.finalize(RegistryExitCodes.Ok, items); } catch { result.finalize_with_code(RegistryExitCodes.ErrRead); } return result; }, async delete(path) { const full_path = exports.Registry.get_full_path(path); const result = new RegistryResult(); result.panic_message = () => exports.Registry.get_panic_message(`failed to delete item "${path}"`); try { await promises_1.default.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, default_value) { const read_result = await exports.Registry.read(path); if (!read_result.has_failed) return read_result; const write_result = await exports.Registry.write(path, default_value); return write_result; }, async test(path) { const full_path = exports.Registry.get_full_path(path); const result = new RegistryResult(); try { await promises_1.default.stat(full_path); result.finalize_with_code(RegistryExitCodes.Ok); } catch { } return result; }, async copy(src, dest) { const full_src = exports.Registry.get_full_path(src); const full_dest = exports.Registry.get_full_path(dest); const result = new RegistryResult(); try { await promises_1.default.cp(full_src, full_dest, { recursive: true }); result.finalize_with_code(RegistryExitCodes.Ok); } catch { } return result; }, async move(src, dest) { const full_src = exports.Registry.get_full_path(src); const full_dest = exports.Registry.get_full_path(dest); const result = new RegistryResult(); try { await promises_1.default.rename(full_src, full_dest); result.finalize_with_code(RegistryExitCodes.Ok); } catch { } return result; }, }; /* MEMORY */ exports.Memory = { base_path: "tmp", get_full_path: (path) => exports.Registry.join_paths(exports.Memory.base_path, path), async init() { await exports.Registry.delete(exports.Memory.base_path); return await exports.Registry.mkdir(exports.Memory.base_path); }, mkdir: async (path) => await exports.Registry.mkdir(exports.Memory.get_full_path(path)), ls: async (path) => await exports.Registry.read(exports.Memory.get_full_path(path)), remember: async (path, content) => await exports.Registry.write(exports.Memory.get_full_path(path), content), recall: async (path) => await exports.Registry.read(exports.Memory.get_full_path(path)), forget: async (path) => await exports.Registry.delete(exports.Memory.get_full_path(path)), }; /* SHELL */ class ShellResult extends Result { cmd = ""; panic_message = () => `Shell: an error occured while trying to run "${this.cmd}".`; constructor(cmd) { super(ExitCodes.ErrUnknown, undefined); this.cmd = cmd; } } const PROCESS_TRACKING_DIR = "processes"; exports.Shell = { async exec(station_command) { 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 exports.Registry.read(path_1.default.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_process_1.default.spawn(sys_cmd, { shell: true, detached: true, }); /* track */ exports.Shell.track(sys_cmd, cp); result.code = ExitCodes.Ok; result.value = cp; return result; }, exec_sync(station_command) { return new Promise(async (res) => { const result = new Result(ExitCodes.Ok, ""); /* execute */ const shell_result = (await exports.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, cp) { const pid = cp.pid; /* safety */ if (pid == undefined) { cp.kill(); return; } const path = path_1.default.join(PROCESS_TRACKING_DIR, pid.toString()); const abort = async (type) => { if (cp.killed == false) cp.kill(); log(type, `Shell: ${pid} dead`); (await exports.Memory.forget(path)).or_log_error(); }; /* create tracking directory if needed */ (await exports.Memory.mkdir(PROCESS_TRACKING_DIR)) .err(() => abort("ERROR")); /* track process */ (await exports.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) { //check if process exists if ((await exports.Memory.recall(path_1.default.join(PROCESS_TRACKING_DIR, pid.toString()))).has_failed) return; process.kill(pid); }, parse_output(output) { let [code, value] = output.split("|"); value = value.split("\n")[0]; return [code, value]; } };