Skip to content
Merged
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 src/runtime/socket/Listener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1891,6 +1891,15 @@ impl WindowsNamedPipeListeningContext {
// return error.FailedChmodPipe;
//}

// `uv_listen` made the pipe an active+ref'd uv handle. Strip libuv's
// loop ref so the owning `Listener`'s `poll_ref` is the only thing
// keeping the process alive (the contract usockets' libuv backend
// applies to its handles); otherwise `server.unref()` drops the
// `poll_ref` but the uv handle still pins `uv_loop_alive` and the
// process never exits.
// SAFETY: `this` is live; `&mut uv_pipe` is scoped to this call.
unsafe { (*this).uv_pipe.unref() };

let (this, _) = scopeguard::ScopeGuard::into_inner(cleanup);
Ok(this)
}
Expand Down
35 changes: 34 additions & 1 deletion test/js/node/net/node-net-server.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { realpathSync } from "fs";
import { bunEnv, bunExe } from "harness";
import { bunEnv, bunExe, tempDir } from "harness";
import { AddressInfo, createServer, Server, Socket } from "net";
import { createTest } from "node-harness";
import { once } from "node:events";
Expand Down Expand Up @@ -629,3 +629,36 @@ describe("accepted socket event-loop hold matches Node (per-connection KeepAlive
).toEqual({ stdout: "fast", exitCode: 0, failureDetail: "" });
});
});

// The Windows named-pipe listener never stripped libuv's own loop ref from its
// uv_pipe_t (uv_listen marks the handle active+ref'd), so server.unref()
// dropped the Listener's KeepAlive but the uv handle still pinned
// uv_loop_alive and the process never exited. TCP and unix-socket listeners go
// through usockets, which unrefs its uv handles up front.
it("server.unref() on a pipe/unix-socket listener lets the process exit", async () => {
// The child exits without close() (natural exit is the observable), so the
// unix socket file must live in a tempDir the parent disposes.
using dir = tempDir("server-unref", {});
const listenPath =
process.platform === "win32"
? "\\\\.\\pipe\\bun-server-unref-" + process.pid + "-" + Math.random().toString(36).slice(2)
: join(String(dir), "server-unref.sock");
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const server = require("net").createServer();
server.listen(process.env.SERVER_UNREF_LISTEN_PATH, () => server.unref());
setTimeout(() => { process.stdout.write("HUNG"); process.exit(1); }, 4000).unref();
`,
],
env: { ...bunEnv, SERVER_UNREF_LISTEN_PATH: listenPath },
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(exitCode === 0 ? "" : stderr).toBe("");
expect(exitCode).toBe(0);
});