"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.Daemons = exports.runCommand = exports.cpExecFile = exports.cpExec = exports.CommandController = exports.Daemon = void 0; const node_util_1 = require("node:util"); const CP = __importStar(require("node:child_process")); var Daemon_1 = require("./Daemon"); Object.defineProperty(exports, "Daemon", { enumerable: true, get: function () { return Daemon_1.Daemon; } }); var CommandController_1 = require("./CommandController"); Object.defineProperty(exports, "CommandController", { enumerable: true, get: function () { return CommandController_1.CommandController; } }); const HealthDaemon_1 = require("./HealthDaemon"); const Daemon_2 = require("./Daemon"); const CommandController_2 = require("./CommandController"); const Oneshot_1 = require("./Oneshot"); /** Promisified version of `child_process.exec` */ exports.cpExec = (0, node_util_1.promisify)(CP.exec); /** Promisified version of `child_process.execFile` */ exports.cpExecFile = (0, node_util_1.promisify)(CP.execFile); const runCommand = () => CommandController_2.CommandController.of(); exports.runCommand = runCommand; /** * A class for defining and controlling the service daemons ```ts Daemons.of({ effects, started, interfaceReceipt, // Provide the interfaceReceipt to prove it was completed healthReceipts, // Provide the healthReceipts or [] to prove they were at least considered }).addDaemon('webui', { command: 'hello-world', // The command to start the daemon ready: { display: 'Web Interface', // The function to run to determine the health status of the daemon fn: () => checkPortListening(effects, 80, { successMessage: 'The web interface is ready', errorMessage: 'The web interface is not ready', }), }, requires: [], }) ``` */ class Daemons { constructor(effects, ids, healthDaemons) { this.effects = effects; this.ids = ids; this.healthDaemons = healthDaemons; this.termPromise = null; } /** * Returns an empty new Daemons class with the provided inputSpec. * * Call .addDaemon() on the returned class to add a daemon. * * Daemons run in the order they are defined, with latter daemons being capable of * depending on prior daemons * * @param effects * * @param started * @returns */ static of(options) { return new Daemons(options.effects, [], []); } addDaemonImpl(id, daemon, requires, ready) { const healthDaemon = new HealthDaemon_1.HealthDaemon(daemon, requires .map((x) => this.ids.indexOf(x)) .filter((x) => x >= 0) .map((id) => this.healthDaemons[id]), id, ready, this.effects); const ids = [...this.ids, id]; const healthDaemons = [...this.healthDaemons, healthDaemon]; return new Daemons(this.effects, ids, healthDaemons); } addDaemon(id, options) { const prev = this; const res = (options) => { if (!options) return prev; const daemon = 'daemon' in options ? options.daemon : Daemon_2.Daemon.of()(this.effects, options.subcontainer, options.exec); return prev.addDaemonImpl(id, daemon, options.requires, options.ready); }; if (options instanceof Function) { const opts = options(); if (opts instanceof Promise) { return opts.then(res); } return res(opts); } return res(options); } addOneshot(id, options) { const prev = this; const res = (options) => { if (!options) return prev; const daemon = Oneshot_1.Oneshot.of()(this.effects, options.subcontainer, options.exec); return prev.addDaemonImpl(id, daemon, options.requires, HealthDaemon_1.EXIT_SUCCESS); }; if (options instanceof Function) { const opts = options(); if (opts instanceof Promise) { return opts.then(res); } return res(opts); } return res(options); } addHealthCheck(id, options) { const prev = this; const res = (options) => { if (!options) return prev; return prev.addDaemonImpl(id, null, options.requires, options.ready); }; if (options instanceof Function) { const opts = options(); if (opts instanceof Promise) { return opts.then(res); } return res(opts); } return res(options); } /** * Runs the entire system until all daemons have returned `ready`. * @param id * @param options * @returns a new Daemons object */ async runUntilSuccess(timeout) { let resolve = (_) => { }; const res = new Promise((res, rej) => { resolve = res; if (timeout) setTimeout(() => { const notReady = this.healthDaemons .filter((d) => !d.isReady) .map((d) => d.id); rej(new Error(`Timed out waiting for ${notReady}`)); }, timeout); }); const daemon = Oneshot_1.Oneshot.of()(this.effects, null, { fn: async () => { resolve(); return null; }, }); const healthDaemon = new HealthDaemon_1.HealthDaemon(daemon, [...this.healthDaemons], '__RUN_UNTIL_SUCCESS', 'EXIT_SUCCESS', this.effects); const daemons = await new Daemons(this.effects, this.ids, [ ...this.healthDaemons, healthDaemon, ]).build(); try { await res; } finally { await daemons.term(); } return null; } /** * Gracefully terminate all daemons in reverse dependency order. * * Daemons with no remaining dependents are shut down first, proceeding * until all daemons have been terminated. Falls back to a bulk shutdown * if a dependency cycle is detected. */ async term() { if (!this.termPromise) { this.termPromise = this._term(); } return this.termPromise; } async _term() { const remaining = new Set(this.healthDaemons); while (remaining.size > 0) { // Find daemons with no remaining dependents const canShutdown = [...remaining].filter((daemon) => ![...remaining].some((other) => other.dependencies.some((dep) => dep.id === daemon.id))); if (canShutdown.length === 0) { // Dependency cycle that should not happen, just shutdown remaining daemons console.warn('Dependency cycle detected, shutting down remaining daemons'); canShutdown.push(...[...remaining].reverse()); } // remove from remaining set canShutdown.forEach((daemon) => remaining.delete(daemon)); // Shutdown daemons with no remaining dependents concurrently await Promise.allSettled(canShutdown.map(async (daemon) => { try { console.debug(`Terminating daemon ${daemon.id}`); const destroySubcontainer = daemon.daemon ? ![...remaining].some((d) => d.daemon?.sharesSubcontainerWith(daemon.daemon)) : false; await daemon.term({ destroySubcontainer }); } catch (e) { console.error(e); } })); } } /** * Start all registered daemons and their health checks. * @returns This `Daemons` instance, now running */ async build() { this.effects.onLeaveContext(() => { this.term().catch((e) => console.error(e)); }); for (const daemon of this.healthDaemons) { daemon.daemon?.markManaged(); await daemon.updateStatus(); } return this; } } exports.Daemons = Daemons; //# sourceMappingURL=Daemons.js.map