"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.CommandController = void 0; const _1 = require("."); const types_1 = require("../../../base/lib/types"); const T = __importStar(require("../../../base/lib/types")); const util_1 = require("../util"); const fs = __importStar(require("node:fs/promises")); const logErrorOnce_1 = require("../../../base/lib/util/logErrorOnce"); /** * Low-level controller for a single running process inside a subcontainer (or as a JS function). * * Manages the child process lifecycle: spawning, waiting, and signal-based termination. * Used internally by {@link Daemon} to manage individual command executions. * * @typeParam Manifest - The service manifest type * @typeParam C - The subcontainer type, or `null` for JS-only commands */ class CommandController extends util_1.Drop { constructor(runningAnswer, state, subcontainer, process, sigtermTimeout = _1.DEFAULT_SIGTERM_TIMEOUT) { super(); this.runningAnswer = runningAnswer; this.state = state; this.subcontainer = subcontainer; this.process = process; this.sigtermTimeout = sigtermTimeout; } /** * Factory method to create a new CommandController. * * Returns a curried async function: `(effects, subcontainer, exec) => CommandController`. * If the exec spec has an `fn` property, runs the function; otherwise spawns a shell command * in the subcontainer. */ static of() { return async (effects, subcontainer, exec) => { try { if ('fn' in exec) { const abort = new AbortController(); const cell = { ctrl: new CommandController(exec.fn(subcontainer, abort.signal).then(async (command) => { if (subcontainer && command && !abort.signal.aborted) { const newCtrl = (await CommandController.of()(effects, subcontainer, command)).leak(); Object.assign(cell.ctrl, newCtrl); return await cell.ctrl.runningAnswer; } else { cell.ctrl.state.exited = true; } return null; }), { exited: false }, subcontainer, abort, exec.sigtermTimeout), }; return cell.ctrl; } let commands; if (T.isUseEntrypoint(exec.command)) { const imageMeta = await fs .readFile(`/media/startos/images/${subcontainer.imageId}.json`, { encoding: 'utf8', }) .catch(() => '{}') .then(JSON.parse); commands = imageMeta.entrypoint ?? []; commands = commands.concat(...(exec.command.overridCmd ?? imageMeta.cmd ?? [])); } else commands = (0, util_1.splitCommand)(exec.command); let childProcess; if (exec.runAsInit) { childProcess = await subcontainer.launch(commands, { env: exec.env, user: exec.user, cwd: exec.cwd, }); } else { childProcess = await subcontainer.spawn(commands, { env: exec.env, user: exec.user, cwd: exec.cwd, stdio: exec.onStdout || exec.onStderr ? 'pipe' : 'inherit', }); } if (exec.onStdout) childProcess.stdout?.on('data', exec.onStdout); if (exec.onStderr) childProcess.stderr?.on('data', exec.onStderr); const state = { exited: false }; const answer = new Promise((resolve, reject) => { childProcess.on('exit', (code) => { state.exited = true; if (code === 0 || code === 143 || (code === null && childProcess.signalCode == 'SIGTERM')) { return resolve(null); } if (code) { return reject(new Error(`${commands[0]} exited with code ${code}`)); } else { return reject(new Error(`${commands[0]} exited with signal ${childProcess.signalCode}`)); } }); }); return new CommandController(answer, state, subcontainer, childProcess, exec.sigtermTimeout); } catch (e) { await subcontainer?.destroy(); throw e; } }; } /** * Wait for the command to finish. Optionally terminate after a timeout. * @param options.timeout - Milliseconds to wait before terminating. Defaults to no timeout. */ async wait({ timeout = types_1.NO_TIMEOUT } = {}) { if (timeout > 0) setTimeout(() => { this.term(); }, timeout); try { if (timeout > 0 && this.process instanceof AbortController) await Promise.race([ this.runningAnswer, new Promise((_, reject) => setTimeout(() => reject(new Error('Timed out waiting for js command to exit')), timeout * 2)), ]); else await this.runningAnswer; } finally { if (!this.state.exited) { if (this.process instanceof AbortController) this.process.abort(); else this.process.kill('SIGKILL'); } await this.subcontainer?.destroy(); } } /** * Terminate the running command by sending a signal. * * Sends the specified signal (default: SIGTERM), then escalates to SIGKILL * after the timeout expires. Destroys the subcontainer after the process exits. * * @param options.signal - The signal to send (default: SIGTERM) * @param options.timeout - Milliseconds before escalating to SIGKILL */ async term({ signal = types_1.SIGTERM, timeout = this.sigtermTimeout } = {}) { try { if (!this.state.exited) { if (this.process instanceof AbortController) return this.process.abort(); if (signal !== 'SIGKILL') { setTimeout(() => { if (this.process instanceof AbortController) this.process.abort(); else this.process.kill('SIGKILL'); }, timeout); } if (!this.process.kill(signal)) { console.error(`failed to send signal ${signal} to pid ${this.process.pid}`); } } if (this.process instanceof AbortController) await Promise.race([ this.runningAnswer, new Promise((_, reject) => setTimeout(() => reject(new Error('Timed out waiting for js command to exit')), timeout * 2)), ]); else await this.runningAnswer; } finally { await this.subcontainer?.destroy(); } } onDrop() { this.term().catch(logErrorOnce_1.logErrorOnce); } } exports.CommandController = CommandController; //# sourceMappingURL=CommandController.js.map