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
9 changes: 9 additions & 0 deletions docs/runtime/child-process.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,14 @@ The `serialization` option controls the underlying communication format between
- `advanced`: (default) Messages are serialized using the JSC `serialize` API, which supports cloning [everything `structuredClone` supports](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). This does not support transferring ownership of objects.
- `json`: Messages are serialized using `JSON.stringify` and `JSON.parse`, which does not support as many object types as `advanced` does.

`childProc.connected` reports whether the IPC channel is still open. It is `true` from the time the process is spawned with `ipc` until either side disconnects or the child exits, and always `false` for a process spawned without `ipc`. Calling `.send()` once it is `false` throws `ERR_IPC_CHANNEL_CLOSED`, so check it before sending to a child that may have gone away:

```ts
if (childProc.connected) {
childProc.send("still there?");
}
```

To disconnect the IPC channel from the parent process, call:

```ts
Expand Down Expand Up @@ -580,6 +588,7 @@ interface Subprocess extends AsyncDisposable {
readonly exitCode: number | null;
readonly signalCode: NodeJS.Signals | null;
readonly killed: boolean;
readonly connected: boolean;

kill(exitCode?: number | NodeJS.Signals): void;
ref(): void;
Expand Down
25 changes: 25 additions & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7518,6 +7518,31 @@ declare module "bun" {
*/
readonly killed: boolean;

/**
* Whether the IPC channel to the subprocess is open.
*
* `true` from the moment a process is spawned with the `ipc` option until the
* channel closes. It becomes `false` synchronously when {@link disconnect} is
* called (even if queued messages are still being flushed), and once the
* subprocess has called `process.disconnect()` or exited. It is always `false`
* for a process spawned without the `ipc` option.
*
* Once this is `false`, {@link send} fails with `ERR_IPC_CHANNEL_CLOSED`. To be
* notified when the channel closes, pass an `onDisconnect` callback to {@link Bun.spawn}.
*
* This is the same value `child_process.ChildProcess` exposes as `subprocess.connected`.
*
* @example
* ```ts
* const child = Bun.spawn(["bun", "child.ts"], { ipc(message) {} });
* child.connected; // true
*
* child.disconnect();
* child.connected; // false
* ```
*/
readonly connected: boolean;

/**
* Kill the process
* @param exitCode Exit code or signal to send to the process
Expand Down
29 changes: 29 additions & 0 deletions test/integration/bun-types/bun-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,35 @@ describe("@types/bun integration test", () => {
});
});

// Also runs on debug builds, where the in-process typeTest cases (which cover the whole
// fixture directory) are skipped: checks fixture/spawn.ts alone against the packed bun-types.
describe("Bun.spawn", () => {
test("fixture/spawn.ts type-checks", async () => {
const checkDir = join(TEMP_DIR, "spawn-fixture-check");
const tsconfig = structuredClone(sourceTsconfig);
tsconfig.files = [join(BASE_FIXTURE_DIR, "spawn.ts")];
tsconfig.compilerOptions.typeRoots = [join(BASE_FIXTURE_DIR, "node_modules", "@types")];
await mkdir(checkDir, { recursive: true });
await makeTree(checkDir, {
"tsconfig.json": JSON.stringify(tsconfig, null, 2),
});

await using proc = Bun.spawn({
cmd: [bunExe(), join(BASE_FIXTURE_DIR, "node_modules", "typescript", "bin", "tsc"), "-p", "."],
env: bunEnv,
cwd: checkDir,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr.trim()).toBe("");
expect(stdout.trim()).toBe("");
expect(exitCode).toBe(0);
});
});

describe("Test Globals", () => {
const code = `
const test_shouldBeAFunction: Function = test;
Expand Down
20 changes: 20 additions & 0 deletions test/integration/bun-types/fixture/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,26 @@ function depromise<T>(_promise: Promise<T>): T {
proc.unref();
}

{
const proc = Bun.spawn(["bun", "child.ts"], {
ipc(message, subprocess) {
tsd.expectType(subprocess.connected).is<boolean>();
},
});

tsd.expectType(proc.connected).is<boolean>();
if (proc.connected) proc.disconnect();

// @ts-expect-error connected is a read-only getter
proc.connected = false;
}

{
// connected exists whether or not the process was spawned with `ipc` (it is false without it).
const proc = Bun.spawn(["echo", "hello"]);
tsd.expectType(proc.connected).is<boolean>();
}

{
const proc = Bun.spawn(["echo", "hello"], {
stdio: ["pipe", "pipe", "pipe"],
Expand Down