diff --git a/src/SignalCode.zig b/src/SignalCode.zig new file mode 100644 index 0000000000..13fb189255 --- /dev/null +++ b/src/SignalCode.zig @@ -0,0 +1,169 @@ +pub const SignalCode = enum(u8) { + SIGHUP = 1, + SIGINT = 2, + SIGQUIT = 3, + SIGILL = 4, + SIGTRAP = 5, + SIGABRT = 6, + SIGBUS = 7, + SIGFPE = 8, + SIGKILL = 9, + SIGUSR1 = 10, + SIGSEGV = 11, + SIGUSR2 = 12, + SIGPIPE = 13, + SIGALRM = 14, + SIGTERM = 15, + SIG16 = 16, + SIGCHLD = 17, + SIGCONT = 18, + SIGSTOP = 19, + SIGTSTP = 20, + SIGTTIN = 21, + SIGTTOU = 22, + SIGURG = 23, + SIGXCPU = 24, + SIGXFSZ = 25, + SIGVTALRM = 26, + SIGPROF = 27, + SIGWINCH = 28, + SIGIO = 29, + SIGPWR = 30, + SIGSYS = 31, + _, + + // The `subprocess.kill()` method sends a signal to the child process. If no + // argument is given, the process will be sent the 'SIGTERM' signal. + pub const default = SignalCode.SIGTERM; + pub const Map = ComptimeEnumMap(SignalCode); + pub fn name(value: SignalCode) ?[]const u8 { + if (@intFromEnum(value) <= @intFromEnum(SignalCode.SIGSYS)) { + return asByteSlice(@tagName(value)); + } + + 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 { + return switch (@intFromEnum(value)) { + 1...31 => 128 +% @intFromEnum(value), + else => null, + }; + } + + pub fn description(signal: SignalCode) ?[]const u8 { + // Description names copied from fish + // https://github.com/fish-shell/fish-shell/blob/00ffc397b493f67e28f18640d3de808af29b1434/fish-rust/src/signal.rs#L420 + return switch (signal) { + .SIGHUP => "Terminal hung up", + .SIGINT => "Quit request", + .SIGQUIT => "Quit request", + .SIGILL => "Illegal instruction", + .SIGTRAP => "Trace or breakpoint trap", + .SIGABRT => "Abort", + .SIGBUS => "Misaligned address error", + .SIGFPE => "Floating point exception", + .SIGKILL => "Forced quit", + .SIGUSR1 => "User defined signal 1", + .SIGUSR2 => "User defined signal 2", + .SIGSEGV => "Address boundary error", + .SIGPIPE => "Broken pipe", + .SIGALRM => "Timer expired", + .SIGTERM => "Polite quit request", + .SIGCHLD => "Child process status changed", + .SIGCONT => "Continue previously stopped process", + .SIGSTOP => "Forced stop", + .SIGTSTP => "Stop request from job control (^Z)", + .SIGTTIN => "Stop from terminal input", + .SIGTTOU => "Stop from terminal output", + .SIGURG => "Urgent socket condition", + .SIGXCPU => "CPU time limit exceeded", + .SIGXFSZ => "File size limit exceeded", + .SIGVTALRM => "Virtual timefr expired", + .SIGPROF => "Profiling timer expired", + .SIGWINCH => "Window size change", + .SIGIO => "I/O on asynchronous file descriptor is possible", + .SIGSYS => "Bad system call", + .SIGPWR => "Power failure", + else => null, + }; + } + + pub fn from(value: anytype) SignalCode { + return @enumFromInt(std.mem.asBytes(&value)[0]); + } + + // This wrapper struct is lame, what if bun's color formatter was more versatile + const Fmt = struct { + signal: SignalCode, + enable_ansi_colors: bool, + pub fn format(this: Fmt, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + const signal = this.signal; + switch (this.enable_ansi_colors) { + inline else => |enable_ansi_colors| { + if (signal.name()) |str| if (signal.description()) |desc| { + try writer.print(Output.prettyFmt("{s} ({s})", enable_ansi_colors), .{ str, desc }); + return; + }; + try writer.print("code {d}", .{@intFromEnum(signal)}); + }, + } + } + }; + + pub fn fmt(signal: SignalCode, enable_ansi_colors: bool) Fmt { + return .{ .signal = signal, .enable_ansi_colors = enable_ansi_colors }; + } + + pub fn fromJS(arg: jsc.JSValue, globalThis: *jsc.JSGlobalObject) !SignalCode { + if (arg.getNumber()) |sig64| { + // Node does this: + if (std.math.isNan(sig64)) { + return SignalCode.default; + } + + // This matches node behavior, minus some details with the error messages: https://gist.github.com/Jarred-Sumner/23ba38682bf9d84dff2f67eb35c42ab6 + if (std.math.isInf(sig64) or @trunc(sig64) != sig64) { + return globalThis.throwInvalidArguments("Unknown signal", .{}); + } + + if (sig64 < 0) { + return globalThis.throwInvalidArguments("Invalid signal: must be >= 0", .{}); + } + + if (sig64 > 31) { + return globalThis.throwInvalidArguments("Invalid signal: must be < 32", .{}); + } + + const code: SignalCode = @enumFromInt(@as(u8, @intFromFloat(sig64))); + return code; + } else if (arg.isString()) { + if (arg.asString().length() == 0) { + return SignalCode.default; + } + const signal_code = try arg.toEnum(globalThis, "signal", SignalCode); + return signal_code; + } else if (!arg.isEmptyOrUndefinedOrNull()) { + return globalThis.throwInvalidArguments("Invalid signal: must be a string or an integer", .{}); + } + + return SignalCode.default; + } +}; + +const std = @import("std"); + +const bun = @import("bun"); +const ComptimeEnumMap = bun.ComptimeEnumMap; +const Output = bun.Output; +const asByteSlice = bun.asByteSlice; + +const jsc = bun.jsc; +const JSGlobalObject = jsc.JSGlobalObject; +const JSValue = jsc.JSValue; diff --git a/src/bun.js/api/bun/js_bun_spawn_bindings.zig b/src/bun.js/api/bun/js_bun_spawn_bindings.zig new file mode 100644 index 0000000000..63ac5f307d --- /dev/null +++ b/src/bun.js/api/bun/js_bun_spawn_bindings.zig @@ -0,0 +1,934 @@ +// This is split into a separate function to conserve stack space. +// On Windows, a single path buffer can take 64 KB. +fn getArgv0(globalThis: *jsc.JSGlobalObject, PATH: []const u8, cwd: []const u8, pretend_argv0: ?[*:0]const u8, first_cmd: JSValue, allocator: std.mem.Allocator) bun.JSError!struct { + argv0: [:0]const u8, + arg0: [:0]u8, +} { + var arg0 = try first_cmd.toSliceOrNullWithAllocator(globalThis, allocator); + defer arg0.deinit(); + // Heap allocate it to ensure we don't run out of stack space. + const path_buf: *bun.PathBuffer = try bun.default_allocator.create(bun.PathBuffer); + defer bun.default_allocator.destroy(path_buf); + + var actual_argv0: [:0]const u8 = ""; + + const argv0_to_use: []const u8 = arg0.slice(); + + // This mimicks libuv's behavior, which mimicks execvpe + // Only resolve from $PATH when the command is not an absolute path + const PATH_to_use: []const u8 = if (strings.containsChar(argv0_to_use, '/')) + "" + // If no $PATH is provided, we fallback to the one from environ + // This is already the behavior of the PATH passed in here. + else if (PATH.len > 0) + PATH + else if (comptime Environment.isPosix) + // If the user explicitly passed an empty $PATH, we fallback to the OS-specific default (which libuv also does) + bun.sliceTo(BUN_DEFAULT_PATH_FOR_SPAWN, 0) + else + ""; + + if (PATH_to_use.len == 0) { + actual_argv0 = try allocator.dupeZ(u8, argv0_to_use); + } else { + const resolved = which(path_buf, PATH_to_use, cwd, argv0_to_use) orelse { + return throwCommandNotFound(globalThis, argv0_to_use); + }; + actual_argv0 = try allocator.dupeZ(u8, resolved); + } + + return .{ + .argv0 = actual_argv0, + .arg0 = if (pretend_argv0) |p| try allocator.dupeZ(u8, bun.sliceTo(p, 0)) else try allocator.dupeZ(u8, arg0.slice()), + }; +} + +/// `argv` for `Bun.spawn` & `Bun.spawnSync` +fn getArgv(globalThis: *jsc.JSGlobalObject, args: JSValue, PATH: []const u8, cwd: []const u8, argv0: *?[*:0]const u8, allocator: std.mem.Allocator, argv: *std.ArrayList(?[*:0]const u8)) bun.JSError!void { + var cmds_array = try args.arrayIterator(globalThis); + // + 1 for argv0 + // + 1 for null terminator + argv.* = try @TypeOf(argv.*).initCapacity(allocator, cmds_array.len + 2); + + if (args.isEmptyOrUndefinedOrNull()) { + return globalThis.throwInvalidArguments("cmd must be an array of strings", .{}); + } + + if (cmds_array.len == 0) { + return globalThis.throwInvalidArguments("cmd must not be empty", .{}); + } + + const argv0_result = try getArgv0(globalThis, PATH, cwd, argv0.*, (try cmds_array.next()).?, allocator); + + argv0.* = argv0_result.argv0.ptr; + argv.appendAssumeCapacity(argv0_result.arg0.ptr); + + while (try cmds_array.next()) |value| { + const arg = try value.toBunString(globalThis); + defer arg.deref(); + + argv.appendAssumeCapacity(try arg.toOwnedSliceZ(allocator)); + } + + if (argv.items.len == 0) { + return globalThis.throwInvalidArguments("cmd must be an array of strings", .{}); + } +} + +/// Bun.spawn() calls this. +pub fn spawn(globalThis: *jsc.JSGlobalObject, args: JSValue, secondaryArgsValue: ?JSValue) bun.JSError!JSValue { + return spawnMaybeSync(globalThis, args, secondaryArgsValue, false); +} + +/// Bun.spawnSync() calls this. +pub fn spawnSync(globalThis: *jsc.JSGlobalObject, args: JSValue, secondaryArgsValue: ?JSValue) bun.JSError!JSValue { + return spawnMaybeSync(globalThis, args, secondaryArgsValue, true); +} + +pub fn spawnMaybeSync( + globalThis: *jsc.JSGlobalObject, + args_: JSValue, + secondaryArgsValue: ?JSValue, + comptime is_sync: bool, +) bun.JSError!JSValue { + if (comptime is_sync) { + // We skip this on Windows due to test failures. + if (comptime !Environment.isWindows) { + // Since the event loop is recursively called, we need to check if it's safe to recurse. + if (!bun.StackCheck.init().isSafeToRecurse()) { + return globalThis.throwStackOverflow(); + } + } + } + + var arena = bun.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var override_env = false; + var env_array = std.ArrayListUnmanaged(?[*:0]const u8){}; + var jsc_vm = globalThis.bunVM(); + + var cwd = jsc_vm.transpiler.fs.top_level_dir; + + var stdio = [3]Stdio{ + .{ .ignore = {} }, + .{ .pipe = {} }, + .{ .inherit = {} }, + }; + + if (comptime is_sync) { + stdio[1] = .{ .pipe = {} }; + stdio[2] = .{ .pipe = {} }; + } + var lazy = false; + var on_exit_callback = JSValue.zero; + var on_disconnect_callback = JSValue.zero; + var PATH = jsc_vm.transpiler.env.get("PATH") orelse ""; + var argv = std.ArrayList(?[*:0]const u8).init(allocator); + var cmd_value = JSValue.zero; + var detached = false; + var args = args_; + var maybe_ipc_mode: if (is_sync) void else ?IPC.Mode = if (is_sync) {} else null; + var ipc_callback: JSValue = .zero; + var extra_fds = std.ArrayList(bun.spawn.SpawnOptions.Stdio).init(bun.default_allocator); + var argv0: ?[*:0]const u8 = null; + var ipc_channel: i32 = -1; + var timeout: ?i32 = null; + var killSignal: SignalCode = SignalCode.default; + var maxBuffer: ?i64 = null; + + var windows_hide: bool = false; + var windows_verbatim_arguments: bool = false; + var abort_signal: ?*jsc.WebCore.AbortSignal = null; + defer { + // Ensure we clean it up on error. + if (abort_signal) |signal| { + signal.unref(); + } + } + + { + if (args.isEmptyOrUndefinedOrNull()) { + return globalThis.throwInvalidArguments("cmd must be an array", .{}); + } + + const args_type = args.jsType(); + if (args_type.isArray()) { + cmd_value = args; + args = secondaryArgsValue orelse JSValue.zero; + } else if (!args.isObject()) { + return globalThis.throwInvalidArguments("cmd must be an array", .{}); + } else if (try args.getTruthy(globalThis, "cmd")) |cmd_value_| { + cmd_value = cmd_value_; + } else { + return globalThis.throwInvalidArguments("cmd must be an array", .{}); + } + + if (args.isObject()) { + if (try args.getTruthy(globalThis, "argv0")) |argv0_| { + const argv0_str = try argv0_.getZigString(globalThis); + if (argv0_str.len > 0) { + argv0 = try argv0_str.toOwnedSliceZ(allocator); + } + } + + // need to update `cwd` before searching for executable with `Which.which` + if (try args.getTruthy(globalThis, "cwd")) |cwd_| { + const cwd_str = try cwd_.getZigString(globalThis); + if (cwd_str.len > 0) { + cwd = try cwd_str.toOwnedSliceZ(allocator); + } + } + } + + if (args != .zero and args.isObject()) { + // This must run before the stdio parsing happens + if (!is_sync) { + if (try args.getTruthy(globalThis, "ipc")) |val| { + if (val.isCell() and val.isCallable()) { + maybe_ipc_mode = ipc_mode: { + if (try args.getTruthy(globalThis, "serialization")) |mode_val| { + if (mode_val.isString()) { + break :ipc_mode try IPC.Mode.fromJS(globalThis, mode_val) orelse { + return globalThis.throwInvalidArguments("serialization must be \"json\" or \"advanced\"", .{}); + }; + } else { + if (!globalThis.hasException()) { + return globalThis.throwInvalidArgumentType("spawn", "serialization", "string"); + } + return .zero; + } + } + break :ipc_mode .advanced; + }; + + ipc_callback = val.withAsyncContextIfNeeded(globalThis); + } + } + } + + if (try args.getTruthy(globalThis, "signal")) |signal_val| { + if (signal_val.as(jsc.WebCore.AbortSignal)) |signal| { + abort_signal = signal.ref(); + } else { + return globalThis.throwInvalidArgumentTypeValue("signal", "AbortSignal", signal_val); + } + } + + if (try args.getTruthy(globalThis, "onDisconnect")) |onDisconnect_| { + if (!onDisconnect_.isCell() or !onDisconnect_.isCallable()) { + return globalThis.throwInvalidArguments("onDisconnect must be a function or undefined", .{}); + } + + on_disconnect_callback = if (comptime is_sync) + onDisconnect_ + else + onDisconnect_.withAsyncContextIfNeeded(globalThis); + } + + if (try args.getTruthy(globalThis, "onExit")) |onExit_| { + if (!onExit_.isCell() or !onExit_.isCallable()) { + return globalThis.throwInvalidArguments("onExit must be a function or undefined", .{}); + } + + on_exit_callback = if (comptime is_sync) + onExit_ + else + onExit_.withAsyncContextIfNeeded(globalThis); + } + + if (try args.getTruthy(globalThis, "env")) |env_arg| { + env_arg.ensureStillAlive(); + const object = env_arg.getObject() orelse { + return globalThis.throwInvalidArguments("env must be an object", .{}); + }; + + override_env = true; + // If the env object does not include a $PATH, it must disable path lookup for argv[0] + var NEW_PATH: []const u8 = ""; + var envp_managed = env_array.toManaged(allocator); + try appendEnvpFromJS(globalThis, object, &envp_managed, &NEW_PATH); + env_array = envp_managed.moveToUnmanaged(); + PATH = NEW_PATH; + } + + try getArgv(globalThis, cmd_value, PATH, cwd, &argv0, allocator, &argv); + + if (try args.get(globalThis, "stdio")) |stdio_val| { + if (!stdio_val.isEmptyOrUndefinedOrNull()) { + if (stdio_val.jsType().isArray()) { + var stdio_iter = try stdio_val.arrayIterator(globalThis); + var i: u31 = 0; + while (try stdio_iter.next()) |value| : (i += 1) { + try stdio[i].extract(globalThis, i, value, is_sync); + if (i == 2) + break; + } + i += 1; + + while (try stdio_iter.next()) |value| : (i += 1) { + var new_item: Stdio = undefined; + try new_item.extract(globalThis, i, value, is_sync); + + const opt = switch (new_item.asSpawnOption(i)) { + .result => |opt| opt, + .err => |e| { + return e.throwJS(globalThis); + }, + }; + if (opt == .ipc) { + ipc_channel = @intCast(extra_fds.items.len); + } + try extra_fds.append(opt); + } + } else { + return globalThis.throwInvalidArguments("stdio must be an array", .{}); + } + } + } else { + if (try args.get(globalThis, "stdin")) |value| { + try stdio[0].extract(globalThis, 0, value, is_sync); + } + + if (try args.get(globalThis, "stderr")) |value| { + try stdio[2].extract(globalThis, 2, value, is_sync); + } + + if (try args.get(globalThis, "stdout")) |value| { + try stdio[1].extract(globalThis, 1, value, is_sync); + } + } + + if (comptime !is_sync) { + if (try args.get(globalThis, "lazy")) |lazy_val| { + if (lazy_val.isBoolean()) { + lazy = lazy_val.toBoolean(); + } + } + } + + if (try args.get(globalThis, "detached")) |detached_val| { + if (detached_val.isBoolean()) { + detached = detached_val.toBoolean(); + } + } + + if (Environment.isWindows) { + if (try args.get(globalThis, "windowsHide")) |val| { + if (val.isBoolean()) { + windows_hide = val.asBoolean(); + } + } + + if (try args.get(globalThis, "windowsVerbatimArguments")) |val| { + if (val.isBoolean()) { + windows_verbatim_arguments = val.asBoolean(); + } + } + } + + if (try args.get(globalThis, "timeout")) |timeout_value| brk: { + if (timeout_value != .null) { + if (timeout_value.isNumber() and std.math.isPositiveInf(timeout_value.asNumber())) { + break :brk; + } + + const timeout_int = try globalThis.validateIntegerRange(timeout_value, u64, 0, .{ .min = 0, .field_name = "timeout" }); + if (timeout_int > 0) + timeout = @intCast(@as(u31, @truncate(timeout_int))); + } + } + + if (try args.get(globalThis, "killSignal")) |val| { + killSignal = try bun.SignalCode.fromJS(val, globalThis); + } + + if (try args.get(globalThis, "maxBuffer")) |val| { + if (val.isNumber() and val.isFinite()) { // 'Infinity' does not set maxBuffer + const value = try val.coerce(i64, globalThis); + if (value > 0) { + maxBuffer = value; + } + } + } + } else { + try getArgv(globalThis, cmd_value, PATH, cwd, &argv0, allocator, &argv); + } + } + + log("spawn maxBuffer: {?d}", .{maxBuffer}); + + if (!override_env and env_array.items.len == 0) { + env_array.items = jsc_vm.transpiler.env.map.createNullDelimitedEnvMap(allocator) catch |err| return globalThis.throwError(err, "in Bun.spawn") catch return .zero; + env_array.capacity = env_array.items.len; + } + + inline for (0..stdio.len) |fd_index| { + if (stdio[fd_index].canUseMemfd(is_sync, fd_index > 0 and maxBuffer != null)) { + if (stdio[fd_index].useMemfd(fd_index)) { + jsc_vm.counters.mark(.spawn_memfd); + } + } + } + var should_close_memfd = Environment.isLinux; + + defer { + if (should_close_memfd) { + inline for (0..stdio.len) |fd_index| { + if (stdio[fd_index] == .memfd) { + stdio[fd_index].memfd.close(); + stdio[fd_index] = .ignore; + } + } + } + } + //"NODE_CHANNEL_FD=" is 16 bytes long, 15 bytes for the number, and 1 byte for the null terminator should be enough/safe + var ipc_env_buf: [32]u8 = undefined; + if (!is_sync) if (maybe_ipc_mode) |ipc_mode| { + // IPC is currently implemented in a very limited way. + // + // Node lets you pass as many fds as you want, they all become be sockets; then, IPC is just a special + // runtime-owned version of "pipe" (in which pipe is a misleading name since they're bidirectional sockets). + // + // Bun currently only supports three fds: stdin, stdout, and stderr, which are all unidirectional + // + // And then one fd is assigned specifically and only for IPC. If the user dont specify it, we add one (default: 3). + // + // When Bun.spawn() is given an `.ipc` callback, it enables IPC as follows: + env_array.ensureUnusedCapacity(allocator, 3) catch |err| return globalThis.throwError(err, "in Bun.spawn") catch return .zero; + const ipc_fd: i32 = brk: { + if (ipc_channel == -1) { + // If the user didn't specify an IPC channel, we need to add one + ipc_channel = @intCast(extra_fds.items.len); + var ipc_extra_fd_default = Stdio{ .ipc = {} }; + const fd: i32 = ipc_channel + 3; + switch (ipc_extra_fd_default.asSpawnOption(fd)) { + .result => |opt| { + try extra_fds.append(opt); + }, + .err => |e| { + return e.throwJS(globalThis); + }, + } + break :brk fd; + } else { + break :brk @intCast(ipc_channel + 3); + } + }; + + const pipe_env = std.fmt.bufPrintZ( + &ipc_env_buf, + "NODE_CHANNEL_FD={d}", + .{ipc_fd}, + ) catch { + return globalThis.throwOutOfMemory(); + }; + env_array.appendAssumeCapacity(pipe_env); + + env_array.appendAssumeCapacity(switch (ipc_mode) { + inline else => |t| "NODE_CHANNEL_SERIALIZATION_MODE=" ++ @tagName(t), + }); + }; + + try env_array.append(allocator, null); + try argv.append(null); + + if (comptime is_sync) { + for (&stdio, 0..) |*io, i| { + io.toSync(@truncate(i)); + } + } + + // If the whole thread is supposed to do absolutely nothing while waiting, + // we can block the thread which reduces CPU usage. + // + // That means: + // - No maximum buffer + // - No timeout + // - No abort signal + // - No stdin, stdout, stderr pipes + // - No extra fds + // - No auto killer (for tests) + // - No execution time limit (for tests) + // - No IPC + // - No inspector (since they might want to press pause or step) + const can_block_entire_thread_to_reduce_cpu_usage_in_fast_path = (comptime Environment.isPosix and is_sync) and + abort_signal == null and + timeout == null and + maxBuffer == null and + !stdio[0].isPiped() and + !stdio[1].isPiped() and + !stdio[2].isPiped() and + extra_fds.items.len == 0 and + !jsc_vm.auto_killer.enabled and + !jsc_vm.jsc_vm.hasExecutionTimeLimit() and + !jsc_vm.isInspectorEnabled() and + !bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_SPAWNSYNC_FAST_PATH.get(); + + const spawn_options = bun.spawn.SpawnOptions{ + .cwd = cwd, + .detached = detached, + .stdin = switch (stdio[0].asSpawnOption(0)) { + .result => |opt| opt, + .err => |e| return e.throwJS(globalThis), + }, + .stdout = switch (stdio[1].asSpawnOption(1)) { + .result => |opt| opt, + .err => |e| return e.throwJS(globalThis), + }, + .stderr = switch (stdio[2].asSpawnOption(2)) { + .result => |opt| opt, + .err => |e| return e.throwJS(globalThis), + }, + .extra_fds = extra_fds.items, + .argv0 = argv0, + .can_block_entire_thread_to_reduce_cpu_usage_in_fast_path = can_block_entire_thread_to_reduce_cpu_usage_in_fast_path, + + .windows = if (Environment.isWindows) .{ + .hide_window = windows_hide, + .verbatim_arguments = windows_verbatim_arguments, + .loop = jsc.EventLoopHandle.init(jsc_vm), + }, + }; + + var spawned = switch (bun.spawn.spawnProcess( + &spawn_options, + @ptrCast(argv.items.ptr), + @ptrCast(env_array.items.ptr), + ) catch |err| switch (err) { + error.EMFILE, error.ENFILE => { + spawn_options.deinit(); + const display_path: [:0]const u8 = if (argv.items.len > 0 and argv.items[0] != null) + std.mem.sliceTo(argv.items[0].?, 0) + else + ""; + var systemerror = bun.sys.Error.fromCode(if (err == error.EMFILE) .MFILE else .NFILE, .posix_spawn).withPath(display_path).toSystemError(); + systemerror.errno = if (err == error.EMFILE) -bun.sys.UV_E.MFILE else -bun.sys.UV_E.NFILE; + return globalThis.throwValue(systemerror.toErrorInstance(globalThis)); + }, + else => { + spawn_options.deinit(); + return globalThis.throwError(err, ": failed to spawn process") catch return .zero; + }, + }) { + .err => |err| { + spawn_options.deinit(); + switch (err.getErrno()) { + .ACCES, .NOENT, .PERM, .ISDIR, .NOTDIR => |errno| { + const display_path: [:0]const u8 = if (argv.items.len > 0 and argv.items[0] != null) + std.mem.sliceTo(argv.items[0].?, 0) + else + ""; + if (display_path.len > 0) { + var systemerror = err.withPath(display_path).toSystemError(); + if (errno == .NOENT) systemerror.errno = -bun.sys.UV_E.NOENT; + return globalThis.throwValue(systemerror.toErrorInstance(globalThis)); + } + }, + else => {}, + } + + return globalThis.throwValue(err.toJS(globalThis)); + }, + .result => |result| result, + }; + + const loop = jsc_vm.eventLoop(); + + const process = spawned.toProcess(loop, is_sync); + + var subprocess = bun.new(Subprocess, .{ + .ref_count = .init(), + .globalThis = globalThis, + .process = process, + .pid_rusage = null, + .stdin = .{ .ignore = {} }, + .stdout = .{ .ignore = {} }, + .stderr = .{ .ignore = {} }, + .stdio_pipes = .{}, + .ipc_data = null, + .flags = .{ + .is_sync = is_sync, + }, + .killSignal = undefined, + }); + + const posix_ipc_fd = if (Environment.isPosix and !is_sync and maybe_ipc_mode != null) + spawned.extra_pipes.items[@intCast(ipc_channel)] + else + bun.invalid_fd; + + MaxBuf.createForSubprocess(subprocess, &subprocess.stderr_maxbuf, maxBuffer); + MaxBuf.createForSubprocess(subprocess, &subprocess.stdout_maxbuf, maxBuffer); + + var promise_for_stream: jsc.JSValue = .zero; + + // When run synchronously, subprocess isn't garbage collected + subprocess.* = Subprocess{ + .globalThis = globalThis, + .process = process, + .pid_rusage = null, + .stdin = Writable.init( + &stdio[0], + loop, + subprocess, + spawned.stdin, + &promise_for_stream, + ) catch { + subprocess.deref(); + return globalThis.throwOutOfMemory(); + }, + .stdout = Readable.init( + stdio[1], + loop, + subprocess, + spawned.stdout, + jsc_vm.allocator, + subprocess.stdout_maxbuf, + is_sync, + ), + .stderr = Readable.init( + stdio[2], + loop, + subprocess, + spawned.stderr, + jsc_vm.allocator, + subprocess.stderr_maxbuf, + is_sync, + ), + // 1. JavaScript. + // 2. Process. + .ref_count = .initExactRefs(2), + .stdio_pipes = spawned.extra_pipes.moveToUnmanaged(), + .ipc_data = if (!is_sync and comptime Environment.isWindows) + if (maybe_ipc_mode) |ipc_mode| ( // + .init(ipc_mode, .{ .subprocess = subprocess }, .uninitialized) // + ) else null + else + null, + + .flags = .{ + .is_sync = is_sync, + }, + .killSignal = killSignal, + .stderr_maxbuf = subprocess.stderr_maxbuf, + .stdout_maxbuf = subprocess.stdout_maxbuf, + }; + + subprocess.process.setExitHandler(subprocess); + + promise_for_stream.ensureStillAlive(); + subprocess.flags.is_stdin_a_readable_stream = promise_for_stream != .zero; + + if (promise_for_stream != .zero and !globalThis.hasException()) { + if (promise_for_stream.toError()) |err| { + _ = globalThis.throwValue(err) catch {}; + } + } + + if (globalThis.hasException()) { + const err = globalThis.takeException(error.JSError); + // Ensure we kill the process so we don't leave things in an unexpected state. + _ = subprocess.tryKill(subprocess.killSignal); + + if (globalThis.hasException()) { + return error.JSError; + } + + return globalThis.throwValue(err); + } + + var posix_ipc_info: if (Environment.isPosix) IPC.Socket else void = undefined; + if (Environment.isPosix and !is_sync) { + if (maybe_ipc_mode) |mode| { + if (uws.us_socket_t.fromFd( + jsc_vm.rareData().spawnIPCContext(jsc_vm), + @sizeOf(*IPC.SendQueue), + posix_ipc_fd.cast(), + 1, + )) |socket| { + subprocess.ipc_data = .init(mode, .{ .subprocess = subprocess }, .uninitialized); + posix_ipc_info = IPC.Socket.from(socket); + } + } + } + + if (subprocess.ipc_data) |*ipc_data| { + if (Environment.isPosix) { + if (posix_ipc_info.ext(*IPC.SendQueue)) |ctx| { + ctx.* = &subprocess.ipc_data.?; + subprocess.ipc_data.?.socket = .{ .open = posix_ipc_info }; + } + } else { + if (ipc_data.windowsConfigureServer( + subprocess.stdio_pipes.items[@intCast(ipc_channel)].buffer, + ).asErr()) |err| { + subprocess.deref(); + return globalThis.throwValue(err.toJS(globalThis)); + } + subprocess.stdio_pipes.items[@intCast(ipc_channel)] = .unavailable; + } + ipc_data.writeVersionPacket(globalThis); + } + + if (subprocess.stdin == .pipe and promise_for_stream == .zero) { + subprocess.stdin.pipe.signal = jsc.WebCore.streams.Signal.init(&subprocess.stdin); + } + + const out = if (comptime !is_sync) + subprocess.toJS(globalThis) + else + JSValue.zero; + if (out != .zero) { + subprocess.this_value.setWeak(out); + // Immediately upgrade to strong if there's pending activity to prevent premature GC + subprocess.updateHasPendingActivity(); + } + + var send_exit_notification = false; + + // This must go before other things happen so that the exit handler is registered before onProcessExit can potentially be called. + if (timeout) |timeout_val| { + subprocess.event_loop_timer.next = bun.timespec.msFromNow(timeout_val); + globalThis.bunVM().timer.insert(&subprocess.event_loop_timer); + subprocess.setEventLoopTimerRefd(true); + } + + if (comptime !is_sync) { + bun.debugAssert(out != .zero); + + if (on_exit_callback.isCell()) { + jsc.Codegen.JSSubprocess.onExitCallbackSetCached(out, globalThis, on_exit_callback); + } + if (on_disconnect_callback.isCell()) { + jsc.Codegen.JSSubprocess.onDisconnectCallbackSetCached(out, globalThis, on_disconnect_callback); + } + if (ipc_callback.isCell()) { + jsc.Codegen.JSSubprocess.ipcCallbackSetCached(out, globalThis, ipc_callback); + } + + if (stdio[0] == .readable_stream) { + jsc.Codegen.JSSubprocess.stdinSetCached(out, globalThis, stdio[0].readable_stream.value); + } + + switch (subprocess.process.watch()) { + .result => {}, + .err => { + send_exit_notification = true; + lazy = false; + }, + } + } + + defer { + if (send_exit_notification) { + if (subprocess.process.hasExited()) { + // process has already exited, we called wait4(), but we did not call onProcessExit() + subprocess.process.onExit(subprocess.process.status, &std.mem.zeroes(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 + subprocess.process.wait(is_sync); + } + } + } + + if (subprocess.stdin == .buffer) { + if (subprocess.stdin.buffer.start().asErr()) |err| { + _ = subprocess.tryKill(subprocess.killSignal); + _ = globalThis.throwValue(err.toJS(globalThis)) catch {}; + return error.JSError; + } + } + + if (subprocess.stdout == .pipe) { + if (subprocess.stdout.pipe.start(subprocess, loop).asErr()) |err| { + _ = subprocess.tryKill(subprocess.killSignal); + _ = globalThis.throwValue(err.toJS(globalThis)) catch {}; + return error.JSError; + } + if ((is_sync or !lazy) and subprocess.stdout == .pipe) { + subprocess.stdout.pipe.readAll(); + } + } + + if (subprocess.stderr == .pipe) { + if (subprocess.stderr.pipe.start(subprocess, loop).asErr()) |err| { + _ = subprocess.tryKill(subprocess.killSignal); + _ = globalThis.throwValue(err.toJS(globalThis)) catch {}; + return error.JSError; + } + + if ((is_sync or !lazy) and subprocess.stderr == .pipe) { + subprocess.stderr.pipe.readAll(); + } + } + + should_close_memfd = false; + + if (comptime !is_sync) { + // Once everything is set up, we can add the abort listener + // Adding the abort listener may call the onAbortSignal callback immediately if it was already aborted + // Therefore, we must do this at the very end. + if (abort_signal) |signal| { + signal.pendingActivityRef(); + subprocess.abort_signal = signal.addListener(subprocess, Subprocess.onAbortSignal); + abort_signal = null; + } + if (!subprocess.process.hasExited()) { + jsc_vm.onSubprocessSpawn(subprocess.process); + } + return out; + } + + comptime bun.assert(is_sync); + + if (can_block_entire_thread_to_reduce_cpu_usage_in_fast_path) { + jsc_vm.counters.mark(.spawnSync_blocking); + const debug_timer = Output.DebugTimer.start(); + subprocess.process.wait(true); + log("spawnSync fast path took {}", .{debug_timer}); + + // watchOrReap will handle the already exited case for us. + } + + switch (subprocess.process.watchOrReap()) { + .result => { + // Once everything is set up, we can add the abort listener + // Adding the abort listener may call the onAbortSignal callback immediately if it was already aborted + // Therefore, we must do this at the very end. + if (abort_signal) |signal| { + signal.pendingActivityRef(); + subprocess.abort_signal = signal.addListener(subprocess, Subprocess.onAbortSignal); + abort_signal = null; + } + }, + .err => { + subprocess.process.wait(true); + }, + } + + if (!subprocess.process.hasExited()) { + jsc_vm.onSubprocessSpawn(subprocess.process); + } + + // We cannot release heap access while JS is running + { + const old_vm = jsc_vm.uwsLoop().internal_loop_data.jsc_vm; + jsc_vm.uwsLoop().internal_loop_data.jsc_vm = null; + defer { + jsc_vm.uwsLoop().internal_loop_data.jsc_vm = old_vm; + } + while (subprocess.computeHasPendingActivity()) { + if (subprocess.stdin == .buffer) { + subprocess.stdin.buffer.watch(); + } + + if (subprocess.stderr == .pipe) { + subprocess.stderr.pipe.watch(); + } + + if (subprocess.stdout == .pipe) { + subprocess.stdout.pipe.watch(); + } + + jsc_vm.tick(); + jsc_vm.eventLoop().autoTick(); + } + } + + subprocess.updateHasPendingActivity(); + + const signalCode = subprocess.getSignalCode(globalThis); + const exitCode = subprocess.getExitCode(globalThis); + const stdout = try subprocess.stdout.toBufferedValue(globalThis); + const stderr = try subprocess.stderr.toBufferedValue(globalThis); + const resource_usage: JSValue = if (!globalThis.hasException()) try subprocess.createResourceUsageObject(globalThis) else .zero; + const exitedDueToTimeout = subprocess.event_loop_timer.state == .FIRED; + const exitedDueToMaxBuffer = subprocess.exited_due_to_maxbuf; + const resultPid = jsc.JSValue.jsNumberFromInt32(subprocess.pid()); + subprocess.finalize(); + + if (globalThis.hasException()) { + // e.g. a termination exception. + return .zero; + } + + const sync_value = jsc.JSValue.createEmptyObject(globalThis, 5 + @as(usize, @intFromBool(!signalCode.isEmptyOrUndefinedOrNull()))); + sync_value.put(globalThis, jsc.ZigString.static("exitCode"), exitCode); + if (!signalCode.isEmptyOrUndefinedOrNull()) { + sync_value.put(globalThis, jsc.ZigString.static("signalCode"), signalCode); + } + sync_value.put(globalThis, jsc.ZigString.static("stdout"), stdout); + sync_value.put(globalThis, jsc.ZigString.static("stderr"), stderr); + sync_value.put(globalThis, jsc.ZigString.static("success"), JSValue.jsBoolean(exitCode.isInt32() and exitCode.asInt32() == 0)); + sync_value.put(globalThis, jsc.ZigString.static("resourceUsage"), resource_usage); + if (timeout != null) sync_value.put(globalThis, jsc.ZigString.static("exitedDueToTimeout"), if (exitedDueToTimeout) .true else .false); + if (maxBuffer != null) sync_value.put(globalThis, jsc.ZigString.static("exitedDueToMaxBuffer"), if (exitedDueToMaxBuffer != null) .true else .false); + sync_value.put(globalThis, jsc.ZigString.static("pid"), resultPid); + + return sync_value; +} + +fn throwCommandNotFound(globalThis: *jsc.JSGlobalObject, command: []const u8) bun.JSError { + const err = jsc.SystemError{ + .message = bun.handleOom(bun.String.createFormat("Executable not found in $PATH: \"{s}\"", .{command})), + .code = bun.String.static("ENOENT"), + .errno = -bun.sys.UV_E.NOENT, + .path = bun.String.cloneUTF8(command), + }; + return globalThis.throwValue(err.toErrorInstance(globalThis)); +} + +pub fn appendEnvpFromJS(globalThis: *jsc.JSGlobalObject, object: *jsc.JSObject, envp: *std.ArrayList(?[*:0]const u8), PATH: *[]const u8) bun.JSError!void { + var object_iter = try jsc.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true }).init(globalThis, object); + defer object_iter.deinit(); + + try envp.ensureTotalCapacityPrecise(object_iter.len + + // +1 incase there's IPC + // +1 for null terminator + 2); + while (try object_iter.next()) |key| { + var value = object_iter.value; + if (value.isUndefined()) continue; + + const line = try std.fmt.allocPrintZ(envp.allocator, "{}={}", .{ key, try value.getZigString(globalThis) }); + + if (key.eqlComptime("PATH")) { + PATH.* = bun.asByteSlice(line["PATH=".len..]); + } + + try envp.append(line); + } +} + +const log = Output.scoped(.Subprocess, .hidden); +extern "C" const BUN_DEFAULT_PATH_FOR_SPAWN: [*:0]const u8; + +const IPC = @import("../../ipc.zig"); +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const bun = @import("bun"); +const Environment = bun.Environment; +const Output = bun.Output; +const SignalCode = bun.SignalCode; +const default_allocator = bun.default_allocator; +const strings = bun.strings; +const uws = bun.uws; +const which = bun.which; +const windows = bun.windows; +const MaxBuf = bun.io.MaxBuf; + +const jsc = bun.jsc; +const JSGlobalObject = jsc.JSGlobalObject; +const JSValue = jsc.JSValue; + +const Subprocess = jsc.Subprocess; +const Readable = Subprocess.Readable; +const Writable = Subprocess.Writable; + +const Process = bun.spawn.Process; +const Rusage = bun.spawn.Rusage; +const Stdio = bun.spawn.Stdio; diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index 3ec2bd7cfb..39bfa0143b 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -85,28 +85,6 @@ pub inline fn assertStdioResult(result: StdioResult) void { pub const ResourceUsage = @import("./subprocess/ResourceUsage.zig"); -pub fn appendEnvpFromJS(globalThis: *jsc.JSGlobalObject, object: *jsc.JSObject, envp: *std.ArrayList(?[*:0]const u8), PATH: *[]const u8) bun.JSError!void { - var object_iter = try jsc.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true }).init(globalThis, object); - defer object_iter.deinit(); - - try envp.ensureTotalCapacityPrecise(object_iter.len + - // +1 incase there's IPC - // +1 for null terminator - 2); - while (try object_iter.next()) |key| { - var value = object_iter.value; - if (value.isUndefined()) continue; - - const line = try std.fmt.allocPrintZ(envp.allocator, "{}={}", .{ key, try value.getZigString(globalThis) }); - - if (key.eqlComptime("PATH")) { - PATH.* = bun.asByteSlice(line["PATH=".len..]); - } - - try envp.append(line); - } -} - const log = Output.scoped(.Subprocess, .visible); pub const StdioKind = enum { stdin, @@ -338,7 +316,7 @@ pub fn asyncDispose(this: *Subprocess, global: *JSGlobalObject, callframe: *jsc. return this.getExited(this_jsvalue, global); } -fn setEventLoopTimerRefd(this: *Subprocess, refd: bool) void { +pub fn setEventLoopTimerRefd(this: *Subprocess, refd: bool) void { if (this.event_loop_timer_refd == refd) return; this.event_loop_timer_refd = refd; if (refd) { @@ -364,41 +342,6 @@ pub fn onMaxBuffer(this: *Subprocess, kind: MaxBuf.Kind) void { _ = this.tryKill(this.killSignal); } -fn parseSignal(arg: jsc.JSValue, globalThis: *jsc.JSGlobalObject) !SignalCode { - if (arg.getNumber()) |sig64| { - // Node does this: - if (std.math.isNan(sig64)) { - return SignalCode.default; - } - - // This matches node behavior, minus some details with the error messages: https://gist.github.com/Jarred-Sumner/23ba38682bf9d84dff2f67eb35c42ab6 - if (std.math.isInf(sig64) or @trunc(sig64) != sig64) { - return globalThis.throwInvalidArguments("Unknown signal", .{}); - } - - if (sig64 < 0) { - return globalThis.throwInvalidArguments("Invalid signal: must be >= 0", .{}); - } - - if (sig64 > 31) { - return globalThis.throwInvalidArguments("Invalid signal: must be < 32", .{}); - } - - const code: SignalCode = @enumFromInt(@as(u8, @intFromFloat(sig64))); - return code; - } else if (arg.isString()) { - if (arg.asString().length() == 0) { - return SignalCode.default; - } - const signal_code = try arg.toEnum(globalThis, "signal", SignalCode); - return signal_code; - } else if (!arg.isEmptyOrUndefinedOrNull()) { - return globalThis.throwInvalidArguments("Invalid signal: must be a string or an integer", .{}); - } - - return SignalCode.default; -} - pub fn kill( this: *Subprocess, globalThis: *JSGlobalObject, @@ -411,7 +354,7 @@ pub fn kill( const arguments = callframe.arguments_old(1); // If signal is 0, then no actual signal is sent, but error checking // is still performed. - const sig: SignalCode = try parseSignal(arguments.ptr[0], globalThis); + const sig: SignalCode = try bun.SignalCode.fromJS(arguments.ptr[0], globalThis); if (globalThis.hasException()) return .zero; @@ -886,888 +829,6 @@ pub fn getSignalCode( return jsc.JSValue.jsNull(); } -pub fn spawn(globalThis: *jsc.JSGlobalObject, args: JSValue, secondaryArgsValue: ?JSValue) bun.JSError!JSValue { - return spawnMaybeSync(globalThis, args, secondaryArgsValue, false); -} - -pub fn spawnSync(globalThis: *jsc.JSGlobalObject, args: JSValue, secondaryArgsValue: ?JSValue) bun.JSError!JSValue { - return spawnMaybeSync(globalThis, args, secondaryArgsValue, true); -} - -extern "C" const BUN_DEFAULT_PATH_FOR_SPAWN: [*:0]const u8; - -// This is split into a separate function to conserve stack space. -// On Windows, a single path buffer can take 64 KB. -fn getArgv0(globalThis: *jsc.JSGlobalObject, PATH: []const u8, cwd: []const u8, pretend_argv0: ?[*:0]const u8, first_cmd: JSValue, allocator: std.mem.Allocator) bun.JSError!struct { - argv0: [:0]const u8, - arg0: [:0]u8, -} { - var arg0 = try first_cmd.toSliceOrNullWithAllocator(globalThis, allocator); - defer arg0.deinit(); - // Heap allocate it to ensure we don't run out of stack space. - const path_buf: *bun.PathBuffer = try bun.default_allocator.create(bun.PathBuffer); - defer bun.default_allocator.destroy(path_buf); - - var actual_argv0: [:0]const u8 = ""; - - const argv0_to_use: []const u8 = arg0.slice(); - - // This mimicks libuv's behavior, which mimicks execvpe - // Only resolve from $PATH when the command is not an absolute path - const PATH_to_use: []const u8 = if (strings.containsChar(argv0_to_use, '/')) - "" - // If no $PATH is provided, we fallback to the one from environ - // This is already the behavior of the PATH passed in here. - else if (PATH.len > 0) - PATH - else if (comptime Environment.isPosix) - // If the user explicitly passed an empty $PATH, we fallback to the OS-specific default (which libuv also does) - bun.sliceTo(BUN_DEFAULT_PATH_FOR_SPAWN, 0) - else - ""; - - if (PATH_to_use.len == 0) { - actual_argv0 = try allocator.dupeZ(u8, argv0_to_use); - } else { - const resolved = which(path_buf, PATH_to_use, cwd, argv0_to_use) orelse { - return throwCommandNotFound(globalThis, argv0_to_use); - }; - actual_argv0 = try allocator.dupeZ(u8, resolved); - } - - return .{ - .argv0 = actual_argv0, - .arg0 = if (pretend_argv0) |p| try allocator.dupeZ(u8, bun.sliceTo(p, 0)) else try allocator.dupeZ(u8, arg0.slice()), - }; -} - -fn getArgv(globalThis: *jsc.JSGlobalObject, args: JSValue, PATH: []const u8, cwd: []const u8, argv0: *?[*:0]const u8, allocator: std.mem.Allocator, argv: *std.ArrayList(?[*:0]const u8)) bun.JSError!void { - var cmds_array = try args.arrayIterator(globalThis); - // + 1 for argv0 - // + 1 for null terminator - argv.* = try @TypeOf(argv.*).initCapacity(allocator, cmds_array.len + 2); - - if (args.isEmptyOrUndefinedOrNull()) { - return globalThis.throwInvalidArguments("cmd must be an array of strings", .{}); - } - - if (cmds_array.len == 0) { - return globalThis.throwInvalidArguments("cmd must not be empty", .{}); - } - - const argv0_result = try getArgv0(globalThis, PATH, cwd, argv0.*, (try cmds_array.next()).?, allocator); - - argv0.* = argv0_result.argv0.ptr; - argv.appendAssumeCapacity(argv0_result.arg0.ptr); - - while (try cmds_array.next()) |value| { - const arg = try value.toBunString(globalThis); - defer arg.deref(); - - argv.appendAssumeCapacity(try arg.toOwnedSliceZ(allocator)); - } - - if (argv.items.len == 0) { - return globalThis.throwInvalidArguments("cmd must be an array of strings", .{}); - } -} - -pub fn spawnMaybeSync( - globalThis: *jsc.JSGlobalObject, - args_: JSValue, - secondaryArgsValue: ?JSValue, - comptime is_sync: bool, -) bun.JSError!JSValue { - if (comptime is_sync) { - // We skip this on Windows due to test failures. - if (comptime !Environment.isWindows) { - // Since the event loop is recursively called, we need to check if it's safe to recurse. - if (!bun.StackCheck.init().isSafeToRecurse()) { - return globalThis.throwStackOverflow(); - } - } - } - - var arena = bun.ArenaAllocator.init(bun.default_allocator); - defer arena.deinit(); - const allocator = arena.allocator(); - - var override_env = false; - var env_array = std.ArrayListUnmanaged(?[*:0]const u8){}; - var jsc_vm = globalThis.bunVM(); - - var cwd = jsc_vm.transpiler.fs.top_level_dir; - - var stdio = [3]Stdio{ - .{ .ignore = {} }, - .{ .pipe = {} }, - .{ .inherit = {} }, - }; - - if (comptime is_sync) { - stdio[1] = .{ .pipe = {} }; - stdio[2] = .{ .pipe = {} }; - } - var lazy = false; - var on_exit_callback = JSValue.zero; - var on_disconnect_callback = JSValue.zero; - var PATH = jsc_vm.transpiler.env.get("PATH") orelse ""; - var argv = std.ArrayList(?[*:0]const u8).init(allocator); - var cmd_value = JSValue.zero; - var detached = false; - var args = args_; - var maybe_ipc_mode: if (is_sync) void else ?IPC.Mode = if (is_sync) {} else null; - var ipc_callback: JSValue = .zero; - var extra_fds = std.ArrayList(bun.spawn.SpawnOptions.Stdio).init(bun.default_allocator); - var argv0: ?[*:0]const u8 = null; - var ipc_channel: i32 = -1; - var timeout: ?i32 = null; - var killSignal: SignalCode = SignalCode.default; - var maxBuffer: ?i64 = null; - - var windows_hide: bool = false; - var windows_verbatim_arguments: bool = false; - var abort_signal: ?*jsc.WebCore.AbortSignal = null; - defer { - // Ensure we clean it up on error. - if (abort_signal) |signal| { - signal.unref(); - } - } - - { - if (args.isEmptyOrUndefinedOrNull()) { - return globalThis.throwInvalidArguments("cmd must be an array", .{}); - } - - const args_type = args.jsType(); - if (args_type.isArray()) { - cmd_value = args; - args = secondaryArgsValue orelse JSValue.zero; - } else if (!args.isObject()) { - return globalThis.throwInvalidArguments("cmd must be an array", .{}); - } else if (try args.getTruthy(globalThis, "cmd")) |cmd_value_| { - cmd_value = cmd_value_; - } else { - return globalThis.throwInvalidArguments("cmd must be an array", .{}); - } - - if (args.isObject()) { - if (try args.getTruthy(globalThis, "argv0")) |argv0_| { - const argv0_str = try argv0_.getZigString(globalThis); - if (argv0_str.len > 0) { - argv0 = try argv0_str.toOwnedSliceZ(allocator); - } - } - - // need to update `cwd` before searching for executable with `Which.which` - if (try args.getTruthy(globalThis, "cwd")) |cwd_| { - const cwd_str = try cwd_.getZigString(globalThis); - if (cwd_str.len > 0) { - cwd = try cwd_str.toOwnedSliceZ(allocator); - } - } - } - - if (args != .zero and args.isObject()) { - // This must run before the stdio parsing happens - if (!is_sync) { - if (try args.getTruthy(globalThis, "ipc")) |val| { - if (val.isCell() and val.isCallable()) { - maybe_ipc_mode = ipc_mode: { - if (try args.getTruthy(globalThis, "serialization")) |mode_val| { - if (mode_val.isString()) { - break :ipc_mode try IPC.Mode.fromJS(globalThis, mode_val) orelse { - return globalThis.throwInvalidArguments("serialization must be \"json\" or \"advanced\"", .{}); - }; - } else { - if (!globalThis.hasException()) { - return globalThis.throwInvalidArgumentType("spawn", "serialization", "string"); - } - return .zero; - } - } - break :ipc_mode .advanced; - }; - - ipc_callback = val.withAsyncContextIfNeeded(globalThis); - } - } - } - - if (try args.getTruthy(globalThis, "signal")) |signal_val| { - if (signal_val.as(jsc.WebCore.AbortSignal)) |signal| { - abort_signal = signal.ref(); - } else { - return globalThis.throwInvalidArgumentTypeValue("signal", "AbortSignal", signal_val); - } - } - - if (try args.getTruthy(globalThis, "onDisconnect")) |onDisconnect_| { - if (!onDisconnect_.isCell() or !onDisconnect_.isCallable()) { - return globalThis.throwInvalidArguments("onDisconnect must be a function or undefined", .{}); - } - - on_disconnect_callback = if (comptime is_sync) - onDisconnect_ - else - onDisconnect_.withAsyncContextIfNeeded(globalThis); - } - - if (try args.getTruthy(globalThis, "onExit")) |onExit_| { - if (!onExit_.isCell() or !onExit_.isCallable()) { - return globalThis.throwInvalidArguments("onExit must be a function or undefined", .{}); - } - - on_exit_callback = if (comptime is_sync) - onExit_ - else - onExit_.withAsyncContextIfNeeded(globalThis); - } - - if (try args.getTruthy(globalThis, "env")) |env_arg| { - env_arg.ensureStillAlive(); - const object = env_arg.getObject() orelse { - return globalThis.throwInvalidArguments("env must be an object", .{}); - }; - - override_env = true; - // If the env object does not include a $PATH, it must disable path lookup for argv[0] - var NEW_PATH: []const u8 = ""; - var envp_managed = env_array.toManaged(allocator); - try appendEnvpFromJS(globalThis, object, &envp_managed, &NEW_PATH); - env_array = envp_managed.moveToUnmanaged(); - PATH = NEW_PATH; - } - - try getArgv(globalThis, cmd_value, PATH, cwd, &argv0, allocator, &argv); - - if (try args.get(globalThis, "stdio")) |stdio_val| { - if (!stdio_val.isEmptyOrUndefinedOrNull()) { - if (stdio_val.jsType().isArray()) { - var stdio_iter = try stdio_val.arrayIterator(globalThis); - var i: u31 = 0; - while (try stdio_iter.next()) |value| : (i += 1) { - try stdio[i].extract(globalThis, i, value, is_sync); - if (i == 2) - break; - } - i += 1; - - while (try stdio_iter.next()) |value| : (i += 1) { - var new_item: Stdio = undefined; - try new_item.extract(globalThis, i, value, is_sync); - - const opt = switch (new_item.asSpawnOption(i)) { - .result => |opt| opt, - .err => |e| { - return e.throwJS(globalThis); - }, - }; - if (opt == .ipc) { - ipc_channel = @intCast(extra_fds.items.len); - } - try extra_fds.append(opt); - } - } else { - return globalThis.throwInvalidArguments("stdio must be an array", .{}); - } - } - } else { - if (try args.get(globalThis, "stdin")) |value| { - try stdio[0].extract(globalThis, 0, value, is_sync); - } - - if (try args.get(globalThis, "stderr")) |value| { - try stdio[2].extract(globalThis, 2, value, is_sync); - } - - if (try args.get(globalThis, "stdout")) |value| { - try stdio[1].extract(globalThis, 1, value, is_sync); - } - } - - if (comptime !is_sync) { - if (try args.get(globalThis, "lazy")) |lazy_val| { - if (lazy_val.isBoolean()) { - lazy = lazy_val.toBoolean(); - } - } - } - - if (try args.get(globalThis, "detached")) |detached_val| { - if (detached_val.isBoolean()) { - detached = detached_val.toBoolean(); - } - } - - if (Environment.isWindows) { - if (try args.get(globalThis, "windowsHide")) |val| { - if (val.isBoolean()) { - windows_hide = val.asBoolean(); - } - } - - if (try args.get(globalThis, "windowsVerbatimArguments")) |val| { - if (val.isBoolean()) { - windows_verbatim_arguments = val.asBoolean(); - } - } - } - - if (try args.get(globalThis, "timeout")) |timeout_value| brk: { - if (timeout_value != .null) { - if (timeout_value.isNumber() and std.math.isPositiveInf(timeout_value.asNumber())) { - break :brk; - } - - const timeout_int = try globalThis.validateIntegerRange(timeout_value, u64, 0, .{ .min = 0, .field_name = "timeout" }); - if (timeout_int > 0) - timeout = @intCast(@as(u31, @truncate(timeout_int))); - } - } - - if (try args.get(globalThis, "killSignal")) |val| { - killSignal = try parseSignal(val, globalThis); - } - - if (try args.get(globalThis, "maxBuffer")) |val| { - if (val.isNumber() and val.isFinite()) { // 'Infinity' does not set maxBuffer - const value = try val.coerce(i64, globalThis); - if (value > 0) { - maxBuffer = value; - } - } - } - } else { - try getArgv(globalThis, cmd_value, PATH, cwd, &argv0, allocator, &argv); - } - } - - log("spawn maxBuffer: {?d}", .{maxBuffer}); - - if (!override_env and env_array.items.len == 0) { - env_array.items = jsc_vm.transpiler.env.map.createNullDelimitedEnvMap(allocator) catch |err| return globalThis.throwError(err, "in Bun.spawn") catch return .zero; - env_array.capacity = env_array.items.len; - } - - inline for (0..stdio.len) |fd_index| { - if (stdio[fd_index].canUseMemfd(is_sync, fd_index > 0 and maxBuffer != null)) { - if (stdio[fd_index].useMemfd(fd_index)) { - jsc_vm.counters.mark(.spawn_memfd); - } - } - } - var should_close_memfd = Environment.isLinux; - - defer { - if (should_close_memfd) { - inline for (0..stdio.len) |fd_index| { - if (stdio[fd_index] == .memfd) { - stdio[fd_index].memfd.close(); - stdio[fd_index] = .ignore; - } - } - } - } - //"NODE_CHANNEL_FD=" is 16 bytes long, 15 bytes for the number, and 1 byte for the null terminator should be enough/safe - var ipc_env_buf: [32]u8 = undefined; - if (!is_sync) if (maybe_ipc_mode) |ipc_mode| { - // IPC is currently implemented in a very limited way. - // - // Node lets you pass as many fds as you want, they all become be sockets; then, IPC is just a special - // runtime-owned version of "pipe" (in which pipe is a misleading name since they're bidirectional sockets). - // - // Bun currently only supports three fds: stdin, stdout, and stderr, which are all unidirectional - // - // And then one fd is assigned specifically and only for IPC. If the user dont specify it, we add one (default: 3). - // - // When Bun.spawn() is given an `.ipc` callback, it enables IPC as follows: - env_array.ensureUnusedCapacity(allocator, 3) catch |err| return globalThis.throwError(err, "in Bun.spawn") catch return .zero; - const ipc_fd: i32 = brk: { - if (ipc_channel == -1) { - // If the user didn't specify an IPC channel, we need to add one - ipc_channel = @intCast(extra_fds.items.len); - var ipc_extra_fd_default = Stdio{ .ipc = {} }; - const fd: i32 = ipc_channel + 3; - switch (ipc_extra_fd_default.asSpawnOption(fd)) { - .result => |opt| { - try extra_fds.append(opt); - }, - .err => |e| { - return e.throwJS(globalThis); - }, - } - break :brk fd; - } else { - break :brk @intCast(ipc_channel + 3); - } - }; - - const pipe_env = std.fmt.bufPrintZ( - &ipc_env_buf, - "NODE_CHANNEL_FD={d}", - .{ipc_fd}, - ) catch { - return globalThis.throwOutOfMemory(); - }; - env_array.appendAssumeCapacity(pipe_env); - - env_array.appendAssumeCapacity(switch (ipc_mode) { - inline else => |t| "NODE_CHANNEL_SERIALIZATION_MODE=" ++ @tagName(t), - }); - }; - - try env_array.append(allocator, null); - try argv.append(null); - - if (comptime is_sync) { - for (&stdio, 0..) |*io, i| { - io.toSync(@truncate(i)); - } - } - - // If the whole thread is supposed to do absolutely nothing while waiting, - // we can block the thread which reduces CPU usage. - // - // That means: - // - No maximum buffer - // - No timeout - // - No abort signal - // - No stdin, stdout, stderr pipes - // - No extra fds - // - No auto killer (for tests) - // - No execution time limit (for tests) - // - No IPC - // - No inspector (since they might want to press pause or step) - const can_block_entire_thread_to_reduce_cpu_usage_in_fast_path = (comptime Environment.isPosix and is_sync) and - abort_signal == null and - timeout == null and - maxBuffer == null and - !stdio[0].isPiped() and - !stdio[1].isPiped() and - !stdio[2].isPiped() and - extra_fds.items.len == 0 and - !jsc_vm.auto_killer.enabled and - !jsc_vm.jsc_vm.hasExecutionTimeLimit() and - !jsc_vm.isInspectorEnabled() and - !bun.feature_flag.BUN_FEATURE_FLAG_DISABLE_SPAWNSYNC_FAST_PATH.get(); - - const spawn_options = bun.spawn.SpawnOptions{ - .cwd = cwd, - .detached = detached, - .stdin = switch (stdio[0].asSpawnOption(0)) { - .result => |opt| opt, - .err => |e| return e.throwJS(globalThis), - }, - .stdout = switch (stdio[1].asSpawnOption(1)) { - .result => |opt| opt, - .err => |e| return e.throwJS(globalThis), - }, - .stderr = switch (stdio[2].asSpawnOption(2)) { - .result => |opt| opt, - .err => |e| return e.throwJS(globalThis), - }, - .extra_fds = extra_fds.items, - .argv0 = argv0, - .can_block_entire_thread_to_reduce_cpu_usage_in_fast_path = can_block_entire_thread_to_reduce_cpu_usage_in_fast_path, - - .windows = if (Environment.isWindows) .{ - .hide_window = windows_hide, - .verbatim_arguments = windows_verbatim_arguments, - .loop = jsc.EventLoopHandle.init(jsc_vm), - }, - }; - - var spawned = switch (bun.spawn.spawnProcess( - &spawn_options, - @ptrCast(argv.items.ptr), - @ptrCast(env_array.items.ptr), - ) catch |err| switch (err) { - error.EMFILE, error.ENFILE => { - spawn_options.deinit(); - const display_path: [:0]const u8 = if (argv.items.len > 0 and argv.items[0] != null) - std.mem.sliceTo(argv.items[0].?, 0) - else - ""; - var systemerror = bun.sys.Error.fromCode(if (err == error.EMFILE) .MFILE else .NFILE, .posix_spawn).withPath(display_path).toSystemError(); - systemerror.errno = if (err == error.EMFILE) -bun.sys.UV_E.MFILE else -bun.sys.UV_E.NFILE; - return globalThis.throwValue(systemerror.toErrorInstance(globalThis)); - }, - else => { - spawn_options.deinit(); - return globalThis.throwError(err, ": failed to spawn process") catch return .zero; - }, - }) { - .err => |err| { - spawn_options.deinit(); - switch (err.getErrno()) { - .ACCES, .NOENT, .PERM, .ISDIR, .NOTDIR => |errno| { - const display_path: [:0]const u8 = if (argv.items.len > 0 and argv.items[0] != null) - std.mem.sliceTo(argv.items[0].?, 0) - else - ""; - if (display_path.len > 0) { - var systemerror = err.withPath(display_path).toSystemError(); - if (errno == .NOENT) systemerror.errno = -bun.sys.UV_E.NOENT; - return globalThis.throwValue(systemerror.toErrorInstance(globalThis)); - } - }, - else => {}, - } - - return globalThis.throwValue(err.toJS(globalThis)); - }, - .result => |result| result, - }; - - const loop = jsc_vm.eventLoop(); - - const process = spawned.toProcess(loop, is_sync); - - var subprocess = bun.new(Subprocess, .{ - .ref_count = .init(), - .globalThis = globalThis, - .process = process, - .pid_rusage = null, - .stdin = .{ .ignore = {} }, - .stdout = .{ .ignore = {} }, - .stderr = .{ .ignore = {} }, - .stdio_pipes = .{}, - .ipc_data = null, - .flags = .{ - .is_sync = is_sync, - }, - .killSignal = undefined, - }); - - const posix_ipc_fd = if (Environment.isPosix and !is_sync and maybe_ipc_mode != null) - spawned.extra_pipes.items[@intCast(ipc_channel)] - else - bun.invalid_fd; - - MaxBuf.createForSubprocess(subprocess, &subprocess.stderr_maxbuf, maxBuffer); - MaxBuf.createForSubprocess(subprocess, &subprocess.stdout_maxbuf, maxBuffer); - - var promise_for_stream: jsc.JSValue = .zero; - - // When run synchronously, subprocess isn't garbage collected - subprocess.* = Subprocess{ - .globalThis = globalThis, - .process = process, - .pid_rusage = null, - .stdin = Writable.init( - &stdio[0], - loop, - subprocess, - spawned.stdin, - &promise_for_stream, - ) catch { - subprocess.deref(); - return globalThis.throwOutOfMemory(); - }, - .stdout = Readable.init( - stdio[1], - loop, - subprocess, - spawned.stdout, - jsc_vm.allocator, - subprocess.stdout_maxbuf, - is_sync, - ), - .stderr = Readable.init( - stdio[2], - loop, - subprocess, - spawned.stderr, - jsc_vm.allocator, - subprocess.stderr_maxbuf, - is_sync, - ), - // 1. JavaScript. - // 2. Process. - .ref_count = .initExactRefs(2), - .stdio_pipes = spawned.extra_pipes.moveToUnmanaged(), - .ipc_data = if (!is_sync and comptime Environment.isWindows) - if (maybe_ipc_mode) |ipc_mode| ( // - .init(ipc_mode, .{ .subprocess = subprocess }, .uninitialized) // - ) else null - else - null, - - .flags = .{ - .is_sync = is_sync, - }, - .killSignal = killSignal, - .stderr_maxbuf = subprocess.stderr_maxbuf, - .stdout_maxbuf = subprocess.stdout_maxbuf, - }; - - subprocess.process.setExitHandler(subprocess); - - promise_for_stream.ensureStillAlive(); - subprocess.flags.is_stdin_a_readable_stream = promise_for_stream != .zero; - - if (promise_for_stream != .zero and !globalThis.hasException()) { - if (promise_for_stream.toError()) |err| { - _ = globalThis.throwValue(err) catch {}; - } - } - - if (globalThis.hasException()) { - const err = globalThis.takeException(error.JSError); - // Ensure we kill the process so we don't leave things in an unexpected state. - _ = subprocess.tryKill(subprocess.killSignal); - - if (globalThis.hasException()) { - return error.JSError; - } - - return globalThis.throwValue(err); - } - - var posix_ipc_info: if (Environment.isPosix) IPC.Socket else void = undefined; - if (Environment.isPosix and !is_sync) { - if (maybe_ipc_mode) |mode| { - if (uws.us_socket_t.fromFd( - jsc_vm.rareData().spawnIPCContext(jsc_vm), - @sizeOf(*IPC.SendQueue), - posix_ipc_fd.cast(), - 1, - )) |socket| { - subprocess.ipc_data = .init(mode, .{ .subprocess = subprocess }, .uninitialized); - posix_ipc_info = IPC.Socket.from(socket); - } - } - } - - if (subprocess.ipc_data) |*ipc_data| { - if (Environment.isPosix) { - if (posix_ipc_info.ext(*IPC.SendQueue)) |ctx| { - ctx.* = &subprocess.ipc_data.?; - subprocess.ipc_data.?.socket = .{ .open = posix_ipc_info }; - } - } else { - if (ipc_data.windowsConfigureServer( - subprocess.stdio_pipes.items[@intCast(ipc_channel)].buffer, - ).asErr()) |err| { - subprocess.deref(); - return globalThis.throwValue(err.toJS(globalThis)); - } - subprocess.stdio_pipes.items[@intCast(ipc_channel)] = .unavailable; - } - ipc_data.writeVersionPacket(globalThis); - } - - if (subprocess.stdin == .pipe and promise_for_stream == .zero) { - subprocess.stdin.pipe.signal = jsc.WebCore.streams.Signal.init(&subprocess.stdin); - } - - const out = if (comptime !is_sync) - subprocess.toJS(globalThis) - else - JSValue.zero; - if (out != .zero) { - subprocess.this_value.setWeak(out); - // Immediately upgrade to strong if there's pending activity to prevent premature GC - subprocess.updateHasPendingActivity(); - } - - var send_exit_notification = false; - - // This must go before other things happen so that the exit handler is registered before onProcessExit can potentially be called. - if (timeout) |timeout_val| { - subprocess.event_loop_timer.next = bun.timespec.msFromNow(timeout_val); - globalThis.bunVM().timer.insert(&subprocess.event_loop_timer); - subprocess.setEventLoopTimerRefd(true); - } - - if (comptime !is_sync) { - bun.debugAssert(out != .zero); - - if (on_exit_callback.isCell()) { - jsc.Codegen.JSSubprocess.onExitCallbackSetCached(out, globalThis, on_exit_callback); - } - if (on_disconnect_callback.isCell()) { - jsc.Codegen.JSSubprocess.onDisconnectCallbackSetCached(out, globalThis, on_disconnect_callback); - } - if (ipc_callback.isCell()) { - jsc.Codegen.JSSubprocess.ipcCallbackSetCached(out, globalThis, ipc_callback); - } - - if (stdio[0] == .readable_stream) { - jsc.Codegen.JSSubprocess.stdinSetCached(out, globalThis, stdio[0].readable_stream.value); - } - - switch (subprocess.process.watch()) { - .result => {}, - .err => { - send_exit_notification = true; - lazy = false; - }, - } - } - - defer { - if (send_exit_notification) { - if (subprocess.process.hasExited()) { - // process has already exited, we called wait4(), but we did not call onProcessExit() - subprocess.process.onExit(subprocess.process.status, &std.mem.zeroes(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 - subprocess.process.wait(is_sync); - } - } - } - - if (subprocess.stdin == .buffer) { - if (subprocess.stdin.buffer.start().asErr()) |err| { - _ = subprocess.tryKill(subprocess.killSignal); - _ = globalThis.throwValue(err.toJS(globalThis)) catch {}; - return error.JSError; - } - } - - if (subprocess.stdout == .pipe) { - if (subprocess.stdout.pipe.start(subprocess, loop).asErr()) |err| { - _ = subprocess.tryKill(subprocess.killSignal); - _ = globalThis.throwValue(err.toJS(globalThis)) catch {}; - return error.JSError; - } - if ((is_sync or !lazy) and subprocess.stdout == .pipe) { - subprocess.stdout.pipe.readAll(); - } - } - - if (subprocess.stderr == .pipe) { - if (subprocess.stderr.pipe.start(subprocess, loop).asErr()) |err| { - _ = subprocess.tryKill(subprocess.killSignal); - _ = globalThis.throwValue(err.toJS(globalThis)) catch {}; - return error.JSError; - } - - if ((is_sync or !lazy) and subprocess.stderr == .pipe) { - subprocess.stderr.pipe.readAll(); - } - } - - should_close_memfd = false; - - if (comptime !is_sync) { - // Once everything is set up, we can add the abort listener - // Adding the abort listener may call the onAbortSignal callback immediately if it was already aborted - // Therefore, we must do this at the very end. - if (abort_signal) |signal| { - signal.pendingActivityRef(); - subprocess.abort_signal = signal.addListener(subprocess, onAbortSignal); - abort_signal = null; - } - if (!subprocess.process.hasExited()) { - jsc_vm.onSubprocessSpawn(subprocess.process); - } - return out; - } - - comptime bun.assert(is_sync); - - if (can_block_entire_thread_to_reduce_cpu_usage_in_fast_path) { - jsc_vm.counters.mark(.spawnSync_blocking); - const debug_timer = Output.DebugTimer.start(); - subprocess.process.wait(true); - log("spawnSync fast path took {}", .{debug_timer}); - - // watchOrReap will handle the already exited case for us. - } - - switch (subprocess.process.watchOrReap()) { - .result => { - // Once everything is set up, we can add the abort listener - // Adding the abort listener may call the onAbortSignal callback immediately if it was already aborted - // Therefore, we must do this at the very end. - if (abort_signal) |signal| { - signal.pendingActivityRef(); - subprocess.abort_signal = signal.addListener(subprocess, onAbortSignal); - abort_signal = null; - } - }, - .err => { - subprocess.process.wait(true); - }, - } - - if (!subprocess.process.hasExited()) { - jsc_vm.onSubprocessSpawn(subprocess.process); - } - - // We cannot release heap access while JS is running - { - const old_vm = jsc_vm.uwsLoop().internal_loop_data.jsc_vm; - jsc_vm.uwsLoop().internal_loop_data.jsc_vm = null; - defer { - jsc_vm.uwsLoop().internal_loop_data.jsc_vm = old_vm; - } - while (subprocess.computeHasPendingActivity()) { - if (subprocess.stdin == .buffer) { - subprocess.stdin.buffer.watch(); - } - - if (subprocess.stderr == .pipe) { - subprocess.stderr.pipe.watch(); - } - - if (subprocess.stdout == .pipe) { - subprocess.stdout.pipe.watch(); - } - - jsc_vm.tick(); - jsc_vm.eventLoop().autoTick(); - } - } - - subprocess.updateHasPendingActivity(); - - const signalCode = subprocess.getSignalCode(globalThis); - const exitCode = subprocess.getExitCode(globalThis); - const stdout = try subprocess.stdout.toBufferedValue(globalThis); - const stderr = try subprocess.stderr.toBufferedValue(globalThis); - const resource_usage: JSValue = if (!globalThis.hasException()) try subprocess.createResourceUsageObject(globalThis) else .zero; - const exitedDueToTimeout = subprocess.event_loop_timer.state == .FIRED; - const exitedDueToMaxBuffer = subprocess.exited_due_to_maxbuf; - const resultPid = jsc.JSValue.jsNumberFromInt32(subprocess.pid()); - subprocess.finalize(); - - if (globalThis.hasException()) { - // e.g. a termination exception. - return .zero; - } - - const sync_value = jsc.JSValue.createEmptyObject(globalThis, 5 + @as(usize, @intFromBool(!signalCode.isEmptyOrUndefinedOrNull()))); - sync_value.put(globalThis, jsc.ZigString.static("exitCode"), exitCode); - if (!signalCode.isEmptyOrUndefinedOrNull()) { - sync_value.put(globalThis, jsc.ZigString.static("signalCode"), signalCode); - } - sync_value.put(globalThis, jsc.ZigString.static("stdout"), stdout); - sync_value.put(globalThis, jsc.ZigString.static("stderr"), stderr); - sync_value.put(globalThis, jsc.ZigString.static("success"), JSValue.jsBoolean(exitCode.isInt32() and exitCode.asInt32() == 0)); - sync_value.put(globalThis, jsc.ZigString.static("resourceUsage"), resource_usage); - if (timeout != null) sync_value.put(globalThis, jsc.ZigString.static("exitedDueToTimeout"), if (exitedDueToTimeout) .true else .false); - if (maxBuffer != null) sync_value.put(globalThis, jsc.ZigString.static("exitedDueToMaxBuffer"), if (exitedDueToMaxBuffer != null) .true else .false); - sync_value.put(globalThis, jsc.ZigString.static("pid"), resultPid); - - return sync_value; -} - -fn throwCommandNotFound(globalThis: *jsc.JSGlobalObject, command: []const u8) bun.JSError { - const err = jsc.SystemError{ - .message = bun.handleOom(bun.String.createFormat("Executable not found in $PATH: \"{s}\"", .{command})), - .code = bun.String.static("ENOENT"), - .errno = -bun.sys.UV_E.NOENT, - .path = bun.String.cloneUTF8(command), - }; - return globalThis.throwValue(err.toErrorInstance(globalThis)); -} - pub fn handleIPCMessage( this: *Subprocess, message: IPC.DecodedIPCMessage, @@ -1834,21 +895,19 @@ pub const StdioResult = if (Environment.isWindows) bun.spawn.WindowsSpawnResult. pub const Writable = @import("./subprocess/Writable.zig").Writable; pub const MaxBuf = bun.io.MaxBuf; - -const string = []const u8; +pub const spawnSync = js_bun_spawn_bindings.spawnSync; +pub const spawn = js_bun_spawn_bindings.spawn; const IPC = @import("../../ipc.zig"); +const js_bun_spawn_bindings = @import("./js_bun_spawn_bindings.zig"); const node_cluster_binding = @import("../../node/node_cluster_binding.zig"); const std = @import("std"); -const Allocator = std.mem.Allocator; const bun = @import("bun"); const Async = bun.Async; const Environment = bun.Environment; const Output = bun.Output; const default_allocator = bun.default_allocator; -const strings = bun.strings; -const uws = bun.uws; const webcore = bun.webcore; const which = bun.which; const CowString = bun.ptr.CowString; @@ -1860,7 +919,6 @@ const JSValue = jsc.JSValue; const PosixSpawn = bun.spawn; const Process = bun.spawn.Process; const Rusage = bun.spawn.Rusage; -const Stdio = bun.spawn.Stdio; const windows = bun.windows; const uv = windows.libuv; diff --git a/src/bun.zig b/src/bun.zig index a806089513..013fdbcf51 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -1069,129 +1069,7 @@ pub fn parseDouble(input: []const u8) !f64 { return jsc.wtf.parseDouble(input); } -pub const SignalCode = enum(u8) { - SIGHUP = 1, - SIGINT = 2, - SIGQUIT = 3, - SIGILL = 4, - SIGTRAP = 5, - SIGABRT = 6, - SIGBUS = 7, - SIGFPE = 8, - SIGKILL = 9, - SIGUSR1 = 10, - SIGSEGV = 11, - SIGUSR2 = 12, - SIGPIPE = 13, - SIGALRM = 14, - SIGTERM = 15, - SIG16 = 16, - SIGCHLD = 17, - SIGCONT = 18, - SIGSTOP = 19, - SIGTSTP = 20, - SIGTTIN = 21, - SIGTTOU = 22, - SIGURG = 23, - SIGXCPU = 24, - SIGXFSZ = 25, - SIGVTALRM = 26, - SIGPROF = 27, - SIGWINCH = 28, - SIGIO = 29, - SIGPWR = 30, - SIGSYS = 31, - _, - - // The `subprocess.kill()` method sends a signal to the child process. If no - // argument is given, the process will be sent the 'SIGTERM' signal. - pub const default = SignalCode.SIGTERM; - pub const Map = ComptimeEnumMap(SignalCode); - pub fn name(value: SignalCode) ?[]const u8 { - if (@intFromEnum(value) <= @intFromEnum(SignalCode.SIGSYS)) { - return asByteSlice(@tagName(value)); - } - - 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 { - return switch (@intFromEnum(value)) { - 1...31 => 128 +% @intFromEnum(value), - else => null, - }; - } - - pub fn description(signal: SignalCode) ?[]const u8 { - // Description names copied from fish - // https://github.com/fish-shell/fish-shell/blob/00ffc397b493f67e28f18640d3de808af29b1434/fish-rust/src/signal.rs#L420 - return switch (signal) { - .SIGHUP => "Terminal hung up", - .SIGINT => "Quit request", - .SIGQUIT => "Quit request", - .SIGILL => "Illegal instruction", - .SIGTRAP => "Trace or breakpoint trap", - .SIGABRT => "Abort", - .SIGBUS => "Misaligned address error", - .SIGFPE => "Floating point exception", - .SIGKILL => "Forced quit", - .SIGUSR1 => "User defined signal 1", - .SIGUSR2 => "User defined signal 2", - .SIGSEGV => "Address boundary error", - .SIGPIPE => "Broken pipe", - .SIGALRM => "Timer expired", - .SIGTERM => "Polite quit request", - .SIGCHLD => "Child process status changed", - .SIGCONT => "Continue previously stopped process", - .SIGSTOP => "Forced stop", - .SIGTSTP => "Stop request from job control (^Z)", - .SIGTTIN => "Stop from terminal input", - .SIGTTOU => "Stop from terminal output", - .SIGURG => "Urgent socket condition", - .SIGXCPU => "CPU time limit exceeded", - .SIGXFSZ => "File size limit exceeded", - .SIGVTALRM => "Virtual timefr expired", - .SIGPROF => "Profiling timer expired", - .SIGWINCH => "Window size change", - .SIGIO => "I/O on asynchronous file descriptor is possible", - .SIGSYS => "Bad system call", - .SIGPWR => "Power failure", - else => null, - }; - } - - pub fn from(value: anytype) SignalCode { - return @enumFromInt(std.mem.asBytes(&value)[0]); - } - - // This wrapper struct is lame, what if bun's color formatter was more versatile - const Fmt = struct { - signal: SignalCode, - enable_ansi_colors: bool, - pub fn format(this: Fmt, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - const signal = this.signal; - switch (this.enable_ansi_colors) { - inline else => |enable_ansi_colors| { - if (signal.name()) |str| if (signal.description()) |desc| { - try writer.print(Output.prettyFmt("{s} ({s})", enable_ansi_colors), .{ str, desc }); - return; - }; - try writer.print("code {d}", .{@intFromEnum(signal)}); - }, - } - } - }; - - pub fn fmt(signal: SignalCode, enable_ansi_colors: bool) Fmt { - return .{ .signal = signal, .enable_ansi_colors = enable_ansi_colors }; - } -}; +pub const SignalCode = @import("./SignalCode.zig").SignalCode; pub fn isMissingIOUring() bool { if (comptime !Environment.isLinux)