import * as T from "../../../base/lib/types" import { asError } from "../../../base/lib/util/asError" import { Drop } from "../util" import { ExecSpawnable, SubContainer } from "../util/SubContainer" import { CommandController } from "./CommandController" const TIMEOUT_INCREMENT_MS = 1000 const MAX_TIMEOUT_MS = 30000 /** * This is a wrapper around CommandController that has a state of off, where the command shouldn't be running * and the others state of running, where it will keep a living running command */ export class Daemon extends Drop { private commandController: CommandController | null = null private shouldBeRunning = false protected exitedSuccess = false protected constructor( private startCommand: () => Promise>, readonly oneshot: boolean = false, protected onExitSuccessFns: (() => void)[] = [], ) { super() } get subContainerHandle(): undefined | ExecSpawnable { return this.commandController?.subContainerHandle } static of() { return async ( effects: T.Effects, subcontainer: SubContainer, command: T.CommandType, options: { runAsInit?: boolean env?: | { [variable: string]: string } | undefined cwd?: string | undefined user?: string | undefined onStdout?: (chunk: Buffer | string | any) => void onStderr?: (chunk: Buffer | string | any) => void sigtermTimeout?: number }, ) => { const startCommand = () => CommandController.of()( effects, subcontainer, command, options, ) return new Daemon(startCommand) } } async start() { if (this.commandController) { return } this.shouldBeRunning = true this.exitedSuccess = false let timeoutCounter = 0 ;(async () => { while (this.shouldBeRunning) { if (this.commandController) await this.commandController .term({ keepSubcontainer: true }) .catch((err) => console.error(err)) this.commandController = await this.startCommand() if ( this.oneshot && (await this.commandController.wait({ keepSubcontainer: true }).then( (_) => true, (err) => { console.error(err) return false }, )) ) { for (const fn of this.onExitSuccessFns) { try { fn() } catch (e) { console.error("EXIT_SUCCESS handler", e) } } this.onExitSuccessFns = [] this.exitedSuccess = true break } await new Promise((resolve) => setTimeout(resolve, timeoutCounter)) timeoutCounter += TIMEOUT_INCREMENT_MS timeoutCounter = Math.min(MAX_TIMEOUT_MS, timeoutCounter) } })().catch((err) => { console.error(asError(err)) }) } async term(termOptions?: { signal?: NodeJS.Signals | undefined timeout?: number | undefined }) { return this.stop(termOptions) } async stop(termOptions?: { signal?: NodeJS.Signals | undefined timeout?: number | undefined }) { this.shouldBeRunning = false await this.commandController ?.term({ ...termOptions }) .catch((e) => console.error(asError(e))) this.commandController = null } onDrop(): void { this.stop().catch((e) => console.error(asError(e))) } }