"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Daemon = void 0; const asError_1 = require("../../../base/lib/util/asError"); const logErrorOnce_1 = require("../../../base/lib/util/logErrorOnce"); const util_1 = require("../util"); const CommandController_1 = require("./CommandController"); const TIMEOUT_INCREMENT_MS = 1000; const MAX_TIMEOUT_MS = 30000; /** * A managed long-running process wrapper around {@link CommandController}. * * When started, the daemon automatically restarts its underlying command on failure * with exponential backoff (up to 30 seconds). When stopped, the command is terminated * gracefully. Implements {@link Drop} for automatic cleanup when the context is left. * * @typeParam Manifest - The service manifest type * @typeParam C - The subcontainer type, or `null` for JS-only daemons */ class Daemon extends util_1.Drop { constructor(subcontainer, startCommand, oneshot = false) { super(); this.subcontainer = subcontainer; this.startCommand = startCommand; this.oneshot = oneshot; this.commandController = null; this.exitedSuccess = false; this.onExitFns = []; this.loop = null; this._managed = false; } /** Returns true if this daemon is a one-shot process (exits after success) */ isOneshot() { return this.oneshot; } /** * Factory method to create a new Daemon. * * Returns a curried function: `(effects, subcontainer, exec) => Daemon`. * Registers an `onLeaveContext` callback that terminates the daemon when the * effects context is left. */ static of() { return (effects, subcontainer, exec) => { let subc = subcontainer; if (subcontainer && subcontainer.isOwned()) subc = subcontainer.rc(); const startCommand = () => CommandController_1.CommandController.of()(effects, (subc?.rc() ?? null), exec); const res = new Daemon(subc, startCommand); effects.onLeaveContext(() => { if (!res._managed) { res.term({ destroySubcontainer: true }).catch((e) => (0, logErrorOnce_1.logErrorOnce)(e)); } }); return res; }; } /** * Start the daemon. If it is already running, this is a no-op. * * The daemon will automatically restart on failure with increasing backoff * until {@link term} is called. */ async start() { if (this.loop) { return; } const abort = new AbortController(); const done = this.runLoop(abort.signal); this.loop = { abort, done }; } async runLoop(signal) { let timeoutCounter = 0; try { while (!signal.aborted) { if (this.commandController) { await this.commandController.term({}).catch(logErrorOnce_1.logErrorOnce); this.commandController = null; } try { this.commandController = await this.startCommand(); if (signal.aborted) { await this.commandController.term({}).catch(logErrorOnce_1.logErrorOnce); this.commandController = null; break; } const success = await this.commandController.wait().then((_) => true, (err) => { if (!signal.aborted) (0, logErrorOnce_1.logErrorOnce)(err); return false; }); this.commandController = null; if (signal.aborted) break; for (const fn of this.onExitFns) { try { fn(success); } catch (e) { console.error('EXIT handler', e); } } if (success && this.oneshot) { this.exitedSuccess = true; break; } } catch (e) { if (!signal.aborted) console.error(e); } if (signal.aborted) break; await new Promise((resolve) => { const timer = setTimeout(resolve, timeoutCounter); signal.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true }); }); timeoutCounter += TIMEOUT_INCREMENT_MS; timeoutCounter = Math.min(MAX_TIMEOUT_MS, timeoutCounter); } } finally { this.loop = null; } } /** * Terminate the daemon, stopping its underlying command. * * Sends the configured signal (default SIGTERM) and waits for the process to exit. * Optionally destroys the subcontainer after termination. * * @param termOptions - Optional termination settings * @param termOptions.signal - The signal to send (default: SIGTERM) * @param termOptions.timeout - Milliseconds to wait before SIGKILL * @param termOptions.destroySubcontainer - Whether to destroy the subcontainer after exit */ async term(termOptions) { this.exitedSuccess = false; this.onExitFns = []; if (this.loop) { this.loop.abort.abort(); } const exiting = this.commandController?.term({ ...termOptions }); this.commandController = null; if (exiting) await exiting.catch(logErrorOnce_1.logErrorOnce); if (this.loop) { await this.loop.done; } if (termOptions?.destroySubcontainer) { await this.subcontainer?.destroy(); } } /** * Mark this daemon as managed by a {@link Daemons} instance. * Suppresses the individual `onLeaveContext` termination since the * `Daemons` instance handles ordered shutdown. */ markManaged() { this._managed = true; } /** Get a reference-counted handle to the daemon's subcontainer, or null if there is none */ subcontainerRc() { return this.subcontainer?.rc() ?? null; } /** Check whether this daemon shares the same subcontainer as another daemon */ sharesSubcontainerWith(other) { return this.subcontainer?.guid === other.subcontainer?.guid; } /** * Register a callback to be invoked each time the daemon's process exits. * @param fn - Callback receiving `true` on clean exit, `false` on error */ onExit(fn) { this.onExitFns.push(fn); } onDrop() { this.term().catch((e) => (0, logErrorOnce_1.logErrorOnce)((0, asError_1.asError)(e))); } } exports.Daemon = Daemon; //# sourceMappingURL=Daemon.js.map