UNPKG

ttsc

Version:

General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.

213 lines 7.41 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ResidentCheckProcess = void 0; const node_child_process_1 = require("node:child_process"); const node_readline_1 = require("node:readline"); const STDERR_TAIL_LIMIT = 64 * 1024; const REPLY_ECHO_LIMIT = 200; const TERMINATION_GRACE_MS = 1_000; /** * FIFO JSON-line client for a check-stage `check-serve` sidecar. * * A watch session serializes cycles, but the client still queues replies so a * caller cannot accidentally pair a late response with the next request. Any * framing failure retires the process; the launcher then falls back to the * ordinary one-shot command for that cycle. */ class ResidentCheckProcess { child; pending = []; reader; failure; stderr = ""; constructor(options) { this.child = (0, node_child_process_1.spawn)(options.binary, [...options.args], { cwd: options.cwd, env: options.env, windowsHide: true, }); const stdin = this.child.stdin; const stdout = this.child.stdout; if (stdin === null || stdout === null) { this.child.kill(); throw new Error("ttsc: resident check host has no stdio pipes"); } this.reader = (0, node_readline_1.createInterface)({ input: stdout }); this.reader.on("line", (line) => this.onLine(line)); this.reader.on("close", () => { if (this.failure === undefined) this.fail(this.exitError()); }); this.child.stderr?.on("data", (chunk) => { const text = typeof chunk === "string" ? chunk : chunk.toString("utf8"); this.stderr = (this.stderr + text).slice(-STDERR_TAIL_LIMIT); }); this.child.on("error", (error) => this.fail(error)); stdin.on("error", (error) => this.fail(error)); stdout.on("error", (error) => this.fail(error)); this.child.stderr?.on("error", () => { }); } request(payload) { if (this.failure !== undefined) return Promise.reject(this.failure); const stdin = this.child.stdin; if (stdin === null || stdin.destroyed) { return Promise.reject(new Error("ttsc: resident check host stdin is closed")); } let line; try { line = `${JSON.stringify(payload)}\n`; } catch (error) { return Promise.reject(asError(error)); } return new Promise((resolve, reject) => { const pending = { reject, resolve }; this.pending.push(pending); try { stdin.write(line, (error) => { if (error === null || error === undefined) return; this.settle(pending, error); this.fail(error); }); } catch (error) { const reason = asError(error); this.settle(pending, reason); this.fail(reason); } }); } dispose() { if (this.failure !== undefined) return; this.fail(new Error("ttsc: resident check host disposed")); } onLine(line) { if (this.failure !== undefined || line.trim().length === 0) return; const pending = this.pending[0]; if (pending === undefined) { this.fail(new Error("ttsc: resident check host sent an unsolicited reply")); return; } const result = parseResidentCheckResult(line); if (result === undefined) { const error = new Error(`ttsc: resident check host sent a malformed reply: ${echoLine(line)}`); this.settle(pending, error); this.fail(error); return; } this.settle(pending, result); } settle(pending, result) { const index = this.pending.indexOf(pending); if (index === -1) return; this.pending.splice(index, 1); if (result instanceof Error) pending.reject(result); else pending.resolve(result); } fail(error) { if (this.failure !== undefined) return; this.failure = error; while (this.pending.length !== 0) { this.settle(this.pending[0], error); } this.terminate(); } terminate() { const stdin = this.child.stdin; if (stdin !== null && !stdin.destroyed) stdin.destroy(); if (this.child.exitCode !== null || this.child.signalCode !== null) return; try { this.child.kill(); } catch { return; } const force = setTimeout(() => { if (this.child.exitCode !== null || this.child.signalCode !== null) return; try { this.child.kill("SIGKILL"); } catch { // The host exited between the liveness check and forced termination. } }, TERMINATION_GRACE_MS); force.unref(); this.child.once("exit", () => clearTimeout(force)); } exitError() { const detail = this.stderr.trim(); if (detail.length !== 0) return new Error(detail); const signal = this.child.signalCode === null ? "" : `, signal ${this.child.signalCode}`; return new Error(`ttsc: resident check host exited (code ${this.child.exitCode ?? "null"}${signal})`); } } exports.ResidentCheckProcess = ResidentCheckProcess; function parseResidentCheckResult(line) { let value; try { value = JSON.parse(line); } catch { return undefined; } if (!isRecord(value) || typeof value.status !== "number" || !Number.isInteger(value.status)) { return undefined; } if (typeof value.stdout !== "string" || typeof value.stderr !== "string") { return undefined; } const telemetry = value.telemetry; if (!isRecord(telemetry) || typeof telemetry.pid !== "number" || !Number.isSafeInteger(telemetry.pid) || typeof telemetry.programLoads !== "number" || !Number.isSafeInteger(telemetry.programLoads) || typeof telemetry.programUpdates !== "number" || !Number.isSafeInteger(telemetry.programUpdates) || typeof telemetry.reused !== "boolean") { return undefined; } return { diagnostics: [], status: value.status, stderr: value.stderr, stdout: value.stdout, telemetry: { pid: telemetry.pid, programLoads: telemetry.programLoads, programUpdates: telemetry.programUpdates, reused: telemetry.reused, }, }; } function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function echoLine(line) { const trimmed = line.trim(); return JSON.stringify(trimmed.length <= REPLY_ECHO_LIMIT ? trimmed : `${trimmed.slice(0, REPLY_ECHO_LIMIT)}…`); } function stderrSuffix(stderr) { const detail = stderr.trim(); return detail.length === 0 ? "" : `: ${detail}`; } function asError(error) { return error instanceof Error ? error : new Error(String(error)); } //# sourceMappingURL=residentCheckProcess.js.map