/**
 * Shared subprocess helpers for runtime code paths.
 *
 * Motivation — why NOT use raw `promisify(exec)`:
 *   Node's `exec` with no `timeout` option hangs forever if the child
 *   never exits, and the default `killSignal` is SIGTERM which a stuck
 *   helper can ignore (or be stuck inside an uninterruptible XPC /
 *   kernel call and never receive). When a hung child lives inside a
 *   `Promise.allSettled` branch or blocks a tool-call handler, the
 *   agent's main loop / tool executor deadlocks.
 *
 * Every shell-exec call on the agent's runtime hot path (screenshot,
 * computer_use tool, android_use tool, transcription, etc.) should go
 * through `execWithTimeout` so the child is SIGKILL'd on timeout and
 * the promise always resolves or rejects in bounded time.
 *
 * This module intentionally has no other dependencies so it can be
 * imported from tool executors without pulling in the logger or config.
 */
import { exec, execFile, spawn } from "node:child_process";
import { promisify } from "node:util";
const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
/** Default per-subprocess timeout. */
export const DEFAULT_EXEC_TIMEOUT_MS = 15_000;
/** Tight timeout for quick-check helpers (`which`, `pmset`, `pgrep`, …). */
export const QUICK_EXEC_TIMEOUT_MS = 5_000;
/** Longer timeout for heavy jobs (compiles, ffmpeg multi-pass, large `adb` payloads). */
export const HEAVY_EXEC_TIMEOUT_MS = 60_000;
/**
 * Run a shell command with a hard timeout. On timeout the child is
 * SIGKILL'd and the promise rejects — never hangs forever like raw
 * `promisify(exec)` does when `{ timeout }` is omitted.
 *
 * stdout / stderr are always returned as strings even when the caller
 * passed an options bag that would normally yield Buffers, so call-sites
 * can rely on string methods without narrowing.
 */
export async function execWithTimeout(command, options = {}) {
    const timeout = options.timeout ?? DEFAULT_EXEC_TIMEOUT_MS;
    const result = await execAsync(command, {
        ...options,
        timeout,
        // SIGTERM is Node's default and is easy for a stuck helper to ignore,
        // or to miss because it's blocked in an uninterruptible syscall.
        // SIGKILL guarantees the child exits so our promise unwedges.
        killSignal: "SIGKILL",
    });
    return {
        stdout: typeof result.stdout === "string" ? result.stdout : result.stdout.toString(),
        stderr: typeof result.stderr === "string" ? result.stderr : result.stderr.toString(),
    };
}
/**
 * `execFile` variant of {@link execWithTimeout}. Prefer this over `execFile`
 * directly when shell metacharacter handling isn't required — it avoids the
 * quoting pitfalls of shell-string commands while giving the same hard-kill
 * timeout guarantee.
 *
 * Like Node's `execFile`, `timeout` is forwarded via the options bag. The
 * kill signal is always forced to `SIGKILL` (any caller-supplied
 * `killSignal` is overwritten) because hung children blocked in syscalls
 * can ignore SIGTERM indefinitely, which defeats the purpose of this
 * wrapper. Callers that genuinely need a different signal should use
 * Node's `child_process.execFile` directly.
 */
export async function execFileWithTimeout(file, args = [], options = {}) {
    const timeout = options.timeout ?? DEFAULT_EXEC_TIMEOUT_MS;
    const result = await execFileAsync(file, args, {
        ...options,
        timeout,
        killSignal: "SIGKILL",
    });
    return {
        stdout: typeof result.stdout === "string" ? result.stdout : result.stdout.toString(),
        stderr: typeof result.stderr === "string" ? result.stderr : result.stderr.toString(),
    };
}
/**
 * Run a command under `sudo` with a hard timeout.
 *
 * - If `password` is provided, pipes it to `sudo -S` via stdin so the
 *   call works even when passwordless sudo (NOPASSWD) is not configured.
 *   `-p ""` suppresses the usual password prompt so stderr stays clean.
 * - If `password` is omitted, falls back to plain `sudo`, which requires
 *   passwordless sudo to already be configured for the current user.
 *
 * Like {@link execFileWithTimeout}, the child is SIGKILL'd on timeout so
 * this never hangs forever even if sudo blocks waiting for a TTY.
 */
export async function execSudo(args, options = {}) {
    const timeoutMs = options.timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
    const password = options.password;
    if (!password) {
        return execFileWithTimeout("sudo", args, { timeout: timeoutMs });
    }
    return new Promise((resolve, reject) => {
        const child = spawn("sudo", ["-S", "-p", "", ...args], {
            stdio: ["pipe", "pipe", "pipe"],
        });
        let stdout = "";
        let stderr = "";
        let timedOut = false;
        child.stdout.on("data", (d) => {
            stdout += d.toString();
        });
        child.stderr.on("data", (d) => {
            stderr += d.toString();
        });
        const timer = setTimeout(() => {
            timedOut = true;
            child.kill("SIGKILL");
        }, timeoutMs);
        child.on("error", (err) => {
            clearTimeout(timer);
            reject(err);
        });
        child.on("close", (code, signal) => {
            clearTimeout(timer);
            if (timedOut) {
                reject(new Error(`sudo timed out after ${timeoutMs}ms`));
                return;
            }
            if (code === 0) {
                resolve({ stdout, stderr });
                return;
            }
            const detail = stderr.trim() || stdout.trim() || "";
            reject(new Error(`sudo exited with code ${code ?? signal ?? "unknown"}${detail ? `: ${detail}` : ""}`));
        });
        child.stdin.write(`${password}\n`);
        child.stdin.end();
    });
}
/**
 * Race a promise against a wall-clock timeout. Used as a last-resort
 * ceiling around a sequence of calls — individual subprocess timeouts
 * are the primary defence, this is belt-and-suspenders for the cases
 * where multiple hung subprocesses in sequence could add up past the
 * acceptable budget.
 *
 * Note: this only rejects the promise; it does NOT cancel the wrapped
 * work. Pair it with `execWithTimeout` at the leaves so child processes
 * still terminate even if the outer race rejects first.
 */
export async function withTimeout(promise, timeoutMs, label) {
    let timer;
    try {
        return await Promise.race([
            promise,
            new Promise((_resolve, reject) => {
                timer = setTimeout(() => {
                    reject(new Error(`${label} timed out after ${timeoutMs}ms`));
                }, timeoutMs);
            }),
        ]);
    }
    finally {
        if (timer)
            clearTimeout(timer);
    }
}
//# sourceMappingURL=exec.js.map