fix(process): await Swift frontend lifetime pipe (#7948)
This commit is contained in:
1 parent
0448f318e6
commit
c92322b473
4 files changed
+40
-15
No files matched your search
@@ -523,6 +523,7 @@ export const createDarwinProcessRunTokenReader = (
|
||||
maxBuffer: 1024 * 1024,
|
||||
signal,
|
||||
stopSignal: SWIFTC_INTERRUPT,
|
||||
lifetimePipe: true,
|
||||
env: compilerEnvironment,
|
||||
});
|
||||
} catch (cause: unknown) {
|
||||
@@ -535,9 +536,9 @@ export const createDarwinProcessRunTokenReader = (
|
||||
} catch (cause: unknown) {
|
||||
operation.primaryFailure = { cause };
|
||||
if (operationRoot !== undefined) {
|
||||
// execFileOutput has settled, so inherited compiler pipes are closed.
|
||||
// That is not proof for a child that did not inherit them; this owner
|
||||
// does not signal processes it did not launch.
|
||||
// The extra pipe keeps settlement pending for Swift frontend writers
|
||||
// that inherit it after the driver and its standard streams close.
|
||||
// It cannot establish completion of a child that did not inherit it.
|
||||
try {
|
||||
await cleanupRoot(operationRoot);
|
||||
} catch (cleanup: unknown) {
|
||||
|
||||
@@ -68,6 +68,14 @@ it.each([undefined, "SIGTERM"] as const)(
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects a lifetime pipe without a stoppable command", async () => {
|
||||
await expect(
|
||||
execFileOutput(process.execPath, ["-e", "process.exit(0)"], {
|
||||
lifetimePipe: true,
|
||||
}),
|
||||
).rejects.toThrow("lifetimePipe requires stopSignal");
|
||||
});
|
||||
|
||||
describe("execFileOutput", () => {
|
||||
it("ignores non-Error and incomplete failure metadata", () => {
|
||||
expect(execFileOutputFailure("failure")).toBeUndefined();
|
||||
|
||||
@@ -20,6 +20,8 @@ type ExecFileOutputOptions = Omit<
|
||||
* execFile sends SIGTERM on abort and rejects before the process exits.
|
||||
*/
|
||||
readonly stopSignal?: NodeJS.Signals;
|
||||
/** Wait for inherited copies of an extra drained descriptor to close. Requires stopSignal. */
|
||||
readonly lifetimePipe?: boolean;
|
||||
};
|
||||
|
||||
/** How long a stopped process may take to exit before it is killed. */
|
||||
@@ -149,7 +151,12 @@ export const execFileBytes = (
|
||||
options: ExecFileOutputOptions = {},
|
||||
): Promise<{ readonly stdout: Buffer; readonly stderr: Buffer }> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const { stopSignal, signal, timeout, ...execOptions } = options;
|
||||
const { stopSignal, signal, timeout, lifetimePipe, ...execOptions } =
|
||||
options;
|
||||
if (lifetimePipe === true && stopSignal === undefined) {
|
||||
reject(new TypeError("lifetimePipe requires stopSignal"));
|
||||
return;
|
||||
}
|
||||
const invalid = timeout === undefined ? undefined : invalidTimeout(timeout);
|
||||
if (invalid !== undefined) {
|
||||
reject(invalid);
|
||||
@@ -213,7 +220,7 @@ const startStoppableCommand = (
|
||||
options: ExecFileOutputOptions & { readonly stopSignal: NodeJS.Signals },
|
||||
complete: (error: Error | null, stdout: Buffer, stderr: Buffer) => void,
|
||||
): void => {
|
||||
const { stopSignal, signal, timeout, ...execOptions } = options;
|
||||
const { stopSignal, signal, timeout, lifetimePipe, ...execOptions } = options;
|
||||
const maxBuffer = options.maxBuffer ?? Number.POSITIVE_INFINITY;
|
||||
if (typeof maxBuffer !== "number" || !(maxBuffer >= 0)) {
|
||||
const error = new RangeError("maxBuffer must be a nonnegative number");
|
||||
@@ -231,9 +238,16 @@ const startStoppableCommand = (
|
||||
// pipe lifetime of descendants still using caller-owned temporary files.
|
||||
const child = spawn(command, [...arguments_], {
|
||||
...execOptions,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
stdio:
|
||||
lifetimePipe === true
|
||||
? ["ignore", "pipe", "pipe", "pipe"]
|
||||
: ["ignore", "pipe", "pipe"],
|
||||
killSignal: stopSignal,
|
||||
});
|
||||
if (lifetimePipe === true) {
|
||||
const pipe = child.stdio[3];
|
||||
if (pipe != null && "resume" in pipe) pipe.resume();
|
||||
}
|
||||
// The first stop request names the failure; a later one is not resent.
|
||||
const stop = (reason: "abort" | "timeout" | "output-limit"): void => {
|
||||
if (stopped !== undefined || rootFailed || processError !== null) return;
|
||||
@@ -244,7 +258,10 @@ const startStoppableCommand = (
|
||||
const onAbort = (): void => {
|
||||
stop("abort");
|
||||
};
|
||||
const capture = (stream: typeof child.stdout, name: "stdout" | "stderr") => {
|
||||
const capture = (
|
||||
stream: NonNullable<typeof child.stdout>,
|
||||
name: "stdout" | "stderr",
|
||||
) => {
|
||||
const chunks: Buffer[] = [];
|
||||
let retained = 0;
|
||||
stream.on("data", (chunk: Buffer) => {
|
||||
@@ -266,8 +283,8 @@ const startStoppableCommand = (
|
||||
});
|
||||
return () => Buffer.concat(chunks, retained);
|
||||
};
|
||||
const stdout = capture(child.stdout, "stdout");
|
||||
const stderr = capture(child.stderr, "stderr");
|
||||
const stdout = capture(child.stdout!, "stdout");
|
||||
const stderr = capture(child.stderr!, "stderr");
|
||||
child.on("error", (error: Error) => {
|
||||
processError ??= error;
|
||||
});
|
||||
|
||||
@@ -813,9 +813,8 @@ const temporaryStateSchema = z.object({
|
||||
});
|
||||
|
||||
/**
|
||||
* A compiler that behaves like swift-driver: a job in its own process group
|
||||
* writes an object under TMPDIR, SIGINT stops the job and deletes the object,
|
||||
* and any other signal leaves the job writing after the driver exits.
|
||||
* A compiler whose detached job closes standard streams but retains fd 3,
|
||||
* writes an object after the SIGINT-stopped driver exits, then closes fd 3.
|
||||
*/
|
||||
const writeTemporaryObjectCompiler = async (directory: string) => {
|
||||
const executable = join(directory, "fake-temporary-xcrun");
|
||||
@@ -828,14 +827,14 @@ const path = process.getBuiltinModule("node:path");
|
||||
const { spawn } = process.getBuiltinModule("node:child_process");
|
||||
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "TemporaryDirectory."));
|
||||
const object = path.join(temporary, "ProcessRunTokenReader-1.o");
|
||||
let lifetime = "ignore";
|
||||
try { if (fs.fstatSync(3).isSocket()) lifetime = 3; } catch {}
|
||||
const job = spawn(process.execPath, ["-e",
|
||||
"setTimeout(() => { const fs = process.getBuiltinModule('node:fs'); fs.mkdirSync(" + JSON.stringify(temporary) +
|
||||
", { recursive: true }); fs.writeFileSync(" + JSON.stringify(object) + ", 'object'); }, 300)"],
|
||||
{ detached: true, stdio: "ignore" });
|
||||
{ detached: true, stdio: ["ignore", "ignore", "ignore", lifetime] });
|
||||
job.unref();
|
||||
process.on("SIGINT", () => {
|
||||
job.kill("SIGKILL");
|
||||
fs.rmSync(object, { force: true });
|
||||
process.exit(130);
|
||||
});
|
||||
const output = process.argv[process.argv.indexOf("-o") + 1];
|
||||
|
||||
Reference in new issue
Block a user