"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const time_1 = require("@longrun-ai/kernel/utils/time");
const child_process_1 = require("child_process");
const promises_1 = __importDefault(require("node:fs/promises"));
const node_net_1 = __importDefault(require("node:net"));
const node_path_1 = __importDefault(require("node:path"));
const node_util_1 = require("node:util");
const cmd_runner_protocol_1 = require("./cmd-runner-protocol");
const process_kill_1 = require("./process-kill");
const shell_capture_env_1 = require("./shell-capture-env");
const execFileAsync = (0, node_util_1.promisify)(child_process_1.execFile);
class ScrollingBuffer {
    maxLines;
    lines = [];
    linesScrolledOut = 0;
    constructor(maxLines) {
        this.maxLines = maxLines;
    }
    addText(text) {
        const newLines = text.split('\n');
        if (newLines[newLines.length - 1] === '') {
            newLines.pop();
        }
        for (const line of newLines) {
            this.lines.push(line);
            if (this.lines.length > this.maxLines) {
                this.lines.shift();
                this.linesScrolledOut += 1;
            }
        }
    }
    snapshot() {
        return {
            content: this.lines.join('\n'),
            linesScrolledOut: this.linesScrolledOut,
        };
    }
}
async function flushIpc(msg) {
    const send = process.send;
    if (typeof send !== 'function') {
        throw new Error('cmd_runner must be launched with an IPC channel');
    }
    await new Promise((resolve, reject) => {
        send.call(process, msg, (error) => {
            if (error) {
                reject(error);
                return;
            }
            resolve();
        });
    });
}
async function readProcessCommandLine(pid) {
    try {
        if (process.platform === 'win32') {
            const command = `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; if ($null -ne $p) { [Console]::Out.Write($p.CommandLine) }`;
            const { stdout } = await execFileAsync('powershell.exe', ['-NoProfile', '-Command', command], { windowsHide: true, maxBuffer: 1024 * 1024 });
            const trimmed = stdout.trim();
            return trimmed === '' ? undefined : trimmed;
        }
        const { stdout } = await execFileAsync('ps', ['-p', String(pid), '-o', 'args='], {
            maxBuffer: 1024 * 1024,
        });
        const trimmed = stdout.trim();
        return trimmed === '' ? undefined : trimmed;
    }
    catch {
        return undefined;
    }
}
function buildStatusPayload(state) {
    const daemonCommandLine = state.daemonCommandLine;
    if (daemonCommandLine === null) {
        throw new Error('cmd_runner invariant violation: daemonCommandLine unavailable');
    }
    return {
        daemonPid: state.daemonPid,
        daemonCommandLine,
        endpoint: state.endpoint,
        runnerPid: process.pid,
        processGroupId: state.processGroupId,
        shell: state.shell,
        startTime: state.startTime,
        isRunning: state.isRunning,
        exitCode: state.exitCode,
        exitSignal: state.exitSignal,
        stdout: state.stdout.snapshot(),
        stderr: state.stderr.snapshot(),
    };
}
async function ensureSocketParentDir(endpoint) {
    if (process.platform === 'win32') {
        return;
    }
    const dir = node_path_1.default.dirname(endpoint);
    await promises_1.default.mkdir(dir, { recursive: true, mode: 0o700 });
    try {
        await promises_1.default.chmod(dir, 0o700);
    }
    catch {
        // Best effort only.
    }
    try {
        await promises_1.default.unlink(endpoint);
    }
    catch (error) {
        const code = typeof error === 'object' && error !== null ? error.code : undefined;
        if (code !== 'ENOENT') {
            throw error;
        }
    }
}
function writeSocketResponse(socket, response, callback) {
    socket.end(`${JSON.stringify(response)}\n`, callback);
}
function isProcessAlive(pid) {
    try {
        process.kill(pid, 0);
        return true;
    }
    catch (error) {
        const code = typeof error === 'object' && error !== null ? error.code : undefined;
        return code !== 'ESRCH';
    }
}
function markDaemonMissing(state) {
    state.isRunning = false;
    state.exitCode = null;
    state.exitSignal = 'SIGKILL';
}
async function main() {
    const initMessage = await new Promise((resolve, reject) => {
        const timeout = setTimeout(() => {
            reject(new Error('cmd_runner timed out waiting for init message'));
        }, 30_000);
        process.once('message', (raw) => {
            clearTimeout(timeout);
            resolve(raw);
        });
    });
    const init = (0, cmd_runner_protocol_1.parseCmdRunnerInitMessage)(initMessage);
    const childProcess = (0, child_process_1.spawn)(init.spawnSpec.command, init.spawnSpec.args, {
        stdio: ['ignore', 'pipe', 'pipe'],
        detached: false,
        env: (0, shell_capture_env_1.buildCapturedShellEnv)(),
        windowsVerbatimArguments: init.spawnSpec.windowsVerbatimArguments === true,
    });
    const daemonPid = childProcess.pid;
    if (typeof daemonPid !== 'number') {
        throw new Error('cmd_runner failed to spawn daemon command: missing pid');
    }
    const stdout = childProcess.stdout;
    const stderr = childProcess.stderr;
    if (stdout === null || stderr === null) {
        throw new Error('cmd_runner failed to spawn daemon command with piped stdout/stderr');
    }
    const endpoint = (0, cmd_runner_protocol_1.getCmdRunnerEndpointForDaemonPid)(daemonPid);
    const state = {
        endpoint,
        daemonPid,
        daemonCommandLine: null,
        processGroupId: process.platform === 'win32' ? undefined : process.pid,
        shell: init.spawnSpec.shellLabel,
        startTime: (0, time_1.formatUnifiedTimestamp)(new Date()),
        isRunning: true,
        exitCode: null,
        exitSignal: null,
        stdout: new ScrollingBuffer(init.scrollbackLines),
        stderr: new ScrollingBuffer(init.scrollbackLines),
    };
    let server;
    let closeRequested = false;
    let timeoutHandle;
    let initialResultSent = false;
    const tryFlushInitialResult = async (msg) => {
        if (initialResultSent) {
            return false;
        }
        initialResultSent = true;
        await flushIpc(msg);
        return true;
    };
    const waitForObservedChildExitOrMissingPid = async (timeoutMs) => {
        if (!state.isRunning || closeRequested || !isProcessAlive(daemonPid)) {
            return true;
        }
        return await new Promise((resolve) => {
            let settled = false;
            let timeout;
            let interval;
            const finish = (exited) => {
                if (settled)
                    return;
                settled = true;
                clearTimeout(timeout);
                clearInterval(interval);
                childProcess.off('close', onClose);
                resolve(exited || !state.isRunning || closeRequested || !isProcessAlive(daemonPid));
            };
            const onClose = () => {
                finish(true);
            };
            timeout = setTimeout(() => {
                finish(false);
            }, timeoutMs);
            interval = setInterval(() => {
                if (!isProcessAlive(daemonPid)) {
                    finish(true);
                }
            }, 25);
            childProcess.once('close', onClose);
        });
    };
    const closeServerAndExit = (code) => {
        if (closeRequested) {
            return;
        }
        closeRequested = true;
        const exit = () => {
            setImmediate(() => {
                process.exit(code);
            });
        };
        const cleanupEndpoint = () => {
            if (process.platform !== 'win32') {
                void promises_1.default.unlink(endpoint).catch(() => {
                    // Best effort only.
                });
            }
        };
        if (!server) {
            cleanupEndpoint();
            exit();
            return;
        }
        try {
            server.close(() => {
                cleanupEndpoint();
                exit();
            });
        }
        catch (error) {
            const codeValue = typeof error === 'object' && error !== null
                ? error.code
                : undefined;
            if (codeValue !== 'ERR_SERVER_NOT_RUNNING') {
                throw error;
            }
            cleanupEndpoint();
            exit();
        }
    };
    childProcess.once('close', (code, signal) => {
        if (timeoutHandle) {
            clearTimeout(timeoutHandle);
        }
        state.isRunning = false;
        state.exitCode = code;
        state.exitSignal = signal;
        void (async () => {
            if (state.daemonCommandLine === null && !initialResultSent) {
                await tryFlushInitialResult({
                    type: 'completed',
                    exitCode: code,
                    exitSignal: signal,
                    stdout: state.stdout.snapshot(),
                    stderr: state.stderr.snapshot(),
                });
            }
            closeServerAndExit(0);
        })().catch(() => {
            closeServerAndExit(0);
        });
    });
    childProcess.once('error', (error) => {
        if (timeoutHandle) {
            clearTimeout(timeoutHandle);
        }
        void tryFlushInitialResult({
            type: 'failed',
            errorText: error.message,
        })
            .catch(() => undefined)
            .finally(() => {
            closeServerAndExit(1);
        });
    });
    stdout.on('data', (data) => {
        state.stdout.addText(data.toString());
    });
    stderr.on('data', (data) => {
        state.stderr.addText(data.toString());
    });
    await ensureSocketParentDir(endpoint);
    if (closeRequested) {
        return;
    }
    server = node_net_1.default.createServer((socket) => {
        socket.setEncoding('utf8');
        let buffer = '';
        socket.on('data', (chunk) => {
            buffer += chunk;
            const newlineIndex = buffer.indexOf('\n');
            if (newlineIndex === -1) {
                return;
            }
            const line = buffer.slice(0, newlineIndex);
            buffer = '';
            void (async () => {
                try {
                    const request = (0, cmd_runner_protocol_1.parseCmdRunnerRequestLine)(line);
                    if (request.type === 'ping') {
                        writeSocketResponse(socket, {
                            type: 'pong',
                            ok: true,
                            ...buildStatusPayload(state),
                        });
                        return;
                    }
                    if (request.type === 'get_status') {
                        writeSocketResponse(socket, {
                            type: 'status',
                            ok: true,
                            ...buildStatusPayload(state),
                        });
                        return;
                    }
                    if (request.type === 'get_output') {
                        const payload = buildStatusPayload(state);
                        writeSocketResponse(socket, {
                            type: 'output',
                            ok: true,
                            ...payload,
                            stdout: request.stdout ? payload.stdout : { content: '', linesScrolledOut: 0 },
                            stderr: request.stderr ? payload.stderr : { content: '', linesScrolledOut: 0 },
                        });
                        return;
                    }
                    await handleStopRequest(request, state, childProcess);
                    const shouldExitAfterResponse = !state.isRunning;
                    writeSocketResponse(socket, {
                        type: 'stop_result',
                        ok: true,
                        ...buildStatusPayload(state),
                    }, shouldExitAfterResponse ? () => closeServerAndExit(0) : undefined);
                }
                catch (error) {
                    writeSocketResponse(socket, {
                        type: 'error',
                        ok: false,
                        errorText: error instanceof Error ? error.message : String(error),
                    });
                }
            })();
        });
    });
    await new Promise((resolve, reject) => {
        server.once('error', reject);
        server.listen(endpoint, () => {
            server.off('error', reject);
            resolve();
        });
    });
    timeoutHandle = setTimeout(() => {
        void (async () => {
            const daemonCommandLine = await readProcessCommandLine(daemonPid);
            if (!state.isRunning) {
                return;
            }
            if (daemonCommandLine === undefined || daemonCommandLine.trim() === '') {
                if (await waitForObservedChildExitOrMissingPid(250)) {
                    return;
                }
                try {
                    process.kill(daemonPid, 'SIGTERM');
                }
                catch {
                    // Best effort only.
                }
                if (!state.isRunning) {
                    return;
                }
                await tryFlushInitialResult({
                    type: 'failed',
                    errorText: `failed to capture daemon command line from OS for pid ${String(daemonPid)}`,
                });
                return;
            }
            if (!state.isRunning) {
                return;
            }
            state.daemonCommandLine = daemonCommandLine;
            await tryFlushInitialResult({
                type: 'daemonized',
                daemonPid,
                daemonCommandLine,
                endpoint,
                runnerPid: process.pid,
                processGroupId: state.processGroupId,
                shell: state.shell,
                startTime: state.startTime,
            });
        })().catch((error) => {
            if (initialResultSent) {
                return;
            }
            void tryFlushInitialResult({
                type: 'failed',
                errorText: error instanceof Error ? error.message : String(error),
            });
        });
    }, init.timeoutSeconds * 1000);
}
async function handleStopRequest(request, state, childProcess) {
    if (!state.isRunning) {
        return;
    }
    if (request.entirePg && process.platform === 'win32') {
        await (0, process_kill_1.bestEffortKillWindowsProcessTree)(state.daemonPid);
        await (0, process_kill_1.sleepMs)(100);
        if (!state.isRunning || !isProcessAlive(state.daemonPid)) {
            if (!state.isRunning) {
                return;
            }
            markDaemonMissing(state);
            return;
        }
    }
    try {
        process.kill(state.daemonPid, 'SIGTERM');
    }
    catch {
        return;
    }
    await (0, process_kill_1.sleepMs)(1000);
    if (!state.isRunning) {
        return;
    }
    try {
        process.kill(state.daemonPid, 'SIGKILL');
    }
    catch {
        // Process may have exited during grace period.
    }
    await (0, process_kill_1.sleepMs)(100);
    if (!state.isRunning) {
        return;
    }
    if (typeof childProcess.kill === 'function') {
        childProcess.kill('SIGKILL');
    }
    await (0, process_kill_1.sleepMs)(100);
    if (state.isRunning && !isProcessAlive(state.daemonPid)) {
        markDaemonMissing(state);
    }
}
void main().catch((error) => {
    void flushIpc({
        type: 'failed',
        errorText: error instanceof Error ? error.message : String(error),
    })
        .catch(() => undefined)
        .finally(() => {
        process.exit(1);
    });
});
