import fs from "node:fs"; import net from "node:net"; import os from "node:os"; import path from "node:path"; import process from "node:process"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; import { createBrokerEndpoint, parseBrokerEndpoint } from "./broker-endpoint.mjs"; import { resolveStateDir } from "./state.mjs"; export const PID_FILE_ENV = "CODEX_COMPANION_APP_SERVER_PID_FILE"; export const LOG_FILE_ENV = "CODEX_COMPANION_APP_SERVER_LOG_FILE"; const BROKER_STATE_FILE = "broker.json"; export function createBrokerSessionDir(prefix = "cxc-") { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); } function connectToEndpoint(endpoint) { const target = parseBrokerEndpoint(endpoint); return net.createConnection({ path: target.path }); } export async function waitForBrokerEndpoint(endpoint, timeoutMs = 2000) { const start = Date.now(); while (Date.now() - start < timeoutMs) { const ready = await new Promise((resolve) => { const socket = connectToEndpoint(endpoint); socket.on("connect", () => { socket.end(); resolve(true); }); socket.on("error", () => resolve(false)); }); if (ready) { return true; } await new Promise((resolve) => setTimeout(resolve, 50)); } return false; } export async function sendBrokerShutdown(endpoint) { await new Promise((resolve) => { const socket = connectToEndpoint(endpoint); socket.setEncoding("utf8"); socket.on("connect", () => { socket.write(`${JSON.stringify({ id: 1, method: "broker/shutdown", params: {} })}\n`); }); socket.on("data", () => { socket.end(); resolve(); }); socket.on("error", resolve); socket.on("close", resolve); }); } export function spawnBrokerProcess({ scriptPath, cwd, endpoint, pidFile, logFile, env = process.env }) { const logFd = fs.openSync(logFile, "a"); const child = spawn(process.execPath, [scriptPath, "serve", "--endpoint", endpoint, "--cwd", cwd, "--pid-file", pidFile], { cwd, env, detached: true, stdio: ["ignore", logFd, logFd] }); child.unref(); fs.closeSync(logFd); return child; } function resolveBrokerStateFile(cwd) { return path.join(resolveStateDir(cwd), BROKER_STATE_FILE); } export function loadBrokerSession(cwd) { const stateFile = resolveBrokerStateFile(cwd); if (!fs.existsSync(stateFile)) { return null; } try { return JSON.parse(fs.readFileSync(stateFile, "utf8")); } catch { return null; } } export function saveBrokerSession(cwd, session) { const stateDir = resolveStateDir(cwd); fs.mkdirSync(stateDir, { recursive: true }); fs.writeFileSync(resolveBrokerStateFile(cwd), `${JSON.stringify(session, null, 2)}\n`, "utf8"); } export function clearBrokerSession(cwd) { const stateFile = resolveBrokerStateFile(cwd); if (fs.existsSync(stateFile)) { fs.unlinkSync(stateFile); } } async function isBrokerEndpointReady(endpoint) { if (!endpoint) { return false; } try { return await waitForBrokerEndpoint(endpoint, 150); } catch { return false; } } export async function ensureBrokerSession(cwd, options = {}) { const existing = loadBrokerSession(cwd); if (existing && (await isBrokerEndpointReady(existing.endpoint))) { return existing; } if (existing) { teardownBrokerSession({ endpoint: existing.endpoint ?? null, pidFile: existing.pidFile ?? null, logFile: existing.logFile ?? null, sessionDir: existing.sessionDir ?? null, pid: existing.pid ?? null, killProcess: options.killProcess ?? null }); clearBrokerSession(cwd); } const sessionDir = createBrokerSessionDir(); const endpointFactory = options.createBrokerEndpoint ?? createBrokerEndpoint; const endpoint = endpointFactory(sessionDir, options.platform); const pidFile = path.join(sessionDir, "broker.pid"); const logFile = path.join(sessionDir, "broker.log"); const scriptPath = options.scriptPath ?? fileURLToPath(new URL("../app-server-broker.mjs", import.meta.url)); const child = spawnBrokerProcess({ scriptPath, cwd, endpoint, pidFile, logFile, env: options.env ?? process.env }); const ready = await waitForBrokerEndpoint(endpoint, options.timeoutMs ?? 2000); if (!ready) { teardownBrokerSession({ endpoint, pidFile, logFile, sessionDir, pid: child.pid ?? null, killProcess: options.killProcess ?? null }); return null; } const session = { endpoint, pidFile, logFile, sessionDir, pid: child.pid ?? null }; saveBrokerSession(cwd, session); return session; } export function teardownBrokerSession({ endpoint = null, pidFile, logFile, sessionDir = null, pid = null, killProcess = null }) { if (Number.isFinite(pid) && killProcess) { try { killProcess(pid); } catch { // Ignore missing or already-exited broker processes. } } if (pidFile && fs.existsSync(pidFile)) { fs.unlinkSync(pidFile); } if (logFile && fs.existsSync(logFile)) { fs.unlinkSync(logFile); } if (endpoint) { try { const target = parseBrokerEndpoint(endpoint); if (target.kind === "unix" && fs.existsSync(target.path)) { fs.unlinkSync(target.path); } } catch { // Ignore malformed or already-removed broker endpoints during teardown. } } const resolvedSessionDir = sessionDir ?? (pidFile ? path.dirname(pidFile) : logFile ? path.dirname(logFile) : null); if (resolvedSessionDir && fs.existsSync(resolvedSessionDir)) { try { fs.rmdirSync(resolvedSessionDir); } catch { // Ignore non-empty or missing directories. } } }