Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion process/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@effectionx/process",
"description": "Spawn and manage child processes with structured concurrency",
"version": "0.7.3",
"version": "0.7.4",
"keywords": [
"effection",
"effectionx",
Expand Down
12 changes: 12 additions & 0 deletions process/src/exec/posix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ export const createPosixProcess: CreateOSProcess = (command, options) => {
},
};

const stdinErrorHandler = (err: Error & { code?: string }) => {
if (err.code === "EPIPE") {
console.warn(
`stdin EPIPE: child process (pid: ${childProcess.pid}) already exited. Writes to stdin are being discarded.`,
);
return;
}
processResult.resolve(Err(err));
};
childProcess.stdin.on("error", stdinErrorHandler);

yield* spawn(function* trapError() {
let [error] = yield* once<[Error]>(childProcess, "error");
processResult.resolve(Err(error));
Expand Down Expand Up @@ -128,6 +139,7 @@ export const createPosixProcess: CreateOSProcess = (command, options) => {
} catch (_e) {
// do nothing, process is probably already dead
}
childProcess.stdin.off("error", stdinErrorHandler);
}
});
};
17 changes: 10 additions & 7 deletions process/src/exec/win32.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,16 @@ export const createWin32Process: CreateOSProcess = (command, options) => {
return status;
}

// Suppress EPIPE errors on stdin - these occur on Windows when the child
// process exits before we finish writing to it. This is expected during
// cleanup when we're killing the process.
childProcess.stdin.on("error", (err: Error & { code?: string }) => {
if (err.code !== "EPIPE") {
throw err;
const stdinErrorHandler = (err: Error & { code?: string }) => {
if (err.code === "EPIPE") {
console.warn(
`stdin EPIPE: child process (pid: ${childProcess.pid}) already exited. Writes to stdin are being discarded.`,
);
return;
}
});
processResult.resolve(Err(err));
};
childProcess.stdin.on("error", stdinErrorHandler);

try {
yield* provide({
Expand Down Expand Up @@ -210,6 +212,7 @@ export const createWin32Process: CreateOSProcess = (command, options) => {
} catch (_e) {
// do nothing, process is probably already dead
}
childProcess.stdin.off("error", stdinErrorHandler);
}
});
};
Expand Down
24 changes: 24 additions & 0 deletions process/test/exec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,3 +565,27 @@ describe("handles env vars", () => {

// Close the main "handles env vars" describe block
});

describe("stdin EPIPE handling", () => {
it("does not crash when writing to stdin after child exits", function* () {
let proc = yield* exec("node './fixtures/read-one-line.js'", {
cwd: import.meta.dirname,
});

// First write succeeds β€” child reads this line and exits
proc.stdin.send("hello\n");

// Wait for child to exit cleanly
let status = yield* proc.join();
expect(status.code).toEqual(0);

// Explicitly write to stdin after child has exited.
// Without the EPIPE handler, this would surface as an uncaught exception.
proc.stdin.send("this should not crash\n");

// If we reach here, the EPIPE was handled gracefully.
// The test completing is the assertion β€” an uncaught EPIPE would
// have crashed the test runner before this point.
expect(true).toBe(true);
});
});
9 changes: 9 additions & 0 deletions process/test/fixtures/read-one-line.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
let buffer = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => {
buffer += chunk;
if (buffer.includes("\n")) {
process.stdout.write("got line\n");
process.exit(0);
}
});
Loading