ttsc
Version:
General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.
92 lines • 3.78 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.captureProcessOutput = captureProcessOutput;
const node_fs_1 = __importDefault(require("node:fs"));
const node_os_1 = __importDefault(require("node:os"));
const node_path_1 = __importDefault(require("node:path"));
/**
* A pair of temporary files standing in for a child process's pipes.
*
* `spawnSync` holds a _piped_ stream in this process's memory and refuses to
* keep more than `maxBuffer` bytes, so any piped capture has to name a ceiling
* — and a ceiling is a number nobody chose for this machine, deciding on the
* user's behalf that a large but legitimate build said too much. Handing the
* child a file descriptor instead means the bytes never pass through this heap
* on their way out of the child: how much a process may write is the
* filesystem's business, and that is the same answer on every machine. Reading
* the result back still materializes a string, so V8's own maximum string
* length remains the outer bound — but that is a property of the runtime rather
* than a budget chosen here, and it is identical everywhere this runs.
*
* The directory is per-call, so two concurrent spawns cannot read each other's
* bytes.
*/
function captureProcessOutput() {
const directory = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "ttsc-spawn-"));
const stdoutPath = node_path_1.default.join(directory, "stdout");
const stderrPath = node_path_1.default.join(directory, "stderr");
const stdoutFd = node_fs_1.default.openSync(stdoutPath, "w+");
let stderrFd;
try {
stderrFd = node_fs_1.default.openSync(stderrPath, "w+");
}
catch (error) {
// The first descriptor and the directory are already live. Nothing else
// will ever hold them, so they are released here rather than left for a
// caller that never received a handle to dispose.
closeQuietly(stdoutFd);
removeQuietly(directory);
throw error;
}
return {
dispose() {
closeQuietly(stdoutFd);
closeQuietly(stderrFd);
removeQuietly(directory);
},
read(stream, encoding) {
const location = stream === "stdout" ? stdoutPath : stderrPath;
let raw;
try {
raw = node_fs_1.default.readFileSync(location);
}
catch {
// A spawn that never launched leaves nothing behind. Report the same
// empty output a failed piped capture would have.
raw = Buffer.alloc(0);
}
return encoding === "buffer" ? raw : raw.toString(encoding ?? "utf8");
},
stderrFd,
stdoutFd,
};
}
/** Close a descriptor, ignoring one that is already closed. */
function closeQuietly(fd) {
try {
node_fs_1.default.closeSync(fd);
}
catch {
// Already closed; removing the directory is what reclaims the space.
}
}
/**
* Remove the capture directory without letting cleanup replace a result.
*
* `dispose` runs from a `finally`, so a throw here would surface instead of the
* spawn's own outcome — and on Windows a grandchild that inherited the handle
* can hold the file long enough to make removal fail. Leaving bytes in the
* system temp directory is the lesser outcome by far.
*/
function removeQuietly(directory) {
try {
node_fs_1.default.rmSync(directory, { force: true, recursive: true });
}
catch {
// Best effort.
}
}
//# sourceMappingURL=captureProcessOutput.js.map