From 2d61c865fc2baedb5b15352a559bf7a6b28694b8 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 21 Mar 2024 23:50:48 -0700 Subject: [PATCH] Replace some of `std.ChildProcess` with `bun.spawnSync` (#9513) * Replace some of std.ChildProcess with bun.spawnSync * Update process.zig * Fix some build errors * Fix linux build * Keep error * Don't print a mesasge in this case * Update spawn.test.ts * Make `bun install` faster on Linux * Comments + edgecases * Fix the tests * Add bun install launch.json --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Co-authored-by: Zack Radisic --- .vscode/launch.json | 14 + src/__global.zig | 4 + src/bun.js/api/bun/process.zig | 735 +++++++++++++------ src/bun.js/api/bun/subprocess.zig | 6 +- src/bun.js/node/node_fs.zig | 36 +- src/bun.zig | 8 + src/cli/bunx_command.zig | 71 +- src/cli/run_command.zig | 297 +++++--- src/copy_file.zig | 153 ++-- src/deps/libuv.zig | 2 +- src/install/install.zig | 3 +- src/install/lifecycle_script_runner.zig | 50 +- src/io/PipeReader.zig | 20 + src/string_builder.zig | 20 + src/sys.zig | 90 +++ src/which.zig | 7 +- test/cli/install/bun-run.test.ts | 56 +- test/js/bun/console/console-iterator.test.ts | 20 +- test/js/bun/spawn/spawn.test.ts | 6 +- 19 files changed, 1132 insertions(+), 466 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 8505007e52..fb2f37e1e5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -404,6 +404,20 @@ "action": "openExternally" } }, + { + "type": "lldb", + "request": "launch", + "name": "bun install [folder]", + "program": "${workspaceFolder}/build/bun-debug", + "args": ["install"], + "cwd": "${fileDirname}", + "env": { + "FORCE_COLOR": "1", + "BUN_DEBUG_QUIET_LOGS": "1", + "BUN_GARBAGE_COLLECTOR_LEVEL": "2" + }, + "console": "internalConsole" + }, { "type": "lldb", "request": "launch", diff --git a/src/__global.zig b/src/__global.zig index 65ee2d3723..e1aca54628 100644 --- a/src/__global.zig +++ b/src/__global.zig @@ -106,6 +106,10 @@ pub fn exitWide(code: u32) noreturn { } pub fn raiseIgnoringPanicHandler(sig: anytype) noreturn { + if (comptime @TypeOf(sig) == bun.SignalCode) { + return raiseIgnoringPanicHandler(@intFromEnum(sig)); + } + Output.flush(); @import("./crash_reporter.zig").on_error = null; if (!Environment.isWindows) { diff --git a/src/bun.js/api/bun/process.zig b/src/bun.js/api/bun/process.zig index daa73f44cd..16a44f582d 100644 --- a/src/bun.js/api/bun/process.zig +++ b/src/bun.js/api/bun/process.zig @@ -85,12 +85,18 @@ const ShellSubprocess = bun.shell.ShellSubprocess; pub const ProcessExitHandler = struct { ptr: TaggedPointer = TaggedPointer.Null, - pub const TaggedPointer = bun.TaggedPointerUnion(.{ - Subprocess, - LifecycleScriptSubprocess, - ShellSubprocess, - // ShellSubprocessMini, - }); + const SyncProcess = if (Environment.isWindows) sync.SyncWindowsProcess else SyncProcessPosix; + const SyncProcessPosix = opaque {}; + + pub const TaggedPointer = bun.TaggedPointerUnion( + .{ + Subprocess, + LifecycleScriptSubprocess, + ShellSubprocess, + + SyncProcess, + }, + ); pub fn init(this: *ProcessExitHandler, ptr: anytype) void { this.ptr = TaggedPointer.init(ptr); @@ -114,6 +120,13 @@ pub const ProcessExitHandler = struct { const subprocess = this.ptr.as(ShellSubprocess); subprocess.onProcessExit(process, status, rusage); }, + @field(TaggedPointer.Tag, bun.meta.typeBaseName(@typeName(SyncProcess))) => { + const subprocess = this.ptr.as(SyncProcess); + if (comptime Environment.isPosix) { + @panic("This code should not reached"); + } + subprocess.onProcessExit(status, rusage); + }, else => { @panic("Internal Bun error: ProcessExitHandler has an invalid tag. Please file a bug report."); }, @@ -151,13 +164,13 @@ pub const Process = struct { pub fn initPosix( posix: PosixSpawnResult, event_loop: anytype, - sync: bool, + sync_: bool, ) *Process { return Process.new(.{ .pid = posix.pid, .pidfd = posix.pidfd orelse 0, .event_loop = JSC.EventLoopHandle.init(event_loop), - .sync = sync, + .sync = sync_, .poller = .{ .detached = {} }, }); } @@ -193,15 +206,15 @@ pub const Process = struct { return this.status.signalCode(); } - pub fn waitPosix(this: *Process, sync: bool) void { + pub fn waitPosix(this: *Process, sync_: bool) void { var rusage = std.mem.zeroes(Rusage); - const waitpid_result = PosixSpawn.wait4(this.pid, if (sync) 0 else std.os.W.NOHANG, &rusage); + const waitpid_result = PosixSpawn.wait4(this.pid, if (sync_) 0 else std.os.W.NOHANG, &rusage); this.onWaitPid(&waitpid_result, &rusage); } - pub fn wait(this: *Process, sync: bool) void { + pub fn wait(this: *Process, sync_: bool) void { if (comptime Environment.isPosix) { - this.waitPosix(sync); + this.waitPosix(sync_); } else if (comptime Environment.isWindows) {} } @@ -225,86 +238,35 @@ pub const Process = struct { this.deref(); } - fn onWaitPid(this: *Process, waitpid_result_: *const JSC.Maybe(PosixSpawn.WaitPidResult), rusage: *const Rusage) void { + fn onWaitPid(this: *Process, waitpid_result: *const JSC.Maybe(PosixSpawn.WaitPidResult), rusage: *const Rusage) void { if (comptime !Environment.isPosix) { @compileError("not implemented on this platform"); } const pid = this.pid; - var waitpid_result = waitpid_result_.*; var rusage_result = rusage.*; - var exit_code: ?u8 = null; - var signal: ?u8 = null; - var err: ?bun.sys.Error = null; - while (true) { - switch (waitpid_result) { + const status: Status = Status.from(pid, waitpid_result) orelse brk: { + switch (this.rewatchPosix()) { + .result => {}, .err => |err_| { - err = err_; - }, - .result => |*result| { - if (result.pid == this.pid) { - if (std.os.W.IFEXITED(result.status)) { - exit_code = std.os.W.EXITSTATUS(result.status); - // True if the process terminated due to receipt of a signal. - } - - if (std.os.W.IFSIGNALED(result.status)) { - signal = @as(u8, @truncate(std.os.W.TERMSIG(result.status))); - } - - // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/waitpid.2.html - // True if the process has not terminated, but has stopped and can - // be restarted. This macro can be true only if the wait call spec-ified specified - // ified the WUNTRACED option or if the child process is being - // traced (see ptrace(2)). - else if (std.os.W.IFSTOPPED(result.status)) { - signal = @as(u8, @truncate(std.os.W.STOPSIG(result.status))); + if (comptime Environment.isMac) { + if (err_.getErrno() == .SRCH) { + break :brk Status.from(pid, &PosixSpawn.wait4( + pid, + if (this.sync) 0 else std.os.W.NOHANG, + &rusage_result, + )); } } + break :brk Status{ .err = err_ }; }, } + break :brk null; + } orelse return; - if (exit_code == null and signal == null and err == null) { - switch (this.rewatchPosix()) { - .result => {}, - .err => |err_| { - if (comptime Environment.isMac) { - if (err_.getErrno() == .SRCH) { - waitpid_result = PosixSpawn.wait4( - pid, - if (this.sync) 0 else std.os.W.NOHANG, - &rusage_result, - ); - continue; - } - } - err = err_; - }, - } - } - - break; - } - - if (exit_code != null) { - this.onExit( - .{ - .exited = .{ .code = exit_code.?, .signal = @enumFromInt(signal orelse 0) }, - }, - &rusage_result, - ); - } else if (signal != null) { - this.onExit( - .{ - .signaled = @enumFromInt(signal.?), - }, - &rusage_result, - ); - } else if (err != null) { - this.onExit(.{ .err = err.? }, &rusage_result); - } + this.onExit(status, &rusage_result); } pub fn watch(this: *Process, vm: anytype) JSC.Maybe(void) { @@ -536,6 +498,52 @@ pub const Status = union(enum) { signal: bun.SignalCode = @enumFromInt(0), }; + pub fn from(pid: pid_t, waitpid_result: *const Maybe(PosixSpawn.WaitPidResult)) ?Status { + var exit_code: ?u8 = null; + var signal: ?u8 = null; + + switch (waitpid_result.*) { + .err => |err_| { + return .{ .err = err_ }; + }, + .result => |*result| { + if (result.pid != pid) { + return null; + } + + if (std.os.W.IFEXITED(result.status)) { + exit_code = std.os.W.EXITSTATUS(result.status); + // True if the process terminated due to receipt of a signal. + } + + if (std.os.W.IFSIGNALED(result.status)) { + signal = @as(u8, @truncate(std.os.W.TERMSIG(result.status))); + } + + // https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/waitpid.2.html + // True if the process has not terminated, but has stopped and can + // be restarted. This macro can be true only if the wait call spec-ified specified + // ified the WUNTRACED option or if the child process is being + // traced (see ptrace(2)). + else if (std.os.W.IFSTOPPED(result.status)) { + signal = @as(u8, @truncate(std.os.W.STOPSIG(result.status))); + } + }, + } + + if (exit_code != null) { + return .{ + .exited = .{ .code = exit_code.?, .signal = @enumFromInt(signal orelse 0) }, + }; + } else if (signal != null) { + return .{ + .signaled = @enumFromInt(signal.?), + }; + } + + return null; + } + pub fn signalCode(this: *const Status) ?bun.SignalCode { return switch (this.*) { .signaled => |sig| sig, @@ -914,6 +922,13 @@ pub const PosixSpawnOptions = struct { detached: bool = false, windows: void = {}, argv0: ?[*:0]const u8 = null, + stream: bool = true, + + /// Apple Extension: If this bit is set, rather + /// than returning to the caller, posix_spawn(2) + /// and posix_spawnp(2) will behave as a more + /// featureful execve(2). + use_execve_on_macos: bool = false, pub const Stdio = union(enum) { path: []const u8, @@ -935,6 +950,7 @@ pub const WindowsSpawnResult = struct { stdout: StdioResult = .unavailable, stderr: StdioResult = .unavailable, extra_pipes: std.ArrayList(StdioResult) = std.ArrayList(StdioResult).init(bun.default_allocator), + stream: bool = true, pub const StdioResult = union(enum) { /// inherit, ignore, path, pipe @@ -947,11 +963,11 @@ pub const WindowsSpawnResult = struct { pub fn toProcess( this: *WindowsSpawnResult, _: anytype, - sync: bool, + sync_: bool, ) *Process { var process = this.process_.?; this.process_ = null; - process.sync = sync; + process.sync = sync_; return process; } @@ -974,6 +990,8 @@ pub const WindowsSpawnOptions = struct { detached: bool = false, windows: WindowsOptions = .{}, argv0: ?[*:0]const u8 = null, + stream: bool = true, + use_execve_on_macos: bool = false, pub const WindowsOptions = struct { verbatim_arguments: bool = false, @@ -1014,6 +1032,8 @@ pub const PosixSpawnResult = struct { stderr: ?bun.FileDescriptor = null, extra_pipes: std.ArrayList(bun.FileDescriptor) = std.ArrayList(bun.FileDescriptor).init(bun.default_allocator), + memfds: [3]bool = .{ false, false, false }, + pub fn close(this: *WindowsSpawnResult) void { for (this.extra_pipes.items) |fd| { _ = bun.sys.close(fd); @@ -1025,12 +1045,12 @@ pub const PosixSpawnResult = struct { pub fn toProcess( this: *const PosixSpawnResult, event_loop: anytype, - sync: bool, + sync_: bool, ) *Process { return Process.initPosix( this.*, event_loop, - sync, + sync_, ); } @@ -1131,6 +1151,14 @@ pub fn spawnProcessPosix( if (comptime Environment.isMac) { flags |= bun.C.POSIX_SPAWN_CLOEXEC_DEFAULT; + + if (options.use_execve_on_macos) { + flags |= bun.C.POSIX_SPAWN_SETEXEC; + + if (options.stdin == .buffer or options.stdout == .buffer or options.stderr == .buffer) { + Output.panic("Internal error: stdin, stdout, and stderr cannot be buffered when use_execve_on_macos is true", .{}); + } + } } if (options.detached) { @@ -1205,6 +1233,32 @@ pub fn spawnProcessPosix( try actions.open(fileno, path, flag | std.os.O.CREAT, 0o664); }, .buffer => { + if (Environment.isLinux) use_memfd: { + if (!options.stream and i > 0) { + // use memfd if we can + const label = switch (i) { + 0 => "spawn_stdio_stdin", + 1 => "spawn_stdio_stdout", + 2 => "spawn_stdio_stderr", + else => "spawn_stdio_generic", + }; + + // We use the linux syscall api because the glibc requirement is 2.27, which is a little close for comfort. + const rc = std.os.linux.memfd_create(label, 0); + if (std.os.linux.getErrno(rc) != .SUCCESS) { + break :use_memfd; + } + + const fd = bun.toFD(rc); + to_close_on_error.append(fd) catch {}; + to_set_cloexec.append(fd) catch {}; + try actions.dup2(fd, fileno); + stdio.* = fd; + spawned.memfds[i] = true; + continue; + } + } + const fds: [2]bun.FileDescriptor = brk: { var fds_: [2]std.c.fd_t = undefined; const rc = std.c.socketpair(std.os.AF.UNIX, std.os.SOCK.STREAM, 0, &fds_); @@ -1614,178 +1668,403 @@ pub fn spawnProcessWindows( return .{ .result = result }; } -// pub const TaskProcess = struct { -// process: *Process, -// pending_error: ?bun.sys.Error = null, -// std: union(enum) { -// buffer: struct { -// out: BufferedOutput = BufferedOutput{}, -// err: BufferedOutput = BufferedOutput{}, -// }, -// unavailable: void, +pub const sync = struct { + pub const Options = struct { + stdin: Stdio = .ignore, + stdout: Stdio = .inherit, + stderr: Stdio = .inherit, + cwd: []const u8 = "", + detached: bool = false, -// pub fn out(this: *@This()) [2]TaskOptions.Output.Result { -// return switch (this.*) { -// .unavailable => .{ .{ .unavailable = {} }, .{ .unavailable = {} } }, -// .buffer => |*buffer| { -// return .{ -// .{ -// .buffer = buffer.out.buffer.moveToUnmanaged().items, -// }, -// .{ -// .buffer = buffer.err.buffer.moveToUnmanaged().items, -// }, -// }; -// }, -// }; -// } -// } = .{ .buffer = .{} }, -// callback: Callback = Callback{}, + argv: []const []const u8 = &.{}, + envp: ?[*:null]?[*:0]const u8, -// pub const Callback = struct { -// ctx: *anyopaque = undefined, -// callback: *const fn (*anyopaque, status: Status, stdout: TaskOptions.Output.Result, stderr: TaskOptions.Output.Result) void = undefined, -// }; + use_execve_on_macos: bool = false, + argv0: ?[*:0]const u8 = null, -// pub inline fn loop(this: *const TaskProcess) JSC.EventLoopHandle { -// return this.process.event_loop; -// } + windows: if (Environment.isWindows) WindowsSpawnOptions.WindowsOptions else void = if (Environment.isWindows) .{} else undefined, -// fn onReaderDone(this: *TaskProcess) void { -// this.maybeFinish(); -// } + pub const Stdio = union(enum) { + inherit: void, + ignore: void, + buffer: if (Environment.isWindows) *uv.Pipe else void, -// fn onReaderError(this: *TaskProcess, err: bun.sys.Error) void { -// this.pending_error = err; + pub fn toStdio(this: *const Stdio) SpawnOptions.Stdio { + return switch (this.*) { + .inherit => .{ .inherit = this.inherit }, + .ignore => .{ .ignore = this.ignore }, + .buffer => .{ .buffer = this.buffer }, + }; + } + }; -// this.maybeFinish(); -// } + pub fn toSpawnOptions(this: *const Options) SpawnOptions { + return SpawnOptions{ + .stdin = this.stdin.toStdio(), + .stdout = this.stdout.toStdio(), + .stderr = this.stderr.toStdio(), + .cwd = this.cwd, + .detached = this.detached, + .use_execve_on_macos = this.use_execve_on_macos, + .stream = false, + .argv0 = this.argv0, + .windows = if (Environment.isWindows) + this.windows + else {}, + }; + } + }; -// pub fn isDone(this: *const TaskProcess) bool { -// if (!this.process.hasExited()) { -// return false; -// } + pub const Result = struct { + status: Status, + stdout: std.ArrayList(u8) = .{ .items = &.{}, .allocator = bun.default_allocator, .capacity = 0 }, + stderr: std.ArrayList(u8) = .{ .items = &.{}, .allocator = bun.default_allocator, .capacity = 0 }, + }; -// switch (this.std) { -// .buffer => |*buffer| { -// if (!buffer.err.is_done) -// return false; + const SyncWindowsPipeReader = struct { + chunks: std.ArrayList([]u8) = .{ .items = &.{}, .allocator = bun.default_allocator, .capacity = 0 }, + pipe: *uv.Pipe, -// if (!buffer.out.is_done) -// return false; -// }, -// else => {}, -// } + err: bun.C.E = .SUCCESS, + context: *SyncWindowsProcess, + onDoneCallback: *const fn (*SyncWindowsProcess, tag: bun.FDTag, chunks: []const []u8, err: bun.C.E) void = &SyncWindowsProcess.onReaderDone, + tag: bun.FDTag = .none, -// return true; -// } + pub usingnamespace bun.New(@This()); -// fn maybeFinish(this: *TaskProcess) void { -// if (!this.isDone()) { -// return; -// } + fn onAlloc(_: *SyncWindowsPipeReader, suggested_size: usize) []u8 { + return bun.default_allocator.alloc(u8, suggested_size) catch bun.outOfMemory(); + } -// const status = brk: { -// if (this.pending_error) |pending_er| { -// if (this.process.status == .exited) { -// break :brk .{ .err = pending_er }; -// } -// } + fn onRead(this: *SyncWindowsPipeReader, data: []const u8) void { + this.chunks.append(@constCast(data)) catch bun.outOfMemory(); + } -// break :brk this.process.status; -// }; + fn onError(this: *SyncWindowsPipeReader, err: bun.C.E) void { + this.err = err; + this.pipe.close(onClose); + } -// const callback = this.callback; -// const out, const err = this.std.out(); + fn onClose(pipe: *uv.Pipe) callconv(.C) void { + const this: *SyncWindowsPipeReader = pipe.getData(SyncWindowsPipeReader) orelse @panic("Expected SyncWindowsPipeReader to have data"); + const context = this.context; + const chunks = this.chunks.items; + const err = if (this.err == .CANCELED) .SUCCESS else this.err; + const tag = this.tag; + const onDoneCallback = this.onDoneCallback; + bun.default_allocator.destroy(this.pipe); + bun.default_allocator.destroy(this); + onDoneCallback(context, tag, chunks, err); + } -// this.process.detach(); -// this.process.deref(); -// this.deinit(); -// callback.callback(callback.ctx, status, out, err); -// } + pub fn start(this: *SyncWindowsPipeReader) Maybe(void) { + this.pipe.setData(this); + this.pipe.ref(); + return this.pipe.readStart(this, onAlloc, onError, onRead); + } + }; -// pub const BufferedOutput = struct { -// poll: *bun.Async.FilePoll = undefined, -// buffer: std.ArrayList(u8) = std.ArrayList(u8).init(bun.default_allocator), -// is_done: bool = false, + const SyncWindowsProcess = struct { + stderr: []const []u8 = &.{}, + stdout: []const []u8 = &.{}, + err: bun.C.E = .SUCCESS, + waiting_count: u8 = 1, + process: *Process, + status: ?Status = null, -// // This is a workaround for "Dependency loop detected" -// parent: *TaskProcess = undefined, + pub usingnamespace bun.New(@This()); -// pub usingnamespace bun.io.PipeReader( -// @This(), -// getFd, -// getBuffer, -// null, -// registerPoll, -// done, -// onError, -// ); + pub fn onProcessExit(this: *SyncWindowsProcess, status: Status, _: *const Rusage) void { + this.status = status; + this.waiting_count -= 1; + this.process.detach(); + this.process.deref(); + } -// pub fn getFd(this: *BufferedOutput) bun.FileDescriptor { -// return this.poll.fd; -// } + pub fn onReaderDone(this: *SyncWindowsProcess, tag: bun.FDTag, chunks: []const []u8, err: bun.C.E) void { + switch (tag) { + .stderr => { + this.stderr = chunks; + }, + .stdout => { + this.stdout = chunks; + }, + else => unreachable, + } + if (err != .SUCCESS) { + this.err = err; + } -// pub fn getBuffer(this: *BufferedOutput) *std.ArrayList(u8) { -// return &this.buffer; -// } + this.waiting_count -= 1; + } + }; -// fn finish(this: *BufferedOutput) void { -// this.poll.flags.insert(.ignore_updates); -// this.parent.loop().putFilePoll(this.parent, this.poll); -// std.debug.assert(!this.is_done); -// this.is_done = true; -// } + fn flattenOwnedChunks(total_allocator: std.mem.Allocator, chunks_allocator: std.mem.Allocator, chunks: []const []u8) ![]u8 { + var total_size: usize = 0; + for (chunks) |chunk| { + total_size += chunk.len; + } + const result = try total_allocator.alloc(u8, total_size); + var remain = result; + for (chunks) |chunk| { + @memcpy(remain[0..chunk.len], chunk); + remain = remain[chunk.len..]; + chunks_allocator.free(chunk); + } -// pub fn done(this: *BufferedOutput, _: []u8) void { -// this.finish(); -// onReaderDone(this.parent); -// } + return result; + } -// pub fn onError(this: *BufferedOutput, err: bun.sys.Error) void { -// this.finish(); -// onReaderError(this.parent, err); -// } + fn spawnWindowsWithoutPipes( + options: *const Options, + argv: [*:null]?[*:0]const u8, + envp: [*:null]?[*:0]const u8, + ) !Maybe(Result) { + var loop = options.windows.loop.platformEventLoop(); + var spawned = switch (try spawnProcessWindows(&options.toSpawnOptions(), argv, envp)) { + .err => |err| return .{ .err = err }, + .result => |proces| proces, + }; -// pub fn registerPoll(this: *BufferedOutput) void { -// switch (this.poll.register(this.parent().loop(), .readable, true)) { -// .err => |err| { -// this.onError(err); -// }, -// .result => {}, -// } -// } + var process = spawned.toProcess(undefined, true); + defer { + process.detach(); + process.deref(); + } + process.enableKeepingEventLoopAlive(); -// pub fn start(this: *BufferedOutput) JSC.Maybe(void) { -// const maybe = this.poll.register(this.parent.loop(), .readable, true); -// if (maybe != .result) { -// this.is_done = true; -// return maybe; -// } + while (!process.hasExited()) { + loop.run(); + } -// this.read(); + return .{ + .result = .{ + .status = process.status, + }, + }; + } -// return .{ -// .result = {}, -// }; -// } -// }; + fn spawnWindowsWithPipes( + options: *const Options, + argv: [*:null]?[*:0]const u8, + envp: [*:null]?[*:0]const u8, + ) !Maybe(Result) { + var loop: JSC.EventLoopHandle = options.windows.loop; + var spawned = switch (try spawnProcessWindows(&options.toSpawnOptions(), argv, envp)) { + .err => |err| return .{ .err = err }, + .result => |proces| proces, + }; + var this = SyncWindowsProcess.new(.{ + .process = spawned.toProcess(undefined, true), + }); + this.process.setExitHandler(this); + defer this.destroy(); + this.process.enableKeepingEventLoopAlive(); + inline for (.{ .stdout, .stderr }) |tag| { + if (@field(spawned, @tagName(tag)) == .buffer) { + var reader = SyncWindowsPipeReader.new(.{ + .context = this, + .tag = tag, + .pipe = @field(spawned, @tagName(tag)).buffer, + }); + this.waiting_count += 1; + switch (reader.start()) { + .err => |err| { + _ = this.process.kill(1); + Output.panic("Unexpected error starting {s} pipe reader\n{}", .{ @tagName(tag), err }); + }, + .result => {}, + } + } + } -// pub const Result = union(enum) { -// fd: bun.FileDescriptor, -// buffer: []u8, -// unavailable: void, + while (this.waiting_count > 0) { + loop.platformEventLoop().tick(); + } -// pub fn deinit(this: *const Result) void { -// return switch (this.*) { -// .fd => { -// _ = bun.sys.close(this.fd); -// }, -// .buffer => { -// bun.default_allocator.free(this.buffer); -// }, -// .unavailable => {}, -// }; -// } -// }; -// }; + const result = Result{ + .status = this.status orelse @panic("Expected Process to have exited when waiting_count == 0"), + .stdout = std.ArrayList(u8).fromOwnedSlice( + bun.default_allocator, + flattenOwnedChunks(bun.default_allocator, bun.default_allocator, this.stdout) catch bun.outOfMemory(), + ), + .stderr = std.ArrayList(u8).fromOwnedSlice( + bun.default_allocator, + flattenOwnedChunks(bun.default_allocator, bun.default_allocator, this.stderr) catch bun.outOfMemory(), + ), + }; + this.stdout = &.{}; + this.stderr = &.{}; + this.process.deref(); + return .{ .result = result }; + } + + pub fn spawnWithArgv( + options: *const Options, + argv: [*:null]?[*:0]const u8, + envp: [*:null]?[*:0]const u8, + ) !Maybe(Result) { + if (comptime Environment.isWindows) { + if (options.stdin != .buffer and options.stderr != .buffer and options.stdout != .buffer) { + return try spawnWindowsWithoutPipes(options, argv, envp); + } + + return try spawnWindowsWithPipes(options, argv, envp); + } + + return spawnPosix(options, argv, envp); + } + + pub fn spawn( + options: *const Options, + ) !Maybe(Result) { + const envp = options.envp orelse std.c.environ; + const argv = options.argv; + var string_builder = bun.StringBuilder{}; + defer string_builder.deinit(bun.default_allocator); + for (argv) |arg| { + string_builder.countZ(arg); + } + + try string_builder.allocate(bun.default_allocator); + + var args = std.ArrayList(?[*:0]u8).initCapacity(bun.default_allocator, argv.len + 1) catch bun.outOfMemory(); + defer args.deinit(); + + for (argv) |arg| { + args.appendAssumeCapacity(@constCast(string_builder.appendZ(arg).ptr)); + } + args.appendAssumeCapacity(null); + + return spawnWithArgv(options, @ptrCast(args.items.ptr), @ptrCast(envp)); + } + + fn spawnPosix( + options: *const Options, + argv: [*:null]?[*:0]const u8, + envp: [*:null]?[*:0]const u8, + ) !Maybe(Result) { + const process = switch (try spawnProcessPosix(&options.toSpawnOptions(), argv, envp)) { + .err => |err| return .{ .err = err }, + .result => |proces| proces, + }; + var out = [2]std.ArrayList(u8){ + std.ArrayList(u8).init(bun.default_allocator), + std.ArrayList(u8).init(bun.default_allocator), + }; + var out_fds = [2]bun.FileDescriptor{ process.stdout orelse bun.invalid_fd, process.stderr orelse bun.invalid_fd }; + defer { + for (out_fds) |fd| { + if (fd != bun.invalid_fd) { + _ = bun.sys.close(fd); + } + } + + if (comptime Environment.isLinux) { + if (process.pidfd) |pidfd| { + _ = bun.sys.close(bun.toFD(pidfd)); + } + } + } + + var out_fds_to_wait_for = [2]bun.FileDescriptor{ + process.stdout orelse bun.invalid_fd, + process.stderr orelse bun.invalid_fd, + }; + + if (process.memfds[0]) { + out_fds_to_wait_for[0] = bun.invalid_fd; + } + + if (process.memfds[1]) { + out_fds_to_wait_for[1] = bun.invalid_fd; + } + + while (out_fds_to_wait_for[0] != bun.invalid_fd or out_fds_to_wait_for[1] != bun.invalid_fd) { + for (&out_fds_to_wait_for, &out, &out_fds) |*fd, *bytes, *out_fd| { + if (fd.* == bun.invalid_fd) continue; + while (true) { + bytes.ensureUnusedCapacity(16384) catch bun.outOfMemory(); + switch (bun.sys.recvNonBlock(fd.*, bytes.unusedCapacitySlice())) { + .err => |err| { + if (err.isRetry() or err.getErrno() == .PIPE) { + break; + } + _ = std.c.kill(process.pid, 1); + return .{ .err = err }; + }, + .result => |bytes_read| { + bytes.items.len += bytes_read; + if (bytes_read == 0) { + _ = bun.sys.close(fd.*); + fd.* = bun.invalid_fd; + out_fd.* = bun.invalid_fd; + break; + } + }, + } + } + } + + var poll_fds_buf = [_]std.c.pollfd{ + .{ + .fd = 0, + .events = std.os.POLL.IN | std.os.POLL.ERR | std.os.POLL.HUP, + .revents = 0, + }, + .{ + .fd = 0, + .events = std.os.POLL.IN | std.os.POLL.ERR | std.os.POLL.HUP, + .revents = 0, + }, + }; + var poll_fds: []std.c.pollfd = poll_fds_buf[0..]; + poll_fds.len = 0; + + if (out_fds_to_wait_for[0] != bun.invalid_fd) { + poll_fds.len += 1; + poll_fds[poll_fds.len - 1].fd = @intCast(out_fds_to_wait_for[0].cast()); + } + + if (out_fds_to_wait_for[1] != bun.invalid_fd) { + poll_fds.len += 1; + poll_fds[poll_fds.len - 1].fd = @intCast(out_fds_to_wait_for[0].cast()); + } + + if (poll_fds.len == 0) { + break; + } + + const rc = std.c.poll(poll_fds.ptr, @intCast(poll_fds.len), -1); + switch (std.c.getErrno(rc)) { + .SUCCESS => {}, + .AGAIN, .INTR => continue, + else => |err| return .{ .err = bun.sys.Error.fromCode(err, .poll) }, + } + } + + const status: Status = brk: { + while (true) { + if (Status.from(process.pid, &PosixSpawn.wait4(process.pid, 0, null))) |stat| break :brk stat; + } + + unreachable; + }; + + if (comptime Environment.isLinux) { + for (process.memfds[1..], &out, out_fds) |memfd, *bytes, out_fd| { + if (memfd) { + bytes.* = bun.sys.File.from(out_fd).readToEnd(bun.default_allocator).bytes; + } + } + } + + return .{ + .result = Result{ + .status = status, + .stdout = out[0], + .stderr = out[1], + }, + }; + } +}; diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index 996588e997..f4aa21aae9 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -1157,7 +1157,7 @@ pub const Subprocess = struct { return error.UnexpectedCreatingStdin; }, } - + pipe.writer.setParent(pipe); subprocess.weak_file_sink_stdin_ptr = pipe; subprocess.flags.has_stdin_destructor_called = false; @@ -1283,6 +1283,10 @@ pub const Subprocess = struct { return switch (this.*) { .pipe => |pipe| { + if (pipe.signal.ptr == @as(*anyopaque, @ptrCast(this))) { + pipe.signal.clear(); + } + pipe.deref(); this.* = .{ .ignore = {} }; diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index c0c9b8bc46..2c8712072a 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -3877,6 +3877,28 @@ pub const NodeFS = struct { return Maybe(Return.CopyFile).success; } + // copy_file_range() is frequently not supported across devices, such as tmpfs. + // This is relevant for `bun install` + // However, sendfile() is supported across devices. + // Only on Linux. There are constraints though. It cannot be used if the file type does not support + pub noinline fn copyFileUsingSendfileOnLinuxWithReadWriteFallback(src: [:0]const u8, dest: [:0]const u8, src_fd: FileDescriptor, dest_fd: FileDescriptor, stat_size: usize, wrote: *u64) Maybe(Return.CopyFile) { + while (true) { + const amt = switch (bun.sys.sendfile(src_fd, dest_fd, std.math.maxInt(i32) - 1)) { + .err => { + return copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, stat_size, wrote); + }, + .result => |amount| amount, + }; + + wrote.* += amt; + if (amt == 0) { + break; + } + } + + return Maybe(Return.CopyFile).success; + } + /// https://github.com/libuv/libuv/pull/2233 /// https://github.com/pnpm/pnpm/issues/2761 /// https://github.com/libuv/libuv/pull/2578 @@ -4034,7 +4056,7 @@ pub const NodeFS = struct { var off_out_copy = @as(i64, @bitCast(@as(u64, 0))); if (!bun.canUseCopyFileRangeSyscall()) { - return copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote); + return copyFileUsingSendfileOnLinuxWithReadWriteFallback(src, dest, src_fd, dest_fd, size, &wrote); } if (size == 0) { @@ -4045,11 +4067,12 @@ pub const NodeFS = struct { const written = linux.copy_file_range(src_fd.cast(), &off_in_copy, dest_fd.cast(), &off_out_copy, std.mem.page_size, 0); if (ret.errnoSysP(written, .copy_file_range, dest)) |err| { return switch (err.getErrno()) { + .INTR => continue, inline .XDEV, .NOSYS => |errno| brk: { if (comptime errno == .NOSYS) { bun.disableCopyFileRangeSyscall(); } - break :brk copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote); + break :brk copyFileUsingSendfileOnLinuxWithReadWriteFallback(src, dest, src_fd, dest_fd, size, &wrote); }, else => return err, }; @@ -4065,11 +4088,12 @@ pub const NodeFS = struct { const written = linux.copy_file_range(src_fd.cast(), &off_in_copy, dest_fd.cast(), &off_out_copy, size, 0); if (ret.errnoSysP(written, .copy_file_range, dest)) |err| { return switch (err.getErrno()) { + .INTR => continue, inline .XDEV, .NOSYS => |errno| brk: { if (comptime errno == .NOSYS) { bun.disableCopyFileRangeSyscall(); } - break :brk copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote); + break :brk copyFileUsingSendfileOnLinuxWithReadWriteFallback(src, dest, src_fd, dest_fd, size, &wrote); }, else => return err, }; @@ -6359,7 +6383,7 @@ pub const NodeFS = struct { var off_out_copy = @as(i64, @bitCast(@as(u64, 0))); if (!bun.canUseCopyFileRangeSyscall()) { - return copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote); + return copyFileUsingSendfileOnLinuxWithReadWriteFallback(src, dest, src_fd, dest_fd, size, &wrote); } if (size == 0) { @@ -6374,7 +6398,7 @@ pub const NodeFS = struct { if (comptime errno == .NOSYS) { bun.disableCopyFileRangeSyscall(); } - break :brk copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote); + break :brk copyFileUsingSendfileOnLinuxWithReadWriteFallback(src, dest, src_fd, dest_fd, size, &wrote); }, else => return err, }; @@ -6394,7 +6418,7 @@ pub const NodeFS = struct { if (comptime errno == .NOSYS) { bun.disableCopyFileRangeSyscall(); } - break :brk copyFileUsingReadWriteLoop(src, dest, src_fd, dest_fd, size, &wrote); + break :brk copyFileUsingSendfileOnLinuxWithReadWriteFallback(src, dest, src_fd, dest_fd, size, &wrote); }, else => return err, }; diff --git a/src/bun.zig b/src/bun.zig index e2b4167a1e..d9713e649f 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -968,6 +968,8 @@ pub const disableCopyFileRangeSyscall = CopyFile.disableCopyFileRangeSyscall; pub const can_use_ioctl_ficlone = CopyFile.can_use_ioctl_ficlone; pub const disable_ioctl_ficlone = CopyFile.disable_ioctl_ficlone; pub const copyFile = CopyFile.copyFile; +pub const copyFileWithState = CopyFile.copyFileWithState; +pub const CopyFileState = CopyFile.CopyFileState; pub fn parseDouble(input: []const u8) !f64 { if (comptime Environment.isWasm) { @@ -1022,6 +1024,10 @@ pub const SignalCode = enum(u8) { return null; } + pub fn valid(value: SignalCode) bool { + return @intFromEnum(value) <= @intFromEnum(SignalCode.SIGSYS) and @intFromEnum(value) >= @intFromEnum(SignalCode.SIGHUP); + } + /// Shell scripts use exit codes 128 + signal number /// https://tldp.org/LDP/abs/html/exitcodes.html pub fn toExitCode(value: SignalCode) ?u8 { @@ -2806,3 +2812,5 @@ pub fn linuxKernelVersion() Semver.Version { pub const WindowsSpawnWorkaround = @import("./child_process_windows.zig"); pub const exe_suffix = if (Environment.isWindows) ".exe" else ""; + +pub const spawnSync = @This().spawn.sync.spawn; diff --git a/src/cli/bunx_command.zig b/src/cli/bunx_command.zig index c054f122f4..8f2794a6b7 100644 --- a/src/cli/bunx_command.zig +++ b/src/cli/bunx_command.zig @@ -508,6 +508,7 @@ pub const BunxCommand = struct { try Run.runBinary( ctx, try this_bundler.fs.dirname_store.append(@TypeOf(out), out), + destination, this_bundler.fs.top_level_dir, this_bundler.env, passthrough, @@ -545,6 +546,7 @@ pub const BunxCommand = struct { try Run.runBinary( ctx, try this_bundler.fs.dirname_store.append(@TypeOf(out), out), + destination, this_bundler.fs.top_level_dir, this_bundler.env, passthrough, @@ -598,41 +600,50 @@ pub const BunxCommand = struct { const argv_to_use = args.slice(); debug("installing package: {s}", .{bun.fmt.fmtSlice(argv_to_use, " ")}); - var child_process = std.ChildProcess.init(argv_to_use, default_allocator); - child_process.cwd_dir = bunx_install_dir; - debug("cwd: {}", .{bun.toFD(bunx_install_dir.fd)}); - // https://github.com/ziglang/zig/issues/5190 - if (Environment.isWindows) { - child_process.cwd = bunx_cache_dir; - } this_bundler.env.map.put("BUN_INTERNAL_BUNX_INSTALL", "true") catch bun.outOfMemory(); - var env_map = try this_bundler.env.map.stdEnvMap(ctx.allocator); - defer env_map.deinit(); - child_process.env_map = env_map.get(); - child_process.stderr_behavior = .Inherit; - child_process.stdin_behavior = .Inherit; - child_process.stdout_behavior = .Inherit; - if (Environment.isWindows) { - try bun.WindowsSpawnWorkaround.spawnWindows(&child_process); - } else { - try child_process.spawn(); - } + const spawn_result = switch ((bun.spawnSync(&.{ + .argv = argv_to_use, - const term = try child_process.wait(); + .envp = try this_bundler.env.map.createNullDelimitedEnvMap(bun.default_allocator), - switch (term) { - .Exited => |exit_code| { - if (exit_code != 0) { - Global.exit(exit_code); - } - }, - .Signal, .Stopped => |signal| { - Global.raiseIgnoringPanicHandler(signal); - }, - .Unknown => { + .cwd = bunx_cache_dir, + .stderr = .inherit, + .stdout = .inherit, + .stdin = .inherit, + + .windows = if (Environment.isWindows) .{ + .loop = bun.JSC.EventLoopHandle.init(bun.JSC.MiniEventLoop.initGlobal(this_bundler.env)), + } else {}, + }) catch |err| { + Output.prettyErrorln("error: bunx failed to install {s} due to error {s}", .{ install_param, @errorName(err) }); + Global.exit(1); + })) { + .err => |err| { + _ = err; // autofix Global.exit(1); }, + .result => |result| result, + }; + + switch (spawn_result.status) { + .exited => |exit| { + if (exit.signal.valid()) { + Global.raiseIgnoringPanicHandler(exit.signal); + } + + if (exit.code != 0) { + Global.exit(exit.code); + } + }, + .signaled => |signal| { + Global.raiseIgnoringPanicHandler(signal); + }, + .err => |err| { + Output.prettyErrorln("error: bunx failed to install {s} due to error:\n{}", .{ install_param, err }); + Global.exit(1); + }, + else => {}, } absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, bun.pathLiteral("{s}/node_modules/.bin/{s}{s}"), .{ bunx_cache_dir, initial_bin_name, bin_extension }) catch unreachable; @@ -651,6 +662,7 @@ pub const BunxCommand = struct { try Run.runBinary( ctx, try this_bundler.fs.dirname_store.append(@TypeOf(out), out), + destination, this_bundler.fs.top_level_dir, this_bundler.env, passthrough, @@ -675,6 +687,7 @@ pub const BunxCommand = struct { try Run.runBinary( ctx, try this_bundler.fs.dirname_store.append(@TypeOf(out), out), + destination, this_bundler.fs.top_level_dir, this_bundler.env, passthrough, diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index 9c95876e80..ee21120203 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -336,7 +336,7 @@ pub const RunCommand = struct { return true; } - var argv = [_]string{ + const argv = [_]string{ shell_bin, if (Environment.isWindows) "/c" else "-c", combined_script, @@ -347,57 +347,76 @@ pub const RunCommand = struct { Output.flush(); } - var child_process = std.ChildProcess.init(&argv, allocator); + const spawn_result = switch ((bun.spawnSync(&.{ + .argv = &argv, + .argv0 = shell_bin.ptr, - var buf_map = try env.map.stdEnvMap(allocator); - defer buf_map.deinit(); - child_process.env_map = buf_map.get(); - child_process.cwd = cwd; - child_process.stderr_behavior = .Inherit; - child_process.stdin_behavior = .Inherit; - child_process.stdout_behavior = .Inherit; + // TODO: remember to free this when we add --filter or --concurrent + // in the meantime we don't need to free it. + .envp = try env.map.createNullDelimitedEnvMap(bun.default_allocator), - if (Environment.isWindows) { - try bun.WindowsSpawnWorkaround.spawnWindows(&child_process); - } else { - try child_process.spawn(); - } + .cwd = cwd, + .stderr = .inherit, + .stdout = .inherit, + .stdin = .inherit, - const result = child_process.wait() catch |err| { + .windows = if (Environment.isWindows) .{ + .loop = JSC.EventLoopHandle.init(JSC.MiniEventLoop.initGlobal(env)), + } else {}, + }) catch |err| { if (!silent) { Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ name, @errorName(err) }); } Output.flush(); return true; + })) { + .err => |err| { + if (!silent) { + Output.prettyErrorln("error: Failed to run script {s} due to error:\n{}", .{ name, err }); + } + + Output.flush(); + return true; + }, + .result => |result| result, }; - switch (result) { - .Exited => |code| { - if (code > 0) { - if (code != 2 and !silent) { - Output.prettyErrorln("error: script \"{s}\" exited with code {d}", .{ name, code }); + switch (spawn_result.status) { + .exited => |exit_code| { + if (exit_code.signal.valid() and exit_code.signal != .SIGINT and !silent) { + Output.prettyErrorln("error: script \"{s}\" was terminated by signal {}", .{ name, exit_code.signal.fmt(Output.enable_ansi_colors_stderr) }); + Output.flush(); + + Global.raiseIgnoringPanicHandler(exit_code.signal); + } + + if (exit_code.code != 0) { + if (exit_code.code != 2 and !silent) { + Output.prettyErrorln("error: script \"{s}\" exited with code {d}", .{ name, exit_code.code }); Output.flush(); } - Global.exit(code); + Global.exit(exit_code.code); } }, - .Signal => |signal| { - if (!silent) { - Output.prettyErrorln("error: script \"{s}\" was terminated by signal {}", .{ name, bun.SignalCode.from(signal).fmt(Output.enable_ansi_colors_stderr) }); - Output.flush(); - } - Global.raiseIgnoringPanicHandler(signal); + .signaled => |signal| { + if (signal.valid() and signal != .SIGINT and !silent) { + Output.prettyErrorln("error: script \"{s}\" was terminated by signal {}", .{ name, signal.fmt(Output.enable_ansi_colors_stderr) }); + Output.flush(); + + Global.raiseIgnoringPanicHandler(signal); + } }, - .Stopped => |signal| { + + .err => |err| { if (!silent) { - Output.prettyErrorln("error: script \"{s}\" was stopped by signal {}", .{ name, bun.SignalCode.from(signal).fmt(Output.enable_ansi_colors_stderr) }); - Output.flush(); + Output.prettyErrorln("error: Failed to run script {s} due to error:\n{}", .{ name, err }); } - Global.raiseIgnoringPanicHandler(signal); + Output.flush(); + return true; }, else => {}, @@ -412,7 +431,7 @@ pub const RunCommand = struct { fn basenameOrBun(str: []const u8) []const u8 { // The full path is not used here, because on windows it is dependant on the // username. Before windows we checked bun_node_dir, but this is not allowed on Windows. - if (strings.hasSuffixComptime(str, "/bun-node/node" ++ bun.exe_suffix)) { + if (strings.hasSuffixComptime(str, "/bun-node/node" ++ bun.exe_suffix) or (Environment.isWindows and strings.hasSuffixComptime(str, "\\bun-node\\node" ++ bun.exe_suffix))) { return "bun"; } return std.fs.path.basename(str); @@ -427,6 +446,7 @@ pub const RunCommand = struct { pub fn runBinary( ctx: Command.Context, executable: []const u8, + executableZ: [:0]const u8, cwd: string, env: *DotEnv.Loader, passthrough: []const string, @@ -455,6 +475,7 @@ pub const RunCommand = struct { try runBinaryWithoutBunxPath( ctx, executable, + executableZ, cwd, env, passthrough, @@ -462,9 +483,21 @@ pub const RunCommand = struct { ); } - pub fn runBinaryWithoutBunxPath( + fn runBinaryGenericError(executable: []const u8, silent: bool, err: bun.sys.Error) noreturn { + if (!silent) { + Output.prettyErrorln("error: Failed to run \"{s}\" due to:\n{}", .{ basenameOrBun(executable), err.withPath(executable) }); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + } + + Global.exit(1); + } + + fn runBinaryWithoutBunxPath( ctx: Command.Context, executable: []const u8, + executableZ: [*:0]const u8, cwd: string, env: *DotEnv.Loader, passthrough: []const string, @@ -480,90 +513,143 @@ pub const RunCommand = struct { argv = try array_list.toOwnedSlice(); } - var child_process = std.ChildProcess.init(argv, ctx.allocator); - - var buf_map = try env.map.stdEnvMap(ctx.allocator); - defer buf_map.deinit(); - child_process.cwd = cwd; - child_process.env_map = buf_map.get(); - child_process.stderr_behavior = .Inherit; - child_process.stdin_behavior = .Inherit; - child_process.stdout_behavior = .Inherit; const silent = ctx.debug.silent; + const spawn_result = bun.spawnSync(&.{ + .argv = argv, + .argv0 = executableZ, - if (Environment.isWindows) { - try bun.WindowsSpawnWorkaround.spawnWindows(&child_process); - } else { - try child_process.spawn(); - } + // TODO: remember to free this when we add --filter or --concurrent + // in the meantime we don't need to free it. + .envp = try env.map.createNullDelimitedEnvMap(bun.default_allocator), - const result = child_process.wait() catch |err| { - if (err == error.AccessDenied) { - if (comptime Environment.isPosix) { - var stat = std.mem.zeroes(std.c.Stat); - const rc = bun.C.stat(executable[0.. :0].ptr, &stat); - if (rc == 0) { - if (std.os.S.ISDIR(stat.mode)) { - if (!silent) - Output.prettyErrorln("error: Failed to run directory \"{s}\"\n", .{executable}); - if (@errorReturnTrace()) |trace| { - std.debug.dumpStackTrace(trace.*); - } - Global.exit(1); + .cwd = cwd, + .stderr = .inherit, + .stdout = .inherit, + .stdin = .inherit, + .use_execve_on_macos = silent, + + .windows = if (Environment.isWindows) .{ + .loop = JSC.EventLoopHandle.init(JSC.MiniEventLoop.initGlobal(env)), + } else {}, + }) catch |err| { + // an error occurred before the process was spawned + print_error: { + if (!silent) { + if (comptime Environment.isPosix) { + switch (bun.sys.stat(executable[0.. :0])) { + .result => |stat| { + if (bun.S.ISDIR(stat.mode)) { + Output.prettyErrorln("error: Failed to run directory \"{s}\"\n", .{basenameOrBun(executable)}); + break :print_error; + } + }, + .err => |err2| { + switch (err2.getErrno()) { + .NOENT, .PERM, .NOTDIR => { + Output.prettyErrorln("error: Failed to run \"{s}\" due to error:\n{}", .{ basenameOrBun(executable), err2 }); + break :print_error; + }, + else => {}, + } + }, } } - } - } - if (!silent) { - Output.prettyErrorln("error: Failed to run \"{s}\" due to error {s}", .{ basenameOrBun(executable), @errorName(err) }); + Output.prettyErrorln("error: Failed to run \"{s}\" due to {s}", .{ basenameOrBun(executable), @errorName(err) }); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + } } Global.exit(1); }; - switch (result) { - .Exited => |code| { - if (!silent) { - const is_probably_trying_to_run_a_pkg_script = - original_script_for_bun_run != null and - ((code == 1 and bun.strings.eqlComptime(original_script_for_bun_run.?, "test")) or - (code == 2 and bun.strings.eqlAnyComptime(original_script_for_bun_run.?, &.{ - "install", - "kill", - "link", - }) and ctx.positionals.len == 1)); - if (is_probably_trying_to_run_a_pkg_script) { - // if you run something like `bun run test`, you get a confusing message because - // you don't usually think about your global path, let alone "/bin/test" - // - // test exits with code 1, the other ones i listed exit with code 2 - // - // so for these script names, print the entire exe name. - Output.errGeneric("\"{s}\" exited with code {d}", .{ executable, code }); - Output.note("a package.json script \"{s}\" was not found", .{original_script_for_bun_run.?}); - } - // 128 + 2 is the exit code of a process killed by SIGINT, which is caused by CTRL + C - else if (code > 0 and code != 130) { - Output.errGeneric("\"{s}\" exited with code {d}", .{ basenameOrBun(executable), code }); - } - } - Global.exit(code); + switch (spawn_result) { + .err => |err| { + // an error occurred while spawning the process + runBinaryGenericError(executable, silent, err); }, - .Signal, .Stopped => |sig| { - // forward the signal to the shell / parent process - if (sig != 0) { - Output.flush(); - Global.raiseIgnoringPanicHandler(sig); - } else if (!silent) { - std.debug.panic("\"{s}\" stopped by signal code 0, which isn't supposed to be possible", .{executable}); + .result => |result| { + switch (result.status) { + // An error occurred after the process was spawned. + .err => |err| { + runBinaryGenericError(executable, silent, err); + }, + + .signaled => |signal| { + if (!silent) { + Output.prettyErrorln("error: Failed to run \"{s}\" due to signal {s}", .{ + basenameOrBun(executable), + signal.name() orelse "unknown", + }); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + } + + Output.flush(); + Global.raiseIgnoringPanicHandler(@intFromEnum(signal)); + }, + + .exited => |exit_code| { + // A process can be both signaled and exited + if (exit_code.signal.valid()) { + if (!silent) { + Output.prettyErrorln("error: \"{s}\" exited with signal {s}", .{ + basenameOrBun(executable), + exit_code.signal.name() orelse "unknown", + }); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + } + + Output.flush(); + Global.raiseIgnoringPanicHandler(@intFromEnum(exit_code.signal)); + } + + const code = exit_code.code; + if (code != 0) { + if (!silent) { + const is_probably_trying_to_run_a_pkg_script = + original_script_for_bun_run != null and + ((code == 1 and bun.strings.eqlComptime(original_script_for_bun_run.?, "test")) or + (code == 2 and bun.strings.eqlAnyComptime(original_script_for_bun_run.?, &.{ + "install", + "kill", + "link", + }) and ctx.positionals.len == 1)); + + if (is_probably_trying_to_run_a_pkg_script) { + // if you run something like `bun run test`, you get a confusing message because + // you don't usually think about your global path, let alone "/bin/test" + // + // test exits with code 1, the other ones i listed exit with code 2 + // + // so for these script names, print the entire exe name. + Output.errGeneric("\"{s}\" exited with code {d}", .{ executable, code }); + Output.note("a package.json script \"{s}\" was not found", .{original_script_for_bun_run.?}); + } + // 128 + 2 is the exit code of a process killed by SIGINT, which is caused by CTRL + C + else if (code > 0 and code != 130) { + Output.errGeneric("\"{s}\" exited with code {d}", .{ basenameOrBun(executable), code }); + } else { + Output.prettyErrorln("error: Failed to run \"{s}\" due to exit code {d}", .{ + basenameOrBun(executable), + code, + }); + } + + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + } + } + + Global.exit(code); + }, + .running => @panic("Unexpected state: process is running"), } - Global.exit(128 + @as(u8, @as(u7, @truncate(sig)))); - }, - .Unknown => |sig| { - if (!silent) { - Output.errGeneric("\"{s}\" stopped with unknown state {d}", .{ basenameOrBun(executable), sig }); - } - Global.exit(1); }, } } @@ -1513,6 +1599,7 @@ pub const RunCommand = struct { return try runBinaryWithoutBunxPath( ctx, try this_bundler.fs.dirname_store.append(@TypeOf(out), out), + destination, this_bundler.fs.top_level_dir, this_bundler.env, passthrough, diff --git a/src/copy_file.zig b/src/copy_file.zig index efbffdbc48..80c6d519d9 100644 --- a/src/copy_file.zig +++ b/src/copy_file.zig @@ -1,3 +1,6 @@ +// Transfer all the data between two file descriptors in the most efficient way. +// The copy starts at offset 0, the initial offsets are preserved. +// No metadata is transferred over. const std = @import("std"); const os = std.os; const math = std.math; @@ -21,12 +24,41 @@ pub const CopyFileRangeError = error{ const CopyFileError = error{SystemResources} || CopyFileRangeError || os.SendFileError; -// Transfer all the data between two file descriptors in the most efficient way. -// The copy starts at offset 0, the initial offsets are preserved. -// No metadata is transferred over. - const InputType = if (Environment.isWindows) bun.OSPathSliceZ else os.fd_t; -pub fn copyFile(in: InputType, out: InputType) CopyFileError!void { + +/// In a `bun install` with prisma, this reduces the system call count from ~18,000 to ~12,000 +/// +/// The intended order here is: +/// 1. ioctl_ficlone +/// 2. copy_file_range +/// 3. sendfile() +/// 4. read() write() loop +/// +/// copy_file_range is supposed to do all the fast ways. It might be unnecessary +/// to do ioctl_ficlone. +/// +/// sendfile() is a good fallback to avoid the read-write loops. sendfile() improves +/// performance by moving the copying step to the kernel. +/// +/// On Linux, sendfile() can work between any two file descriptors which can be mmap'd. +/// This means that it cannot work with TTYs and some special devices +/// But it can work with two ordinary files +/// +/// on macoS and other platforms, sendfile() only works when one of the ends is a socket +/// and in general on macOS, it doesn't seem to have much performance impact. +const LinuxCopyFileState = packed struct { + /// This is the most important flag for reducing the system call count + /// When copying files from one folder to another, if we see EXDEV once + /// there's a very good chance we will see it for every file thereafter in that folder. + /// So we should remember whether or not we saw it and keep the state for roughly one directory tree. + has_seen_exdev: bool = false, + has_ioctl_ficlone_failed: bool = false, + has_copy_file_range_failed: bool = false, + has_sendfile_failed: bool = false, +}; +const EmptyCopyFileState = struct {}; +pub const CopyFileState = if (Environment.isLinux) LinuxCopyFileState else EmptyCopyFileState; +pub fn copyFileWithState(in: InputType, out: InputType, copy_file_state: *CopyFileState) CopyFileError!void { if (comptime Environment.isMac) { const rc = os.system.fcopyfile(in, out, null, os.system.COPYFILE_DATA); switch (os.errno(rc)) { @@ -40,25 +72,30 @@ pub fn copyFile(in: InputType, out: InputType) CopyFileError!void { } if (comptime Environment.isLinux) { - if (can_use_ioctl_ficlone()) { + if (can_use_ioctl_ficlone() and !copy_file_state.has_seen_exdev and !copy_file_state.has_ioctl_ficlone_failed) { // We only check once if the ioctl is supported, and cache the result. // EXT4 does not support FICLONE. const rc = bun.C.linux.ioctl_ficlone(bun.toFD(out), bun.toFD(in)); + // the ordering is flipped but it is consistent with other system calls. + bun.sys.syslog("ioctl_ficlone({d}, {d}) = {d}", .{ in, out, rc }); switch (std.os.linux.getErrno(rc)) { .SUCCESS => return, - .FBIG => return error.FileTooBig, - .IO => return error.InputOutput, - .ISDIR => return error.IsDir, - .NOMEM => return error.OutOfMemory, - .NOSPC => return error.NoSpaceLeft, - .OVERFLOW => return error.Unseekable, - .TXTBSY => return error.FileBusy, - .XDEV => {}, + .XDEV => { + copy_file_state.has_seen_exdev = true; + }, + + // Don't worry about EINTR here. + .INTR => {}, + .ACCES, .BADF, .INVAL, .OPNOTSUPP, .NOSYS, .PERM => { bun.Output.debug("ioctl_ficlonerange is NOT supported", .{}); can_use_ioctl_ficlone_.store(-1, .Monotonic); + copy_file_state.has_ioctl_ficlone_failed = true; + }, + else => { + // Failed for some other reason + copy_file_state.has_ioctl_ficlone_failed = true; }, - else => |err| return os.unexpectedErrno(err), } } @@ -69,7 +106,7 @@ pub fn copyFile(in: InputType, out: InputType) CopyFileError!void { // The kernel checks the u64 value `offset+count` for overflow, use // a 32 bit value so that the syscall won't return EINVAL except for // impossibly large files (> 2^64-1 - 2^32-1). - const amt = try copyFileRange(in, offset, out, offset, math.maxInt(u32), 0); + const amt = try copyFileRange(in, out, math.maxInt(i32) - 1, 0, copy_file_state); // Terminate when no data was copied if (amt == 0) break :cfr_loop; offset += amt; @@ -107,7 +144,10 @@ pub fn copyFile(in: InputType, out: InputType) CopyFileError!void { offset += amt; } } - +pub fn copyFile(in: InputType, out: InputType) CopyFileError!void { + var state: CopyFileState = .{}; + return copyFileWithState(in, out, &state); +} const Platform = @import("root").bun.analytics.GenerateHeader.GeneratePlatform; var can_use_copy_file_range = std.atomic.Value(i32).init(0); @@ -175,39 +215,70 @@ pub fn can_use_ioctl_ficlone() bool { } const fd_t = std.os.fd_t; -pub fn copyFileRange(in: fd_t, off_in: u64, out: fd_t, off_out: u64, len: usize, flags: u32) CopyFileRangeError!usize { - if (canUseCopyFileRangeSyscall()) { - var off_in_copy = @as(i64, @bitCast(off_in)); - var off_out_copy = @as(i64, @bitCast(off_out)); - const rc = std.os.linux.copy_file_range(in, &off_in_copy, out, &off_out_copy, len, flags); +pub fn copyFileRange(in: fd_t, out: fd_t, len: usize, flags: u32, copy_file_state: *CopyFileState) CopyFileRangeError!usize { + if (canUseCopyFileRangeSyscall() and !copy_file_state.has_seen_exdev and !copy_file_state.has_copy_file_range_failed) { + while (true) { + const rc = std.os.linux.copy_file_range(in, null, out, null, len, flags); + bun.sys.syslog("copy_file_range({d}, {d}, {d}) = {d}", .{ in, out, len, rc }); + switch (std.os.linux.getErrno(rc)) { + .SUCCESS => return @as(usize, @intCast(rc)), + // these may not be regular files, try fallback + .INVAL => { + copy_file_state.has_copy_file_range_failed = true; + }, + // support for cross-filesystem copy added in Linux 5.3 + // and even then, it is frequently not supported. + .XDEV => { + copy_file_state.has_seen_exdev = true; + copy_file_state.has_copy_file_range_failed = true; + }, + // syscall added in Linux 4.5, use fallback + .OPNOTSUPP, .NOSYS => { + copy_file_state.has_copy_file_range_failed = true; + bun.Output.debug("copy_file_range is NOT supported", .{}); + can_use_copy_file_range.store(-1, .Monotonic); + }, + .INTR => continue, + else => { + // failed for some other reason + copy_file_state.has_copy_file_range_failed = true; + }, + } + break; + } + } + + while (!copy_file_state.has_sendfile_failed) { + const rc = std.os.linux.sendfile(@intCast(out), @intCast(in), null, len); + bun.sys.syslog("sendfile({d}, {d}, {d}) = {d}", .{ in, out, len, rc }); switch (std.os.linux.getErrno(rc)) { .SUCCESS => return @as(usize, @intCast(rc)), - .BADF => return error.FilesOpenedWithWrongFlags, - .FBIG => return error.FileTooBig, - .IO => return error.InputOutput, - .ISDIR => return error.IsDir, - .NOMEM => return error.OutOfMemory, - .NOSPC => return error.NoSpaceLeft, - .OVERFLOW => return error.Unseekable, - .PERM => return error.PermissionDenied, - .TXTBSY => return error.FileBusy, + .INTR => continue, // these may not be regular files, try fallback - .INVAL => {}, - // support for cross-filesystem copy added in Linux 5.3, use fallback - .XDEV => {}, - // syscall added in Linux 4.5, use fallback - .NOSYS => { - bun.Output.debug("copy_file_range is NOT supported", .{}); - can_use_copy_file_range.store(-1, .Monotonic); + .INVAL => { + copy_file_state.has_sendfile_failed = true; + }, + // This shouldn't happen? + .XDEV => { + copy_file_state.has_seen_exdev = true; + copy_file_state.has_sendfile_failed = true; + }, + // they might not support it + .OPNOTSUPP, .NOSYS => { + copy_file_state.has_sendfile_failed = true; + }, + else => { + // failed for some other reason, fallback to read-write loop + copy_file_state.has_sendfile_failed = true; }, - else => |err| return os.unexpectedErrno(err), } + break; } var buf: [8 * 4096]u8 = undefined; const adjusted_count = @min(buf.len, len); - const amt_read = try os.pread(in, buf[0..adjusted_count], off_in); + const amt_read = try os.read(in, buf[0..adjusted_count]); if (amt_read == 0) return 0; - return os.pwrite(out, buf[0..amt_read], off_out); + return os.write(out, buf[0..amt_read]); } diff --git a/src/deps/libuv.zig b/src/deps/libuv.zig index 8577026fff..cb5e704673 100644 --- a/src/deps/libuv.zig +++ b/src/deps/libuv.zig @@ -408,7 +408,7 @@ pub const Handle = extern struct { fn HandleMixin(comptime Type: type) type { return struct { pub fn getData(this: *const Type, comptime DataType: type) ?*DataType { - return @ptrCast(uv_handle_get_data(@ptrCast(this))); + return @alignCast(@ptrCast(uv_handle_get_data(@ptrCast(this)))); } pub fn getLoop(this: *const Type) *Loop { return uv_handle_get_loop(@ptrCast(this)); diff --git a/src/install/install.zig b/src/install/install.zig index d102f4dc69..64ab127e95 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -1251,6 +1251,7 @@ pub const PackageInstall = struct { var in_buf: if (Environment.isWindows) bun.OSPathBuffer else void = undefined; var out_buf: if (Environment.isWindows) bun.OSPathBuffer else void = undefined; + var copy_file_state: bun.CopyFileState = .{}; while (try walker.next()) |entry| { if (entry.kind != .file) continue; @@ -1301,7 +1302,7 @@ pub const PackageInstall = struct { _ = C.fchmod(outfile.handle, @intCast(stat.mode)); } - bun.copyFile(in_file.handle, outfile.handle) catch |err| { + bun.copyFileWithState(in_file.handle, outfile.handle, ©_file_state) catch |err| { progress_.root.end(); progress_.refresh(); diff --git a/src/install/lifecycle_script_runner.zig b/src/install/lifecycle_script_runner.zig index d398103221..c72034cdec 100644 --- a/src/install/lifecycle_script_runner.zig +++ b/src/install/lifecycle_script_runner.zig @@ -80,6 +80,7 @@ pub const LifecycleScriptSubprocess = struct { return; const process = this.process orelse return; + this.handleExit(process.status); } @@ -168,6 +169,8 @@ pub const LifecycleScriptSubprocess = struct { .loop = JSC.EventLoopHandle.init(&manager.event_loop), } else {}, + + .stream = false, }; this.remaining_fds = 0; @@ -175,15 +178,24 @@ pub const LifecycleScriptSubprocess = struct { if (comptime Environment.isPosix) { if (spawned.stdout) |stdout| { - this.stdout.setParent(this); - this.remaining_fds += 1; - try this.stdout.start(stdout, true).unwrap(); + if (!spawned.memfds[1]) { + this.stdout.setParent(this); + this.remaining_fds += 1; + try this.stdout.start(stdout, true).unwrap(); + } else { + this.stdout.setParent(this); + this.stdout.startMemfd(stdout); + } } - if (spawned.stderr) |stderr| { - this.stderr.setParent(this); - this.remaining_fds += 1; - try this.stderr.start(stderr, true).unwrap(); + if (!spawned.memfds[2]) { + this.stderr.setParent(this); + this.remaining_fds += 1; + try this.stderr.start(stderr, true).unwrap(); + } else { + this.stderr.setParent(this); + this.stderr.startMemfd(stderr); + } } } else if (comptime Environment.isWindows) { if (spawned.stdout == .buffer) { @@ -217,21 +229,31 @@ pub const LifecycleScriptSubprocess = struct { pub fn printOutput(this: *LifecycleScriptSubprocess) void { if (!this.manager.options.log_level.isVerbose()) { - if (this.stdout.buffer().items.len +| this.stderr.buffer().items.len == 0) { + var stdout = this.stdout.finalBuffer(); + + // Reuse the memory + if (stdout.items.len == 0 and stdout.capacity > 0 and this.stderr.buffer().capacity == 0) { + this.stderr.buffer().* = stdout.*; + stdout.* = std.ArrayList(u8).init(bun.default_allocator); + } + + var stderr = this.stderr.finalBuffer(); + + if (stdout.items.len +| stderr.items.len == 0) { return; } Output.disableBuffering(); Output.flush(); - if (this.stdout.buffer().items.len > 0) { - Output.errorWriter().print("{s}\n", .{this.stdout.buffer().items}) catch {}; - this.stdout.buffer().clearAndFree(); + if (stdout.items.len > 0) { + Output.errorWriter().print("{s}\n", .{stdout.items}) catch {}; + stdout.clearAndFree(); } - if (this.stderr.buffer().items.len > 0) { - Output.errorWriter().print("{s}\n", .{this.stderr.buffer().items}) catch {}; - this.stderr.buffer().clearAndFree(); + if (stderr.items.len > 0) { + Output.errorWriter().print("{s}\n", .{stderr.items}) catch {}; + stderr.clearAndFree(); } Output.enableBuffering(); diff --git a/src/io/PipeReader.zig b/src/io/PipeReader.zig index 7952e00a0f..761f3e75a3 100644 --- a/src/io/PipeReader.zig +++ b/src/io/PipeReader.zig @@ -638,6 +638,7 @@ const PosixBufferedReader = struct { received_eof: bool = false, closed_without_reporting: bool = false, close_handle: bool = true, + memfd: bool = false, }; pub fn init(comptime Type: type) PosixBufferedReader { @@ -679,6 +680,11 @@ const PosixBufferedReader = struct { this.handle.setOwner(this); } + pub fn startMemfd(this: *PosixBufferedReader, fd: bun.FileDescriptor) void { + this.flags.memfd = true; + this.handle = .{ .fd = fd }; + } + pub usingnamespace PosixPipeReader(@This(), .{ .getFd = @ptrCast(&getFd), .getBuffer = @ptrCast(&buffer), @@ -747,6 +753,18 @@ const PosixBufferedReader = struct { return &@as(*PosixBufferedReader, @alignCast(@ptrCast(this)))._buffer; } + pub fn finalBuffer(this: *PosixBufferedReader) *std.ArrayList(u8) { + if (this.flags.memfd and this.handle == .fd) { + defer this.handle.close(null, {}); + _ = bun.sys.File.readToEndWithArrayList(.{ .handle = this.handle.fd }, this.buffer()).unwrap() catch |err| { + bun.Output.debugWarn("error reading from memfd\n{}", .{err}); + return this.buffer(); + }; + } + + return this.buffer(); + } + pub fn disableKeepingProcessAlive(this: *@This(), event_loop_ctx: anytype) void { _ = event_loop_ctx; // autofix this.updateRef(false); @@ -992,6 +1010,8 @@ pub const WindowsBufferedReader = struct { return &this._buffer; } + pub const finalBuffer = buffer; + pub fn hasPendingActivity(this: *const WindowsOutputReader) bool { const source = this.source orelse return false; return source.isActive(); diff --git a/src/string_builder.zig b/src/string_builder.zig index d535f4e400..18a3564874 100644 --- a/src/string_builder.zig +++ b/src/string_builder.zig @@ -23,6 +23,10 @@ pub fn initCapacity( }; } +pub fn countZ(this: *StringBuilder, slice: string) void { + this.cap += slice.len + 1; +} + pub fn count(this: *StringBuilder, slice: string) void { this.cap += slice.len; } @@ -50,6 +54,22 @@ pub fn append16(this: *StringBuilder, slice: []const u16) ?[:0]u8 { return null; } +pub fn appendZ(this: *StringBuilder, slice: string) [:0]const u8 { + if (comptime Environment.allow_assert) { + assert(this.len + 1 <= this.cap); // didn't count everything + assert(this.ptr != null); // must call allocate first + } + + bun.copy(u8, this.ptr.?[this.len..this.cap], slice); + this.ptr.?[this.len + slice.len] = 0; + const result = this.ptr.?[this.len..this.cap][0..slice.len :0]; + this.len += slice.len + 1; + + if (comptime Environment.allow_assert) assert(this.len <= this.cap); + + return result; +} + pub fn append(this: *StringBuilder, slice: string) string { if (comptime Environment.allow_assert) { assert(this.len <= this.cap); // didn't count everything diff --git a/src/sys.zig b/src/sys.zig index 6868ffa07e..4395c289a4 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -118,6 +118,7 @@ pub const Tag = enum(u8) { realpath, futime, pidfd_open, + poll, kevent, kqueue, @@ -411,6 +412,24 @@ pub fn chdir(destination: anytype) Maybe(void) { return Maybe(void).todo(); } +pub fn sendfile(src: bun.FileDescriptor, dest: bun.FileDescriptor, len: usize) Maybe(usize) { + while (true) { + const rc = std.os.linux.sendfile( + dest.cast(), + src.cast(), + null, + // we set a maximum to avoid EINVAL + @min(len, std.math.maxInt(i32) - 1), + ); + if (Maybe(usize).errnoSysFd(rc, .sendfile, src)) |err| { + if (err.getErrno() == .INTR) continue; + return err; + } + + return .{ .result = rc }; + } +} + pub fn stat(path: [:0]const u8) Maybe(bun.Stat) { if (Environment.isWindows) { return sys_uv.stat(path); @@ -2197,6 +2216,28 @@ pub fn writeNonblocking(fd: bun.FileDescriptor, buf: []const u8) Maybe(usize) { return write(fd, buf); } +pub fn getFileSize(fd: bun.FileDescriptor) Maybe(usize) { + if (Environment.isWindows) { + var size: windows.LARGE_INTEGER = undefined; + if (windows.GetFileSizeEx(fd.cast(), &size) == windows.FALSE) { + const err = Error.fromCode(windows.getLastErrno(), .fstat); + log("GetFileSizeEx({}) = {s}", .{ fd, err.name() }); + return .{ .err = err }; + } + log("GetFileSizeEx({}) = {d}", .{ fd, size }); + return .{ .result = @intCast(@max(size, 0)) }; + } + + switch (fstat(fd)) { + .result => |*stat_| { + return .{ .result = @intCast(@max(stat_.size, 0)) }; + }, + .err => |err| { + return .{ .err = err }; + }, + } +} + pub fn isPollable(mode: mode_t) bool { return os.S.ISFIFO(mode) or os.S.ISSOCK(mode); } @@ -2317,4 +2358,53 @@ pub const File = struct { // TODO: probably return the error? we have a lot of code paths which do not so we are keeping for now _ = This.close(self.handle); } + + pub fn getEndPos(self: File) Maybe(usize) { + return getFileSize(self.handle); + } + + pub const ReadToEndResult = struct { + bytes: std.ArrayList(u8) = std.ArrayList(u8).init(default_allocator), + err: ?Error = null, + }; + pub fn readToEndWithArrayList(this: File, list: *std.ArrayList(u8)) Maybe(usize) { + const size = switch (this.getEndPos()) { + .err => |err| { + return .{ .err = err }; + }, + .result => |s| s, + }; + + list.ensureUnusedCapacity(size + 16) catch bun.outOfMemory(); + + var total: i64 = 0; + while (true) { + if (list.unusedCapacitySlice().len == 0) { + list.ensureUnusedCapacity(16) catch bun.outOfMemory(); + } + + switch (bun.sys.pread(this.handle, list.unusedCapacitySlice(), total)) { + .err => |err| { + return .{ .err = err }; + }, + .result => |bytes_read| { + if (bytes_read == 0) { + break; + } + + list.items.len += bytes_read; + total += @intCast(bytes_read); + }, + } + } + + return .{ .result = @intCast(total) }; + } + pub fn readToEnd(this: File, allocator: std.mem.Allocator) ReadToEndResult { + var list = std.ArrayList(u8).init(allocator); + return switch (readToEndWithArrayList(this, &list)) { + .err => |err| .{ .err = err, .bytes = list }, + .result => .{ .err = null, .bytes = list }, + }; + } }; diff --git a/src/which.zig b/src/which.zig index 9e476f9349..32dffb8d59 100644 --- a/src/which.zig +++ b/src/which.zig @@ -77,8 +77,11 @@ pub fn endsWithExtension(str: []const u8) bool { /// Check if the WPathBuffer holds a existing file path, checking also for windows extensions variants like .exe, .cmd and .bat (internally used by whichWin) fn searchBin(buf: *bun.WPathBuffer, path_size: usize, check_windows_extensions: bool) ?[:0]const u16 { - if (bun.sys.existsOSPath(buf[0..path_size :0], true)) - return buf[0..path_size :0]; + if (!check_windows_extensions) + // On Windows, files without extensions are not executable + // Therefore, we should only care about this check when the file already has an extension. + if (bun.sys.existsOSPath(buf[0..path_size :0], true)) + return buf[0..path_size :0]; if (check_windows_extensions) { buf[path_size] = '.'; diff --git a/test/cli/install/bun-run.test.ts b/test/cli/install/bun-run.test.ts index 909137b671..c764415a19 100644 --- a/test/cli/install/bun-run.test.ts +++ b/test/cli/install/bun-run.test.ts @@ -124,29 +124,43 @@ for (let withRun of [false, true]) { expect(exitCode).toBe(200); }); - it.skipIf(isWindows)("exit signal works", async () => { - { - const { stdout, stderr, exitCode, signalCode } = spawnSync({ - cmd: [bunExe(), "run", "bash", "-c", "kill -4 $$"], - cwd: run_dir, - env: bunEnv, - }); + describe.each(["--silent", "not silent"])("%s", silentOption => { + const silent = silentOption === "--silent"; + it("exit signal works", async () => { + { + const { stdout, stderr, exitCode, signalCode } = spawnSync({ + cmd: [bunExe(), silent ? "--silent" : "", "run", "bash", "-c", "kill -4 $$"].filter(Boolean), + cwd: run_dir, + env: bunEnv, + }); - expect(stderr.toString()).toBe(""); - expect(signalCode).toBe("SIGILL"); - expect(exitCode).toBe(null); - } - { - const { stdout, stderr, exitCode, signalCode } = spawnSync({ - cmd: [bunExe(), "run", "bash", "-c", "kill -9 $$"], - cwd: run_dir, - env: bunEnv, - }); + if (silent) { + expect(stderr.toString()).toBe(""); + } else { + expect(stderr.toString()).toContain("bash"); + expect(stderr.toString()).toContain("SIGILL"); + } - expect(stderr.toString()).toBe(""); - expect(signalCode).toBe("SIGKILL"); - expect(exitCode).toBe(null); - } + expect(signalCode).toBe("SIGILL"); + expect(exitCode).toBe(null); + } + { + const { stdout, stderr, exitCode, signalCode } = spawnSync({ + cmd: [bunExe(), silent ? "--silent" : "", "run", "bash", "-c", "kill -9 $$"], + cwd: run_dir, + env: bunEnv, + }); + + if (silent) { + expect(stderr.toString()).toBe(""); + } else { + expect(stderr.toString()).toContain("bash"); + expect(stderr.toString()).toContain("SIGKILL"); + } + expect(signalCode).toBe("SIGKILL"); + expect(exitCode).toBe(null); + } + }); }); for (let withLogLevel of [true, false]) { diff --git a/test/js/bun/console/console-iterator.test.ts b/test/js/bun/console/console-iterator.test.ts index 6c10625440..a95b57d9ce 100644 --- a/test/js/bun/console/console-iterator.test.ts +++ b/test/js/bun/console/console-iterator.test.ts @@ -1,6 +1,6 @@ import { spawnSync, spawn } from "bun"; import { describe, expect, it } from "bun:test"; -import { bunExe } from "harness"; +import { bunEnv, bunExe } from "harness"; describe("should work for static input", () => { const inputs = [ @@ -18,9 +18,7 @@ describe("should work for static input", () => { const { stdout } = spawnSync({ cmd: [bunExe(), import.meta.dir + "/" + "console-iterator-run.ts"], stdin: Buffer.from(input), - env: { - BUN_DEBUG_QUIET_LOGS: "1", - }, + env: bunEnv, }); expect(stdout.toString()).toBe(input.replaceAll("\n", "")); }); @@ -44,17 +42,14 @@ describe("should work for streaming input", () => { cmd: [bunExe(), import.meta.dir + "/" + "console-iterator-run.ts"], stdin: "pipe", stdout: "pipe", - env: { - BUN_DEBUG_QUIET_LOGS: "1", - }, + env: bunEnv, }); const { stdout, stdin } = proc; stdin.write(input.slice(0, (input.length / 2) | 0)); stdin.flush(); await new Promise(resolve => setTimeout(resolve, 1)); stdin.write(input.slice((input.length / 2) | 0)); - stdin.flush(); - stdin.end(); + await stdin.end(); expect(await new Response(stdout).text()).toBe(input.replaceAll("\n", "")); proc.kill(0); @@ -68,14 +63,11 @@ it("can use the console iterator more than once", async () => { cmd: [bunExe(), import.meta.dir + "/" + "console-iterator-run-2.ts"], stdin: "pipe", stdout: "pipe", - env: { - BUN_DEBUG_QUIET_LOGS: "1", - }, + env: bunEnv, }); const { stdout, stdin } = proc; stdin.write("hello\nworld\nbreak\nanother\nbreak\n"); - stdin.flush(); - stdin.end(); + await stdin.end(); expect(await new Response(stdout).text()).toBe('["hello","world"]["another"]'); proc.kill(0); diff --git a/test/js/bun/spawn/spawn.test.ts b/test/js/bun/spawn/spawn.test.ts index 96cd78fdd7..81e8a31f03 100644 --- a/test/js/bun/spawn/spawn.test.ts +++ b/test/js/bun/spawn/spawn.test.ts @@ -1,7 +1,7 @@ import { ArrayBufferSink, readableStreamToText, spawn, spawnSync, write } from "bun"; import { beforeAll, describe, expect, it } from "bun:test"; import { closeSync, fstatSync, openSync } from "fs"; -import { gcTick as _gcTick, bunEnv, bunExe, isWindows, withoutAggressiveGC } from "harness"; +import { gcTick as _gcTick, bunEnv, bunExe, isLinux, isMacOS, isPosix, isWindows, withoutAggressiveGC } from "harness"; import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "path"; @@ -504,8 +504,8 @@ for (let [gcTick, label] of [ }); } -// This is a posix only test -if (!process.env.BUN_FEATURE_FLAG_FORCE_WAITER_THREAD && !isWindows) { +// This is a test which should only be used when pidfd and EVTFILT_PROC is NOT available +if (!process.env.BUN_FEATURE_FLAG_FORCE_WAITER_THREAD && isPosix && !isMacOS) { it("with BUN_FEATURE_FLAG_FORCE_WAITER_THREAD", async () => { const result = spawnSync({ cmd: [bunExe(), "test", path.resolve(import.meta.path)],