stabilize, add daemon state, add base build script
This commit is contained in:
parent
a1963cf491
commit
241d50e42a
12 changed files with 512 additions and 46 deletions
34
README.md
Normal file
34
README.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
# BDMGR.D TS
|
||||
|
||||
Implémentation de BDMGR daemon en typescript.
|
||||
|
||||
## Dépendances
|
||||
|
||||
Deno :
|
||||
|
||||
```sh
|
||||
curl -fsSL https://deno.land/x/install/install.sh | sh
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Démon :
|
||||
```sh
|
||||
sudo ./daemon.ts
|
||||
```
|
||||
|
||||
Controleur :
|
||||
```sh
|
||||
control.ts - controls BDMGRDTS daemon.
|
||||
Usage:
|
||||
./control.ts Command [ARG...]
|
||||
|
||||
Commands:
|
||||
help Prints a help message.
|
||||
stop Stops the daemon.
|
||||
status Get a list of enabled containers.
|
||||
create NAME BASE Create a new container from a specific base.
|
||||
enable NAME Enables a container.
|
||||
disable NAME Disables a container.
|
||||
reload NAME Reloads a container, equivalent to enable then disable.
|
||||
```
|
|
@ -1,31 +1,93 @@
|
|||
#!/bin/env -S deno run -A --unstable
|
||||
|
||||
import { daemon_send, new_cmd_disable, new_cmd_enable, new_cmd_status } from "./src/lib.ts";
|
||||
import { socket_path } from "./src/lib/paths.ts";
|
||||
import { daemon_send, new_cmd_disable, new_cmd_enable, new_cmd_status, new_cmd_stop } from "./src/lib.ts";
|
||||
import { load_all_configs, new_container_config, save_container_config } from "./src/lib/config.ts";
|
||||
import { load_base, new_container_context } from "./src/lib/create.ts";
|
||||
import { container_paths, socket_path } from "./src/lib/paths.ts";
|
||||
import { log_from, run } from "./src/lib/utils.ts";
|
||||
|
||||
await main();
|
||||
const log = log_from("control");
|
||||
|
||||
if (import.meta.main) await main();
|
||||
async function main() {
|
||||
const [kind, ...rest] = Deno.args;
|
||||
const [command, ...rest] = Deno.args;
|
||||
|
||||
if (kind === "status") {
|
||||
const res = await daemon_send(socket_path(), new_cmd_status());
|
||||
console.log({ res });
|
||||
if (["-h", "--help", "help"].includes(command)) {
|
||||
return console.log(`control.ts - controls BDMGRDTS daemon.
|
||||
Usage:
|
||||
./control.ts Command [ARG...]
|
||||
|
||||
Commands:
|
||||
help Prints a help message.
|
||||
stop Stops the daemon.
|
||||
status Get a list of enabled containers.
|
||||
create NAME BASE Create a new container from a specific base.
|
||||
enable NAME Enables a container.
|
||||
disable NAME Disables a container.
|
||||
reload NAME Reloads a container, equivalent to enable then disable.
|
||||
`);
|
||||
}
|
||||
|
||||
if (kind === "enable") {
|
||||
if (command === "create") {
|
||||
const [name, base] = rest;
|
||||
if (name === undefined) log("expects NAME argument");
|
||||
if (base === undefined) log("expects BASE argument");
|
||||
await create(name, base);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "status") {
|
||||
const res = await daemon_send(socket_path(), new_cmd_status());
|
||||
log(res);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "enable") {
|
||||
const [name] = rest;
|
||||
if (name === undefined) log("expects NAME argument");
|
||||
const res = await daemon_send(socket_path(), new_cmd_enable(name));
|
||||
console.log({ res });
|
||||
log(res);
|
||||
return;
|
||||
}
|
||||
|
||||
if (kind === "disable") {
|
||||
if (command === "disable") {
|
||||
const [name] = rest;
|
||||
if (name === undefined) log("expects NAME argument");
|
||||
const res = await daemon_send(socket_path(), new_cmd_disable(name));
|
||||
console.log({ res });
|
||||
log(res);
|
||||
return;
|
||||
}
|
||||
|
||||
if (kind === "stop") {
|
||||
const res = await daemon_send(socket_path(), new_cmd_status());
|
||||
console.log({ res });
|
||||
if (command === "reload") {
|
||||
const [name] = rest;
|
||||
if (name === undefined) log("expects NAME argument");
|
||||
const res = await daemon_send(socket_path(), new_cmd_enable(name));
|
||||
log(res);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "stop") {
|
||||
const res = await daemon_send(socket_path(), new_cmd_stop());
|
||||
log(res);
|
||||
return;
|
||||
}
|
||||
|
||||
log("unknown command", command);
|
||||
}
|
||||
|
||||
export async function create(name: string, base_name: string) {
|
||||
log("loading base", base_name);
|
||||
const base = await load_base(base_name);
|
||||
const known_containers = await load_all_configs();
|
||||
|
||||
const paths = container_paths(name);
|
||||
await Deno.mkdir(paths.base);
|
||||
const configuration = new_container_config(name);
|
||||
|
||||
log("creating the container", name, "at", paths.base);
|
||||
const context = new_container_context(paths.root, configuration, known_containers);
|
||||
await Deno.mkdir(paths.root);
|
||||
await run("sudo", "chown", "root:root", paths.root);
|
||||
await base.build(context);
|
||||
await save_container_config(configuration);
|
||||
}
|
||||
|
|
|
@ -1,50 +1,127 @@
|
|||
#!/bin/env -S deno run -A --unstable
|
||||
|
||||
import { daemon_listen, Runner, start_runner } from "./src/lib.ts";
|
||||
import { new_container_config } from "./src/lib/config.ts";
|
||||
import { socket_path } from "./src/lib/paths.ts";
|
||||
import { load_container_config } from "./src/lib/config.ts";
|
||||
import { socket_path, state_path } from "./src/lib/paths.ts";
|
||||
import { log_from, sleep } from "./src/lib/utils.ts";
|
||||
|
||||
await main();
|
||||
const log = log_from("daemon");
|
||||
|
||||
if (import.meta.main) await main();
|
||||
async function main() {
|
||||
const server = daemon_listen(socket_path());
|
||||
const enabled = new Map<string, Runner>();
|
||||
const state = await create_state();
|
||||
const server = await daemon_listen(socket_path());
|
||||
log("listening to", socket_path());
|
||||
|
||||
async function finish() {
|
||||
log("stopping");
|
||||
server.server.close();
|
||||
for (const runner of state.enabled.values()) {
|
||||
try {
|
||||
await runner.stop();
|
||||
} catch (_) { /* on s'en fou */ }
|
||||
}
|
||||
await Deno.remove(socket_path());
|
||||
for (const runner of enabled.values()) await runner.stop();
|
||||
Deno.exit(0);
|
||||
}
|
||||
Deno.addSignalListener("SIGINT", finish);
|
||||
console.log("listening to", socket_path());
|
||||
|
||||
for await (const { cmd, respond } of server) {
|
||||
console.log("received", { cmd });
|
||||
log("received", cmd);
|
||||
|
||||
if (cmd.kind === "status") {
|
||||
await respond(JSON.stringify(Array.from(enabled.keys())));
|
||||
await respond(JSON.stringify(state.list()));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cmd.kind === "enable") {
|
||||
const config = new_container_config(cmd.name); // TODO
|
||||
const runner = start_runner(config);
|
||||
enabled.set(cmd.name, runner);
|
||||
await respond("enabled");
|
||||
(async () => {
|
||||
try {
|
||||
await state.enable(cmd.name);
|
||||
await state.save();
|
||||
await respond("enabled");
|
||||
} catch (error) {
|
||||
log("experienced failure", error);
|
||||
await respond(`${error}`);
|
||||
}
|
||||
})();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cmd.kind === "disable") {
|
||||
await enabled.get(cmd.name)?.stop();
|
||||
enabled.delete(cmd.name);
|
||||
await respond("disabled");
|
||||
(async () => {
|
||||
try {
|
||||
await state.disable(cmd.name);
|
||||
await state.save();
|
||||
await respond("disabled");
|
||||
} catch (error) {
|
||||
log("experienced failure", error);
|
||||
await respond(`${error}`);
|
||||
}
|
||||
})();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cmd.kind === "reload") {
|
||||
(async () => {
|
||||
try {
|
||||
await state.disable(cmd.name);
|
||||
await sleep(500);
|
||||
await state.enable(cmd.name);
|
||||
await respond("reloaded");
|
||||
} catch (error) {
|
||||
log("experienced failure", error);
|
||||
await respond(`${error}`);
|
||||
}
|
||||
})();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cmd.kind === "stop") {
|
||||
await respond(JSON.stringify("stopping, ++"));
|
||||
await state.save();
|
||||
await respond("adios");
|
||||
await finish();
|
||||
}
|
||||
|
||||
await respond("unknown");
|
||||
await respond("unknown command");
|
||||
}
|
||||
}
|
||||
|
||||
async function create_state() {
|
||||
const self = {
|
||||
enabled: new Map<string, Runner>(),
|
||||
async enable(name: string) {
|
||||
if (self.enabled.has(name)) throw new Error("Container already enabled");
|
||||
log("loading container", name);
|
||||
const config = await load_container_config(name); // TODO
|
||||
if (config === null) throw new Error("can't read config");
|
||||
log("starting container", name);
|
||||
self.enabled.set(name, await start_runner(config));
|
||||
log("container", name, "started");
|
||||
},
|
||||
async disable(name: string) {
|
||||
const container = self.enabled.get(name);
|
||||
if (container === undefined) throw new Error("Container not found");
|
||||
await container.stop();
|
||||
self.enabled.delete(name);
|
||||
},
|
||||
list() {
|
||||
return Array.from(self.enabled.keys());
|
||||
},
|
||||
async save() {
|
||||
const content = JSON.stringify({ enabled: self.list() });
|
||||
await Deno.writeTextFile(state_path(), content);
|
||||
},
|
||||
};
|
||||
try {
|
||||
log("trying to recover state from", state_path());
|
||||
const loaded = JSON.parse(await Deno.readTextFile(state_path()));
|
||||
for (const name of loaded.enabled ?? []) {
|
||||
await self.enable(name);
|
||||
}
|
||||
log("successfully loaded from", state_path());
|
||||
} catch (error) {
|
||||
log("experienced failure", error);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
|
26
instance/local/bases/arch/module.ts
Normal file
26
instance/local/bases/arch/module.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { Base, BaseContext } from "../../../src/lib.ts";
|
||||
|
||||
export { build };
|
||||
|
||||
const build: Base = async (c) => {
|
||||
// installs base
|
||||
await c.sh(`pacstrap -K -c '${c.root_dir}' base`);
|
||||
await c.sh(`rm -fr '${c.root_dir}/etc/securetty' '${c.root_dir}/usr/share/factory/etc/securetty'`);
|
||||
|
||||
await c.sh_in(`printf 'root:noussommesdescrabes' | chpasswd --crypt-method DES`);
|
||||
await pacman(c, "nano", "git", "base-devel");
|
||||
await c.sh_in(`systemctl enable systemd-networkd.service`);
|
||||
|
||||
// ssh
|
||||
await pacman(c, "openssh");
|
||||
await c.sh_in(`
|
||||
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config
|
||||
systemctl enable sshd.service
|
||||
`);
|
||||
const port = c.available_port(2250);
|
||||
c.redirect(port, 22);
|
||||
};
|
||||
|
||||
async function pacman(c: BaseContext, ...packages: string[]) {
|
||||
await c.sh_in(`pacman -Syu --noconfirm '${packages.join("' '")}'`);
|
||||
}
|
14
instance/src/bin/proxy.ts
Normal file → Executable file
14
instance/src/bin/proxy.ts
Normal file → Executable file
|
@ -1,11 +1,15 @@
|
|||
#!/bin/env -S deno run -A --unstable
|
||||
|
||||
await main();
|
||||
if (import.meta.main) await main();
|
||||
async function main() {
|
||||
const [from_port, to_ip, to_port] = Deno.args;
|
||||
if (to_port === undefined) Deno.exit(2);
|
||||
if (to_port === undefined) {
|
||||
console.error("[proxy] usage: ./proxy.ts <from_port> <to_ip> <to_port>");
|
||||
Deno.exit(2);
|
||||
}
|
||||
|
||||
const server = Deno.listen({ transport: "tcp", port: parseInt(from_port) });
|
||||
console.log("[proxy] listening on port", from_port, "redirecting to", to_ip, "port", to_port);
|
||||
|
||||
for await (const connection of server) {
|
||||
try {
|
||||
|
@ -16,3 +20,9 @@ async function main() {
|
|||
} catch (_) { /* isok */ }
|
||||
}
|
||||
}
|
||||
|
||||
export function proxy_command(from_port: number, to_ip: string, to_port: number) {
|
||||
const path = new URL("", import.meta.url).pathname;
|
||||
const args = [from_port, to_ip, to_port].map((v) => v.toString());
|
||||
return new Deno.Command(path, { args });
|
||||
}
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
export type { Base, BaseContext } from "./lib/create.ts";
|
||||
|
||||
import { toText } from "https://deno.land/std@0.208.0/streams/to_text.ts";
|
||||
import { lines } from "./lib/utils.ts";
|
||||
import { lines, log_from, loop_process, LoopProcess, run, sleep } from "./lib/utils.ts";
|
||||
import { ContainerConfig } from "./lib/config.ts";
|
||||
import { container_paths } from "./lib/paths.ts";
|
||||
import { container_command, get_container_addresses } from "./lib/nspawn.ts";
|
||||
import { proxy_command } from "./bin/proxy.ts";
|
||||
|
||||
const log = log_from("lib");
|
||||
|
||||
export type CmdStatus = ReturnType<typeof new_cmd_status>;
|
||||
export function new_cmd_status() {
|
||||
|
@ -17,15 +24,22 @@ export function new_cmd_disable(name: string) {
|
|||
return { kind: "disable" as const, name };
|
||||
}
|
||||
|
||||
export type CmdReload = ReturnType<typeof new_cmd_reload>;
|
||||
export function new_cmd_reload(name: string) {
|
||||
return { kind: "reload" as const, name };
|
||||
}
|
||||
|
||||
export type CmdStop = ReturnType<typeof new_cmd_stop>;
|
||||
export function new_cmd_stop() {
|
||||
return { kind: "stop" as const };
|
||||
}
|
||||
|
||||
export type Cmd = CmdStatus | CmdEnable | CmdDisable | CmdStop;
|
||||
export type Cmd = CmdStatus | CmdEnable | CmdDisable | CmdReload | CmdStop;
|
||||
|
||||
export function daemon_listen(sock_path: string) {
|
||||
export async function daemon_listen(sock_path: string) {
|
||||
const server = Deno.listen({ transport: "unix", path: sock_path });
|
||||
await Deno.chmod(sock_path, 0o775);
|
||||
await run("chgrp", "wheel", sock_path);
|
||||
const generator = async function* () {
|
||||
for await (const request of server) {
|
||||
const respond = async (message: string) => {
|
||||
|
@ -53,12 +67,52 @@ export async function daemon_send(sock_path: string, command: Cmd) {
|
|||
return await toText(request.readable);
|
||||
}
|
||||
|
||||
export type Runner = ReturnType<typeof start_runner>;
|
||||
export type Runner = Awaited<ReturnType<typeof start_runner>>;
|
||||
export function start_runner(config: ContainerConfig) {
|
||||
const { name } = config;
|
||||
const paths = container_paths(name);
|
||||
const command = container_command(name, paths.root, {
|
||||
boot: true,
|
||||
veth: true,
|
||||
cmd_opts: {
|
||||
stdin: "null",
|
||||
stdout: "null",
|
||||
},
|
||||
});
|
||||
const proxies = [] as LoopProcess[];
|
||||
const container_loop = loop_process(command, {
|
||||
on_start: () => {
|
||||
log("container", name, "started");
|
||||
(async () => {
|
||||
await sleep(1_000);
|
||||
const [address] = await get_container_addresses(name);
|
||||
proxies.push(...start_proxies(address, config.redirects));
|
||||
})();
|
||||
},
|
||||
on_stop: async () => {
|
||||
log("container", name, "stopped");
|
||||
for (const p of proxies) await p.kill();
|
||||
await sleep(500);
|
||||
proxies.splice(0, proxies.length);
|
||||
},
|
||||
});
|
||||
return {
|
||||
name: config.name,
|
||||
name,
|
||||
config,
|
||||
container_loop,
|
||||
stop: async () => {
|
||||
// TODO
|
||||
for (const p of proxies) await p.kill();
|
||||
await container_loop.kill();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function start_proxies(address: string, redirects: [number, number][]) {
|
||||
const redirections = redirects
|
||||
.map(([from_port, to_port]) => {
|
||||
return loop_process(proxy_command(from_port, address, to_port), {
|
||||
on_start: () => console.log("starting proxy", from_port, "to", address, "port", to_port),
|
||||
});
|
||||
});
|
||||
return redirections;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { container_paths } from "./paths.ts";
|
||||
import { exists } from "./utils.ts";
|
||||
import { container_paths, containers_path } from "./paths.ts";
|
||||
import { exists, log_from } from "./utils.ts";
|
||||
|
||||
const log = log_from("config");
|
||||
|
||||
export type ContainerConfig = ReturnType<typeof new_container_config>;
|
||||
export function new_container_config(name: string) {
|
||||
|
@ -11,11 +13,27 @@ export function new_container_config(name: string) {
|
|||
}
|
||||
|
||||
export async function load_container_config(name: string) {
|
||||
const config_path = container_paths(name).config;
|
||||
const config_path = container_paths(name).configuration;
|
||||
log("loading config for", name);
|
||||
if (!exists(config_path)) return null;
|
||||
const content = await Deno.readTextFile(config_path);
|
||||
const read = JSON.parse(content);
|
||||
const default_ = new_container_config(name);
|
||||
if (read.version < default_.version) throw new Error("read conf version is outdated");
|
||||
return { ...default_, ...read };
|
||||
return { ...default_, ...read } as ContainerConfig;
|
||||
}
|
||||
|
||||
export async function save_container_config(config: ContainerConfig) {
|
||||
log("saving config of", config.name);
|
||||
const config_path = container_paths(config.name).configuration;
|
||||
const serialized = JSON.stringify(config, null, 4);
|
||||
await Deno.writeTextFile(config_path, serialized);
|
||||
}
|
||||
|
||||
export async function load_all_configs() {
|
||||
const names = Array.from(Deno.readDirSync(containers_path()))
|
||||
.filter((e) => e.isDirectory)
|
||||
.map((e) => e.name);
|
||||
const results = await Promise.all(names.map((name) => load_container_config(name)));
|
||||
return results.filter((item) => item != null) as ContainerConfig[];
|
||||
}
|
||||
|
|
69
instance/src/lib/create.ts
Normal file
69
instance/src/lib/create.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
// import { blob } from "https://deno.land/std@0.208.0/streams/mod.ts"
|
||||
|
||||
import { ContainerConfig } from "./config.ts";
|
||||
import { base_paths } from "./paths.ts";
|
||||
import { lines, log_from } from "./utils.ts";
|
||||
|
||||
export type Base = (context: BaseContext) => unknown;
|
||||
const log = log_from("BUILDING");
|
||||
|
||||
export type BaseContext = ReturnType<typeof new_container_context>;
|
||||
export function new_container_context(root_dir: string, config: ContainerConfig, known_containers: ContainerConfig[]) {
|
||||
return {
|
||||
root_dir,
|
||||
config,
|
||||
sh: async (script: string) => {
|
||||
log(`running '${script.replace("\n", " ").slice(0, 20)}...'`);
|
||||
const process = new Deno.Command("sudo", {
|
||||
args: ["sh", "-c", "set -e;" + script],
|
||||
stdin: "null",
|
||||
stdout: "piped",
|
||||
}).spawn();
|
||||
log_lines_prefixed(" | ", process.stdout);
|
||||
const res = await process.status;
|
||||
if (!res.success) throw new Error("process failed");
|
||||
},
|
||||
sh_in: async (script: string) => {
|
||||
log(`inside '${root_dir}' running '${script.replace("\n", " ").slice(0, 20)}...'`);
|
||||
const process = new Deno.Command("sudo", {
|
||||
args: ["systemd-nspawn", `--directory=${root_dir}`, "-P"],
|
||||
stdin: "piped",
|
||||
stdout: "piped",
|
||||
}).spawn();
|
||||
write_all(process.stdin, "set -e;" + script + "\nexit 0\n");
|
||||
log_lines_prefixed(" | ", process.stdout);
|
||||
const res = await process.status;
|
||||
if (!res.success) throw new Error("process failed");
|
||||
},
|
||||
redirect: (from: number, to: number) => {
|
||||
log("redirects", from, "to", to);
|
||||
config.redirects.push([from, to]);
|
||||
},
|
||||
available_port: (target: number) => {
|
||||
const used_by_known_containers = known_containers.map((c) => c.redirects[0]).flat();
|
||||
const used_by_self = config.redirects.map((r) => r[0]);
|
||||
const used = [...used_by_known_containers, ...used_by_self];
|
||||
let result = target;
|
||||
while (used.includes(result)) result += 1;
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function log_lines_prefixed(prefix: string, stream: ReadableStream<Uint8Array>) {
|
||||
for await (const line of lines(stream)) {
|
||||
console.log(prefix + line);
|
||||
}
|
||||
}
|
||||
|
||||
async function write_all(stream: WritableStream<Uint8Array>, text: string) {
|
||||
await stream.getWriter().write(new TextEncoder().encode(text));
|
||||
}
|
||||
|
||||
export async function load_base(name: string) {
|
||||
const paths = base_paths(name);
|
||||
const module = await import(paths.module);
|
||||
const build = module["build"] as (context: BaseContext) => unknown;
|
||||
if (build === undefined) throw new Error("module does not contain a build procedure");
|
||||
return { build };
|
||||
}
|
|
@ -1 +1,37 @@
|
|||
// wrapper
|
||||
|
||||
import { log_from, sleep } from "./utils.ts";
|
||||
|
||||
const log = log_from("nspawn");
|
||||
|
||||
export function container_command(name: string, directory: string, opts?: {
|
||||
veth?: boolean;
|
||||
boot?: boolean;
|
||||
cmd_opts?: Deno.CommandOptions;
|
||||
}) {
|
||||
const args = [
|
||||
`--machine=${name}`,
|
||||
`--directory=${directory}`,
|
||||
];
|
||||
if (opts?.veth ?? false) args.push("--network-veth");
|
||||
if (opts?.boot ?? false) args.push("--boot");
|
||||
const command = new Deno.Command("systemd-nspawn", { ...opts?.cmd_opts, args });
|
||||
return command;
|
||||
}
|
||||
|
||||
export async function get_container_addresses(name: string) {
|
||||
const command = new Deno.Command("machinectl", { args: ["status", name], stdout: "piped" });
|
||||
while (true) {
|
||||
await sleep(500);
|
||||
log("getting address of", name);
|
||||
const { code, stdout } = await command.output();
|
||||
if (code != 0) continue;
|
||||
const output = new TextDecoder().decode(stdout);
|
||||
const [_, rest] = output.split(" Address: ");
|
||||
if (rest === undefined) continue;
|
||||
const [raw] = rest.split(" OS: ");
|
||||
const addresses = raw.trim().split("\n").map((l) => l.trim());
|
||||
if (addresses.length === 0) continue;
|
||||
return addresses;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,21 @@ export function containers_path() {
|
|||
|
||||
export function container_paths(name: string) {
|
||||
const base = containers_path() + "/" + name;
|
||||
const conf = base + "/config.json";
|
||||
const configuration = base + "/config.json";
|
||||
const root = base + "/root";
|
||||
return { base, configuration: conf, root };
|
||||
return { base, configuration, root };
|
||||
}
|
||||
|
||||
export function state_path() {
|
||||
return instance_root_path() + "/local/state.json";
|
||||
}
|
||||
|
||||
export function bases_path() {
|
||||
return instance_root_path() + "/local/bases";
|
||||
}
|
||||
|
||||
export function base_paths(name: string) {
|
||||
const base = bases_path() + "/" + name;
|
||||
const module = base + "/module.ts";
|
||||
return { base, module };
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { TextLineStream } from "https://deno.land/std@0.208.0/streams/mod.ts";
|
||||
|
||||
export type Channel<T> = ReturnType<typeof channel<T>>;
|
||||
export function channel<T>() {
|
||||
const inner = {
|
||||
queue: [] as T[],
|
||||
|
@ -41,3 +42,54 @@ export function lines(readable: ReadableStream<Uint8Array>) {
|
|||
.pipeThrough(new TextDecoderStream())
|
||||
.pipeThrough(new TextLineStream());
|
||||
}
|
||||
|
||||
const async_noop = async () => {};
|
||||
export type LoopProcess = ReturnType<typeof loop_process>;
|
||||
export function loop_process(
|
||||
command: Deno.Command,
|
||||
opts?: { delay?: number; on_start?: () => unknown; on_stop?: () => unknown },
|
||||
) {
|
||||
const events = {
|
||||
on_start: opts?.on_start ?? async_noop,
|
||||
on_stop: opts?.on_stop ?? async_noop,
|
||||
};
|
||||
const kill_sig = channel<"kill">();
|
||||
async function launch() {
|
||||
while (true) {
|
||||
await events.on_start();
|
||||
const child_process = command.spawn();
|
||||
const result = await Promise.any([kill_sig.receive(), child_process.output()]);
|
||||
if (result === "kill") {
|
||||
await events.on_stop();
|
||||
child_process.kill();
|
||||
break;
|
||||
}
|
||||
await events.on_stop();
|
||||
await sleep(opts?.delay ?? 500);
|
||||
}
|
||||
}
|
||||
const looping = launch();
|
||||
return {
|
||||
kill: async () => {
|
||||
kill_sig.send("kill"), await looping;
|
||||
},
|
||||
events,
|
||||
looping,
|
||||
};
|
||||
}
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolver) => setTimeout(resolver, ms));
|
||||
}
|
||||
|
||||
export async function run(command: string, ...args: string[]) {
|
||||
const { code } = await new Deno.Command(command, { args }).output();
|
||||
if (code != 0) throw new Error(`command ${command} failed`);
|
||||
}
|
||||
|
||||
export function log_from(...prefixes: string[]) {
|
||||
const prefix = prefixes.map((p) => `[${p}]`).join("").padStart(10);
|
||||
return function (...args: unknown[]) {
|
||||
console.log(prefix, ...args);
|
||||
};
|
||||
}
|
||||
|
|
14
sync.sh
Executable file
14
sync.sh
Executable file
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
cd "$(dirname "$(realpath "$0")")"
|
||||
|
||||
|
||||
if [ $# -lt 1 ]
|
||||
then exit 2
|
||||
fi
|
||||
|
||||
|
||||
DEST="$1"
|
||||
|
||||
|
||||
rsync -avzhP ./instance "$DEST"
|
Loading…
Add table
Add a link
Reference in a new issue