diff --git a/deployer/index.ts b/deployer/index.ts index 2cae01c..ab0b00c 100644 --- a/deployer/index.ts +++ b/deployer/index.ts @@ -1,9 +1,10 @@ import { ssh, SSH } from './ssh.js' -import { Expression, BuiltOutput } from './nix.js' +import { Expression } from './nix.js' +type Command = () => Promise class Cmd { - registry: Record Promise> = {}; - register(obj: string, action: string, fn: () => Promise) { + registry: Record = {}; + register(obj: string, action: string, fn: Command) { this.registry[`${obj}.${action}`] = fn } registerAll(obj: string, intf: Object & { _commands?: string[] }) { @@ -43,10 +44,11 @@ class Machine { name: string; hasHome: boolean = false; hostname?: string; - constructor(args: {name?: string, hasHome?: boolean, hostname?: string}) { - Object.assign(this, args) - // todo - this.name = "" + constructor({name = "", hasHome = false, hostname}: {name?: string, hasHome?: boolean, hostname?: string}) { + this.name = name + this.hasHome = hasHome + this.hostname = hostname + // name can be set later } isLocal() { return os.hostname() == this.name @@ -93,7 +95,7 @@ const machines = { for (const [name, machine] of Object.entries(machines)) machine.name = name -function cmd(target: { _commands?: string[] }, propertyKey: string, descriptor: PropertyDescriptor): void { +function cmd(target: { _commands?: string[] }, propertyKey: string, _descriptor: PropertyDescriptor): void { if (target._commands == undefined) target._commands = [] target._commands.push(propertyKey) } diff --git a/deployer/ssh.ts b/deployer/ssh.ts index 95fee51..0ffcf9f 100644 --- a/deployer/ssh.ts +++ b/deployer/ssh.ts @@ -1,10 +1,30 @@ import 'zx/globals'; -import { retry } from 'zx/experimental' -import { spawn, SpawnOptions, ChildProcess } from 'child_process' +import { spawn, ChildProcess } from 'child_process' import { Socket } from 'node:net' +async function promiseWithTimeout(timeoutMs: number, promise: Promise, failureMessage?: string) { + let timeoutHandle: NodeJS.Timeout | null = null + const timeoutPromise = new Promise((_resolve, reject) => { + timeoutHandle = setTimeout(() => reject(new Error(failureMessage)), timeoutMs) + }) + const result = await Promise.race([promise, timeoutPromise]) + if (timeoutHandle) clearTimeout(timeoutHandle) + return result +} + +const to_kill: ProcessPromise[] = [] + +// sad that I can't chain these +process.on("uncaughtException", err => { + console.error("uncaught exception", err) + console.log("killing", to_kill.length, "child processes") + for (const k of to_kill) if (k.child?.pid) process.kill(k.child.pid) + to_kill.length = 0 + process.exit(1) +}) + export function ssh(host: string): Promise export function ssh(host: string, cb: () => Promise): Promise export async function ssh(host: string, cb?: () => Promise) { @@ -14,10 +34,14 @@ export async function ssh(host: string, cb?: () => Promise) { } catch (p) { if (p instanceof ProcessOutput && p.exitCode == 255) { console.log("Spawning ssh master") - let x = $`ssh ${host} -M -N -o compression=no` - await sleep(1000) - await retry(20, '1s', () => $`ssh ${host} -O check`.quiet()) - console.log("SSH connected") + const x = $`ssh ${host} -M -N -o Compression=no -o PermitLocalCommand=yes -o LocalCommand="echo connected"` + to_kill.push(x) + await promiseWithTimeout(60000, new Promise((resolve, _reject) => { + x.stdout.on('data', (d: Buffer) => { + if (d.toString('utf8').trim() == "connected") + resolve() + }) + })) if (x.child) { x.child.unref() if (x.child.stderr instanceof Socket) x.child.stderr.unref() @@ -42,6 +66,9 @@ export class SSH { constructor(public host: string) { } within(cb: () => Promise): Promise { return within(async () => { + // todo: the default options.shell is set to local which.sync('bash') + // which doesn't neccesarily work on the remote + $.shell = "bash" $.spawn = (command: string, options: any): any => { const stdio = ["pipe", options.stdio[1], options.stdio[2]] const proc: ChildProcess = spawn("ssh", [this.host, options.shell],