diff --git a/src/bun.js/WebKit b/src/bun.js/WebKit index 590f9c4019..ce4af76c27 160000 --- a/src/bun.js/WebKit +++ b/src/bun.js/WebKit @@ -1 +1 @@ -Subproject commit 590f9c4019bf8c29ce3187c12ef52892a65e18d9 +Subproject commit ce4af76c270078c06822efda52dcb7adf5902f3b diff --git a/src/bun.js/api/BunObject.classes.ts b/src/bun.js/api/BunObject.classes.ts index 3f529794ff..8b01189f37 100644 --- a/src/bun.js/api/BunObject.classes.ts +++ b/src/bun.js/api/BunObject.classes.ts @@ -96,6 +96,10 @@ export default [ fn: "kill", length: 1, }, + "@@asyncDispose": { + fn: "asyncDispose", + length: 1, + }, killed: { getter: "getKilled", diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index c09f9e9164..6d5cbe73df 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -574,6 +574,34 @@ pub const Subprocess = struct { return this.stdout.toJS(globalThis, this.hasExited()); } + pub fn asyncDispose( + this: *Subprocess, + global: *JSGlobalObject, + _: *JSC.CallFrame, + ) callconv(.C) JSValue { + if (this.process.hasExited()) { + // rely on GC to clean everything up in this case + return .undefined; + } + + // unref streams so that this disposed process will not prevent + // the process from exiting causing a hang + this.stdin.unref(); + this.stdout.unref(); + this.stderr.unref(); + + switch (this.tryKill(SignalCode.default)) { + .result => {}, + .err => |err| { + // Signal 9 should always be fine, but just in case that somehow fails. + global.throwValue(err.toJSC(global)); + return .zero; + }, + } + + return this.getExited(global); + } + pub fn kill( this: *Subprocess, globalThis: *JSGlobalObject, diff --git a/src/bun.js/api/server.classes.ts b/src/bun.js/api/server.classes.ts index deb571c7eb..b2f5610df8 100644 --- a/src/bun.js/api/server.classes.ts +++ b/src/bun.js/api/server.classes.ts @@ -20,6 +20,10 @@ function generate(name) { fn: "doReload", length: 2, }, + "@@dispose": { + fn: "dispose", + length: 0, + }, stop: { fn: "doStop", length: 1, diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 35713fe023..a55d3b8b15 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -5122,6 +5122,7 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp } = .{}, pub const doStop = JSC.wrapInstanceMethod(ThisServer, "stopFromJS", false); + pub const dispose = JSC.wrapInstanceMethod(ThisServer, "disposeFromJS", false); pub const doUpgrade = JSC.wrapInstanceMethod(ThisServer, "onUpgrade", false); pub const doPublish = JSC.wrapInstanceMethod(ThisServer, "publish", false); pub const doReload = onReload; @@ -5547,6 +5548,16 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp return JSC.JSValue.jsUndefined(); } + pub fn disposeFromJS(this: *ThisServer) JSC.JSValue { + if (this.listener != null) { + JSC.C.JSValueUnprotect(this.globalThis, this.thisObject.asObjectRef()); + this.thisObject = JSC.JSValue.jsUndefined(); + this.stop(true); + } + + return JSC.JSValue.jsUndefined(); + } + pub fn getPort( this: *ThisServer, _: *JSC.JSGlobalObject, diff --git a/src/bun.js/api/sockets.classes.ts b/src/bun.js/api/sockets.classes.ts index 4a46355719..1fb3ba11e6 100644 --- a/src/bun.js/api/sockets.classes.ts +++ b/src/bun.js/api/sockets.classes.ts @@ -111,6 +111,11 @@ function generate(ssl) { length: 0, }, + "@@dispose": { + fn: "shutdown", + length: 0, + }, + shutdown: { fn: "shutdown", length: 1, @@ -181,6 +186,10 @@ export default [ fn: "stop", length: 1, }, + "@@dispose": { + fn: "stop", + length: 0, + }, ref: { fn: "ref", @@ -239,6 +248,10 @@ export default [ fn: "close", length: 0, }, + "@@dispose": { + fn: "close", + length: 0, + }, reload: { fn: "reload", length: 1, diff --git a/src/js/bun/ffi.ts b/src/js/bun/ffi.ts index c339bef114..17dfcb0217 100644 --- a/src/js/bun/ffi.ts +++ b/src/js/bun/ffi.ts @@ -106,6 +106,10 @@ class JSCallback { closeCallback(ctx); } } + + [Symbol.dispose]() { + this.close(); + } } class CString extends String { diff --git a/src/js/node/child_process.ts b/src/js/node/child_process.ts index f1c594e573..0a09cff0e5 100644 --- a/src/js/node/child_process.ts +++ b/src/js/node/child_process.ts @@ -995,6 +995,12 @@ class ChildProcess extends EventEmitter { pid; channel; + [Symbol.dispose]() { + if (!this.killed) { + this.kill(); + } + } + get killed() { if (this.#handle == null) return false; } @@ -1310,7 +1316,7 @@ class ChildProcess extends EventEmitter { this.#handle.disconnect(); } - kill(sig) { + kill(sig?) { const signal = sig === 0 ? sig : convertToValidSignal(sig === undefined ? "SIGTERM" : sig); if (this.#handle) { diff --git a/test/js/bun/http/serve.test.ts b/test/js/bun/http/serve.test.ts index 05caaccf3b..f59e764d0a 100644 --- a/test/js/bun/http/serve.test.ts +++ b/test/js/bun/http/serve.test.ts @@ -1513,3 +1513,18 @@ it("should resolve pending promise if requested ended with pending read", async }, ); }); + +it("should work with dispose keyword", async () => { + let url: string; + { + using server = Bun.serve({ + port: 0, + fetch() { + return new Response("OK"); + }, + }); + url = server.url; + expect((await fetch(url)).status).toBe(200); + } + expect(fetch(url)).rejects.toThrow(); +}); diff --git a/test/js/bun/spawn/spawn.test.ts b/test/js/bun/spawn/spawn.test.ts index aaca1957fa..7875082c52 100644 --- a/test/js/bun/spawn/spawn.test.ts +++ b/test/js/bun/spawn/spawn.test.ts @@ -782,3 +782,18 @@ describe("close handling", () => { } } }); + +it("dispose keyword works", async () => { + let captured; + { + await using proc = spawn({ + cmd: [bunExe(), "-e", "await Bun.sleep(100000)"], + }); + captured = proc; + await Bun.sleep(100); + } + await Bun.sleep(0); + expect(captured.killed).toBe(true); + expect(captured.exitCode).toBe(null); + expect(captured.signalCode).toBe("SIGTERM"); +});