@the-stations-project/sdk
Version:
SDK for developing Station Services.
367 lines (366 loc) • 13.6 kB
JavaScript
;
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];
}
};