diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 5f17bd8f2d..fc3db716b6 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -4931,8 +4931,8 @@ pub const Interpreter = struct { .child = undefined, .buffered_closed = buffered_closed, } }; - const subproc = switch (Subprocess.spawnAsync(this.base.eventLoop(), &shellio, spawn_args, &this.exec.subproc.child)) { - // FIXME: There's a race condition where this could change variants before spawnAsync returns. + var did_exit_immediately = false; + const subproc = switch (Subprocess.spawnAsync(this.base.eventLoop(), &shellio, spawn_args, &this.exec.subproc.child, &did_exit_immediately)) { .result => this.exec.subproc.child, .err => |*e| { this.exec = .none; @@ -4943,6 +4943,17 @@ pub const Interpreter = struct { subproc.ref(); this.spawn_arena_freed = true; arena.deinit(); + + if (did_exit_immediately) { + if (subproc.process.hasExited()) { + // process has already exited, we called wait4(), but we did not call onProcessExit() + subproc.process.onExit(subproc.process.status, &std.mem.zeroes(bun.spawn.Rusage)); + } else { + // process has already exited, but we haven't called wait4() yet + // https://cs.github.com/libuv/libuv/blob/b00d1bd225b602570baee82a6152eaa823a84fa6/src/unix/process.c#L1007 + subproc.process.wait(false); + } + } } fn setStdioFromRedirect(stdio: *[3]shell.subproc.Stdio, flags: ast.RedirectFlags, val: shell.subproc.Stdio) void { @@ -10591,9 +10602,9 @@ pub const Interpreter = struct { if (err == .sys and err.sys.getErrno() == .BUSY and (task.tgt_absolute != null and - err.sys.path.eqlUTF8(task.tgt_absolute.?)) or + err.sys.path.eqlUTF8(task.tgt_absolute.?)) or (task.src_absolute != null and - err.sys.path.eqlUTF8(task.src_absolute.?))) + err.sys.path.eqlUTF8(task.src_absolute.?))) { log("{} got ebusy {d} {d}", .{ this, this.state.exec.ebusy.tasks.items.len, this.state.exec.paths_to_copy.len }); this.state.exec.ebusy.tasks.append(bun.default_allocator, task) catch bun.outOfMemory(); diff --git a/src/shell/subproc.zig b/src/shell/subproc.zig index 9ced2d1fa7..cec72799ae 100644 --- a/src/shell/subproc.zig +++ b/src/shell/subproc.zig @@ -746,6 +746,7 @@ pub const ShellSubprocess = struct { shellio: *ShellIO, spawn_args_: SpawnArgs, out: **@This(), + notify_caller_process_already_exited: *bool, ) bun.shell.Result(void) { var arena = bun.ArenaAllocator.init(bun.default_allocator); defer arena.deinit(); @@ -761,6 +762,7 @@ pub const ShellSubprocess = struct { &spawn_args, shellio, out, + notify_caller_process_already_exited, )) { .result => |subproc| subproc, .err => |err| return .{ .err = err }, @@ -778,6 +780,7 @@ pub const ShellSubprocess = struct { spawn_args: *SpawnArgs, shellio: *ShellIO, out_subproc: **@This(), + notify_caller_process_already_exited: *bool, ) bun.shell.Result(*@This()) { const is_sync = config.is_sync; @@ -876,26 +879,16 @@ pub const ShellSubprocess = struct { subprocess.stdin.pipe.signal = JSC.WebCore.Signal.init(&subprocess.stdin); } - var send_exit_notification = false; - if (comptime !is_sync) { switch (subprocess.process.watch()) { .result => {}, .err => { - send_exit_notification = true; + notify_caller_process_already_exited.* = true; spawn_args.lazy = false; }, } } - defer { - if (send_exit_notification) { - // process has already exited - // https://cs.github.com/libuv/libuv/blob/b00d1bd225b602570baee82a6152eaa823a84fa6/src/unix/process.c#L1007 - subprocess.wait(subprocess.flags.is_sync); - } - } - if (subprocess.stdin == .buffer) { subprocess.stdin.buffer.start().assert(); } @@ -1128,10 +1121,10 @@ pub const PipeReader = struct { if (Environment.isWindows) { this.reader.source = switch (result) { - .buffer => .{ .pipe = this.stdio_result.buffer }, - .buffer_fd => .{ .file = bun.io.Source.openFile(this.stdio_result.buffer_fd) }, - .unavailable => @panic("Shouldn't happen."), - }; + .buffer => .{ .pipe = this.stdio_result.buffer }, + .buffer_fd => .{ .file = bun.io.Source.openFile(this.stdio_result.buffer_fd) }, + .unavailable => @panic("Shouldn't happen."), + }; } this.reader.setParent(this); diff --git a/test/js/bun/shell/shell-immediate-exit-fixture.js b/test/js/bun/shell/shell-immediate-exit-fixture.js new file mode 100644 index 0000000000..f007b2bab7 --- /dev/null +++ b/test/js/bun/shell/shell-immediate-exit-fixture.js @@ -0,0 +1,17 @@ +import { $, which } from "bun"; + +const cat = which("cat"); + +const promises = []; +for (let j = 0; j < 500; j++) { + for (let i = 0; i < 100; i++) { + promises.push($`${cat} ${import.meta.path}`.text().then(() => {})); + } + if (j % 10 === 0) { + await Promise.all(promises); + promises.length = 0; + console.count("Ran"); + } +} + +await Promise.all(promises); diff --git a/test/js/bun/shell/shell-load.test.ts b/test/js/bun/shell/shell-load.test.ts new file mode 100644 index 0000000000..2f05a75289 --- /dev/null +++ b/test/js/bun/shell/shell-load.test.ts @@ -0,0 +1,9 @@ +import { describe, test, expect } from "bun:test"; +import { isCI, isWindows } from "harness"; +import path from "path"; +describe("shell load", () => { + // windows process spawning is a lot slower + test.skipIf(isCI && isWindows)("immediate exit", () => { + expect([path.join(import.meta.dir, "./shell-immediate-exit-fixture.js")]).toRun(); + }); +});