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
17 changes: 16 additions & 1 deletion packages/bun-usockets/src/eventing/libuv.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ int us_internal_libuv_peer_reset_probe(LIBUS_SOCKET_DESCRIPTOR fd) {
if (send(fd, "", 0, 0) != SOCKET_ERROR) {
return 0;
}
return WSAGetLastError() != WSAEWOULDBLOCK;
int err = WSAGetLastError();
/* WSAESHUTDOWN means our own shutdown(SD_SEND) ran; that is not a peer
* reset. The fin_deferred sweep probes sockets after local shutdown. */
return err != WSAEWOULDBLOCK && err != WSAESHUTDOWN;
}

static struct us_socket_t *us_internal_poll_cb_adopted_socket(struct us_poll_t *wp) {
Expand Down Expand Up @@ -143,6 +146,9 @@ static void poll_cb(uv_poll_t *p, int status, int events) {
events |= UV_READABLE;
}
}
if (!error && !eof && !(events & (UV_READABLE | UV_WRITABLE))) {
return;
}
us_internal_dispatch_ready_poll((struct us_poll_t *)p->data, error, eof, events);
}

Expand Down Expand Up @@ -304,7 +310,16 @@ void us_internal_poll_set_type(struct us_poll_t *p, int poll_type) {
LIBUS_SOCKET_DESCRIPTOR us_poll_fd(struct us_poll_t *p) { return p->fd; }

void us_loop_pump(struct us_loop_t *loop) {
/* POSIX parity: us_loop_run_bun_tick polls epoll/kqueue and dispatches
* regardless of ref state (it only early-outs on num_polls == 0). libuv's
* uv_run() skips its body when uv__loop_alive() is 0, so IOCP completions
* for unref'd handles (subprocess exit packets, socket events) and due
* timers are never processed. Bun's outer drive loops (wait_for_promise,
* bun:test) supply their own keep-going predicate, so force exactly one
* non-blocking iteration; UV_RUN_NOWAIT keeps the poll timeout at 0. */
loop->uv_loop->active_handles++;
uv_run(loop->uv_loop, UV_RUN_NOWAIT);
loop->uv_loop->active_handles--;
}

struct us_loop_t *us_create_loop(void *hint,
Expand Down
7 changes: 3 additions & 4 deletions src/spawn/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -946,10 +946,9 @@ impl PollerWindows {
}

pub fn disable_keeping_event_loop_alive(&mut self, _event_loop: bun_io::EventLoopCtx) {
// This is disabled on Windows
// uv_unref() causes the onExitUV callback to *never* be called
// This breaks a lot of stuff...
// Once fixed, re-enable "should not hang after unref" test in spawn.test
// uv_unref() drops this handle from loop->active_handles. us_loop_pump
// forces a non-blocking uv_run iteration regardless, so the
// wait-thread's IOCP exit packet is still dequeued and on_exit_uv fires.
match self {
PollerWindows::Uv(p) => {
p.unref();
Expand Down
46 changes: 40 additions & 6 deletions test/js/bun/spawn/spawn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,8 +630,7 @@ describe("spawn unref and kill should not hang", () => {
stderr: "ignore",
stdin: "ignore",
});
// TODO: on Windows
if (!isWindows) proc.unref();
proc.unref();
await proc.exited;
}

Expand All @@ -647,7 +646,7 @@ describe("spawn unref and kill should not hang", () => {
});

proc.kill();
if (!isWindows) proc.unref();
proc.unref();

await proc.exited;
console.count("Finished");
Expand All @@ -663,16 +662,14 @@ describe("spawn unref and kill should not hang", () => {
stderr: "ignore",
stdin: "ignore",
});
// TODO: on Windows
if (!isWindows) proc.unref();
proc.unref();
proc.kill();
await proc.exited;
}

expect().pass();
});

// process.unref() on Windows does not work ye :(
it("should not hang after unref", async () => {
const proc = spawn({
cmd: [bunExe(), path.join(import.meta.dir, "does-not-hang.js")],
Expand Down Expand Up @@ -780,6 +777,43 @@ describe("should not hang", () => {
}
});

describe("unref() + .exited with nothing else ref'd (Windows)", () => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Windows: with only an unref'd uv_process_t left, uv_run() used to skip its
// body and never dequeue the IOCP exit packet, so these children busy-spun
// forever. us_loop_pump now forces one non-blocking iteration (POSIX parity).
for (const [name, body] of [
["unref() then await .exited", `const p = Bun.spawn(opts); p.unref(); await p.exited;`],
[".exited then unref() then await", `const p = Bun.spawn(opts); const done = p.exited; p.unref(); await done;`],
[
"onExit then unref()",
`const { promise, resolve } = Promise.withResolvers();
const p = Bun.spawn({ ...opts, onExit: resolve }); p.unref(); await promise;`,
],
] as const) {
it(name, async () => {
await using child = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const opts = { cmd: [${JSON.stringify(bunExe())}, "-e", ""], stdio: ["ignore", "ignore", "ignore"] };
${body}
console.log("resolved");`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([child.stdout.text(), child.stderr.text(), child.exited]);
expect({ stdout, stderr, exitCode, signalCode: child.signalCode }).toEqual({
stdout: "resolved\n",
stderr: "",
exitCode: 0,
signalCode: null,
});
});
}
});

it("#3480", async () => {
{
using server = Bun.serve({
Expand Down
20 changes: 12 additions & 8 deletions test/js/bun/windows/appcontainer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,20 @@ if (isWindows) {
const sizeOut = Buffer.alloc(8);
kernel32.InitializeProcThreadAttributeList(null, 1, 0, ptr(sizeOut));
const attrList = Buffer.alloc(Number(sizeOut.readBigUInt64LE(0)));
if (kernel32.InitializeProcThreadAttributeList(ptr(attrList), 1, 0, ptr(sizeOut)) === 0)
throw new Error("InitializeProcThreadAttributeList");
// PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES = 0x20009
if (
kernel32.UpdateProcThreadAttribute(ptr(attrList), 0, 0x20009n as any, ptr(secCaps), 24n as any, null, null) === 0
)
throw new Error("UpdateProcThreadAttribute");
keepAlive = [attrList, secCaps];
keepAlive = [attrList, secCaps, sizeOut];

launchInContainer = (cmdline: string, cwd: string, timeoutMs: number): number => {
// The attribute list is rebuilt on each call (same backing buffers):
// reusing a module-load-time list across later event-loop turns proved
// fragile (CreateProcessW started returning ERROR_INVALID_PARAMETER).
if (kernel32.InitializeProcThreadAttributeList(ptr(attrList), 1, 0, ptr(sizeOut)) === 0)
throw new Error("InitializeProcThreadAttributeList");
// PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES = 0x20009
if (
kernel32.UpdateProcThreadAttribute(ptr(attrList), 0, 0x20009n as any, ptr(secCaps), 24n as any, null, null) ===
0
)
throw new Error("UpdateProcThreadAttribute");
// STARTUPINFOEXW: 104-byte STARTUPINFOW (cb=112) + lpAttributeList.
const siex = Buffer.alloc(112);
siex.writeUInt32LE(112, 0);
Expand Down
38 changes: 38 additions & 0 deletions test/js/node/http/node-http-connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -782,3 +782,41 @@ describe("Should be compatible with node.js", () => {
expect(await process.exited).toBe(0);
});
});

// Windows: after FIN on a CONNECT-tunnel socket, AFD's level-triggered
// UV_DISCONNECT used to re-derive EOF and bounce the poll between 0 and
// WRITABLE forever (pins the poll_cb allow_half_open arm).
test("CONNECT: process exits after the tunnel socket is re-emitted as a connection and the server closes", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const http = require("node:http");
let endCount = 0;
const server = http.createServer(() => { throw new Error("request listener should not run"); });
server.on("connect", (req, socket) => {
socket.on("end", () => endCount++);
socket.write("HTTP/1.1 200 Connection Established\\r\\n\\r\\n");
server.emit("connection", socket);
server.close();
});
server.listen(0, () => {
http.request({ port: server.address().port, method: "CONNECT" }).end();
});
process.on("exit", () => {
if (endCount !== 1) throw new Error("end fired " + endCount + " times (expected 1)");
console.log("ok");
});`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout, stderr, exitCode, signalCode: proc.signalCode }).toEqual({
stdout: "ok\n",
stderr: "",
exitCode: 0,
signalCode: null,
});
});
31 changes: 30 additions & 1 deletion test/js/web/abort/abort.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import { writeFileSync } from "fs";
import { bunEnv, bunExe, tmpdirSync } from "harness";
import { bunEnv, bunExe, tempDir, tmpdirSync } from "harness";
import { tmpdir } from "os";
import { join } from "path";

Expand Down Expand Up @@ -88,6 +88,35 @@ describe("AbortSignal", () => {
expect(ac.reason.code).toBe(23);
});

// #33334: with nothing else ref'd, uv_run() skipped its body on Windows so
// uv__run_timers never ran and the whole file hung. Subprocess so a
// regression is an attributable failure, not a file-level timeout.
test("awaiting AbortSignal.timeout(n) abort event with nothing else ref'd does not hang (#33334)", async () => {
using dir = tempDir("abort-33334", {
"timeout.test.ts": `import { expect, test } from "bun:test";
test("AbortSignal.timeout fires", async () => {
const signal = AbortSignal.timeout(1);
const { promise, resolve } = Promise.withResolvers<Event>();
signal.addEventListener("abort", resolve, { once: true });
await promise;
expect(signal.aborted).toBe(true);
});`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "timeout.test.ts"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stderr: stderr.includes("1 pass") ? "1 pass" : stderr, exitCode, signalCode: proc.signalCode }).toEqual({
stderr: "1 pass",
exitCode: 0,
signalCode: null,
});
});

// https://wpt.fyi/results/dom/abort/timeout.any.html "AbortSignal timeouts fire in order"
test("AbortSignal.timeout with equal deadlines fire in creation order", async () => {
const src = `
Expand Down
3 changes: 1 addition & 2 deletions test/js/web/abort/abort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,7 @@ describe("AbortSignal", () => {
expect(() => AbortSignal.timeout(timeout)).toThrow(TypeError);
});
}
// FIXME: test runner hangs when this is enabled
test.skip("timeout works", done => {
test("timeout works", done => {
const abort = AbortSignal.timeout(1);
abort.addEventListener("abort", event => {
done();
Expand Down
Loading