"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