diff options
Diffstat (limited to 'src/bun.js/api')
-rw-r--r-- | src/bun.js/api/bun.classes.ts | 51 | ||||
-rw-r--r-- | src/bun.js/api/bun.zig | 761 | ||||
-rw-r--r-- | src/bun.js/api/bun/spawn.zig | 256 |
3 files changed, 1068 insertions, 0 deletions
diff --git a/src/bun.js/api/bun.classes.ts b/src/bun.js/api/bun.classes.ts new file mode 100644 index 000000000..3a74549d2 --- /dev/null +++ b/src/bun.js/api/bun.classes.ts @@ -0,0 +1,51 @@ +import { define } from "../scripts/class-definitions"; + +export default [ + define({ + name: "Subprocess", + construct: true, + finalize: true, + klass: {}, + JSType: "0b11101110", + proto: { + pid: { + getter: "getPid", + }, + stdin: { + getter: "getStdin", + cache: true, + }, + stdout: { + getter: "getStdout", + cache: true, + }, + stderr: { + getter: "getStderr", + cache: true, + }, + + ref: { + fn: "doRef", + length: 0, + }, + unref: { + fn: "doUnref", + length: 0, + }, + + kill: { + fn: "kill", + length: 1, + }, + + killed: { + getter: "getKilled", + }, + + exitStatus: { + getter: "getExitStatus", + cache: true, + }, + }, + }), +]; diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig index f36549485..739449f3f 100644 --- a/src/bun.js/api/bun.zig +++ b/src/bun.js/api/bun.zig @@ -1195,6 +1195,9 @@ pub const Class = NewClass( .which = .{ .rfn = which, }, + .spawn = .{ + .rfn = JSC.wrapWithHasContainer(Subprocess, "spawn", false, false, false), + }, }, .{ .main = .{ @@ -3367,3 +3370,761 @@ pub const JSZlib = struct { return array_buffer.toJSWithContext(globalThis.ref(), reader, reader_deallocator, null); } }; + +pub const Subprocess = struct { + pub usingnamespace JSC.Codegen.JSSubprocess; + + pid: std.os.pid_t, + stdin: Writable, + stdout: Readable, + stderr: Readable, + + killed: bool = false, + has_ref: bool = false, + + exit_promise: JSValue = JSValue.zero, + this_jsvalue: JSValue = JSValue.zero, + + exit_code: ?u8 = null, + + has_waitpid_task: bool = false, + notification_task: JSC.AnyTask = undefined, + waitpid_task: JSC.AnyTask = undefined, + wait_task: JSC.ConcurrentTask = .{}, + + finalized: bool = false, + + globalThis: *JSC.JSGlobalObject, + + pub fn constructor( + _: *JSC.JSGlobalObject, + _: *JSC.CallFrame, + ) callconv(.C) ?*Subprocess { + return null; + } + + const Readable = union(enum) { + fd: JSC.Node.FileDescriptor, + pipe: JSC.WebCore.ReadableStream, + inherit: void, + ignore: void, + closed: void, + + pub fn init(stdio: std.meta.Tag(Stdio), fd: i32, globalThis: *JSC.JSGlobalObject) Readable { + return switch (stdio) { + .inherit => Readable{ .inherit = {} }, + .ignore => Readable{ .ignore = {} }, + .pipe => brk: { + var blob = JSC.WebCore.Blob.findOrCreateFileFromPath(.{ .fd = fd }, globalThis); + defer blob.detach(); + + var stream = JSC.WebCore.ReadableStream.fromBlob(globalThis, &blob, 0); + + break :brk Readable{ .pipe = JSC.WebCore.ReadableStream.fromJS(stream, globalThis).? }; + }, + .callback, .fd, .path, .blob => Readable{ .fd = @intCast(JSC.Node.FileDescriptor, fd) }, + }; + } + + pub fn close(this: *Readable) void { + switch (this.*) { + .fd => |fd| { + _ = JSC.Node.Syscall.close(fd); + }, + .pipe => |pipe| { + pipe.done(); + }, + else => {}, + } + + this.* = .closed; + } + + pub fn toJS(this: Readable) JSValue { + switch (this) { + .fd => |fd| { + return JSValue.jsNumber(fd); + }, + .pipe => |pipe| { + return pipe.toJS(); + }, + else => { + return JSValue.jsUndefined(); + }, + } + } + }; + + pub fn getStderr( + this: *Subprocess, + _: *JSGlobalObject, + ) callconv(.C) JSValue { + return this.stderr.toJS(); + } + + pub fn getStdin( + this: *Subprocess, + globalThis: *JSGlobalObject, + ) callconv(.C) JSValue { + return this.stdin.toJS(globalThis); + } + + pub fn getStdout( + this: *Subprocess, + _: *JSGlobalObject, + ) callconv(.C) JSValue { + return this.stdout.toJS(); + } + + pub fn kill( + this: *Subprocess, + globalThis: *JSGlobalObject, + callframe: *JSC.CallFrame, + ) callconv(.C) JSValue { + var arguments = callframe.arguments(1); + var sig: i32 = 0; + + if (arguments.len > 0) { + sig = arguments.ptr[0].toInt32(); + } + + if (!(sig > -1 and sig < std.math.maxInt(u8))) { + globalThis.throwInvalidArguments("Invalid signal: must be > -1 and < 255", .{}); + return JSValue.jsUndefined(); + } + + if (this.killed) { + return JSValue.jsUndefined(); + } + + const err = std.c.kill(this.pid, sig); + if (err != 0) { + return JSC.Node.Syscall.Error.fromCode(std.c.getErrno(err), .kill).toJSC(globalThis); + } + + return JSValue.jsUndefined(); + } + + pub fn onKill( + this: *Subprocess, + ) void { + if (this.killed) { + return; + } + + this.killed = true; + this.closePorts(); + } + + pub fn closePorts(this: *Subprocess) void { + if (this.stdout == .pipe) { + this.stdout.pipe.isLocked() + this.stdout.pipe.cancel(this.globalThis); + } + + if (this.stderr == .pipe) { + this.stderr.pipe.cancel(this.globalThis); + } + + this.stdin.close(); + this.stdout.close(); + this.stderr.close(); + } + + pub fn unref(this: *Subprocess) void { + if (!this.has_ref) + return; + this.has_ref = false; + this.globalThis.bunVM().active_tasks -= 1; + } + + pub fn ref(this: *Subprocess) void { + if (this.has_ref) + return; + this.has_ref = true; + this.globalThis.bunVM().active_tasks += 1; + } + + pub fn doRef(this: *Subprocess, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSValue { + this.ref(); + return JSC.JSValue.jsUndefined(); + } + + pub fn doUnref(this: *Subprocess, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) callconv(.C) JSValue { + this.unref(); + return JSC.JSValue.jsUndefined(); + } + + pub fn getPid( + this: *Subprocess, + _: *JSGlobalObject, + ) callconv(.C) JSValue { + return JSValue.jsNumber(this.pid); + } + + pub fn getKilled( + this: *Subprocess, + _: *JSGlobalObject, + ) callconv(.C) JSValue { + return JSValue.jsBoolean(this.killed); + } + + const Writable = union(enum) { + pipe: *JSC.WebCore.FileSink, + fd: JSC.Node.FileDescriptor, + inherit: void, + ignore: void, + + pub fn init(stdio: std.meta.Tag(Stdio), fd: i32, globalThis: *JSC.JSGlobalObject) !Writable { + switch (stdio) { + .path, .pipe, .callback => { + var sink = try globalThis.bunVM().allocator.create(JSC.WebCore.FileSink); + sink.* = .{ + .opened_fd = fd, + .buffer = bun.ByteList.init(&.{}), + .allocator = globalThis.bunVM().allocator, + }; + + return Writable{ .pipe = sink }; + }, + .blob, .fd => { + return Writable{ .fd = @intCast(JSC.Node.FileDescriptor, fd) }; + }, + .inherit => { + return Writable{ .inherit = {} }; + }, + .ignore => { + return Writable{ .ignore = {} }; + }, + } + } + + pub fn toJS(this: Writable, globalThis: *JSC.JSGlobalObject) JSValue { + return switch (this) { + .pipe => |pipe| pipe.toJS(globalThis), + .fd => |fd| JSValue.jsNumber(fd), + .ignore => JSValue.jsUndefined(), + .inherit => JSValue.jsUndefined(), + }; + } + + pub fn close(this: *Writable) void { + return switch (this.*) { + .pipe => |pipe| { + _ = pipe.end(null); + }, + .fd => |fd| { + _ = JSC.Node.Syscall.close(fd); + }, + .ignore => {}, + .inherit => {}, + }; + } + }; + + pub fn finalize(this: *Subprocess) callconv(.C) void { + this.unref(); + this.closePorts(); + this.finalized = true; + + if (this.exit_code != null) + bun.default_allocator.destroy(this); + } + + pub fn getExitStatus( + this: *Subprocess, + globalThis: *JSGlobalObject, + ) callconv(.C) JSValue { + if (this.exit_code) |code| { + return JSC.JSPromise.resolvedPromiseValue(globalThis, JSC.JSValue.jsNumber(code)); + } + + if (this.exit_promise == .zero) { + this.exit_promise = JSC.JSPromise.create(globalThis).asValue(globalThis); + } + + return this.exit_promise; + } + + pub fn spawn(globalThis: *JSC.JSGlobalObject, args: JSValue) JSValue { + var arena = std.heap.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + var allocator = arena.allocator(); + + var env: [*:null]?[*:0]const u8 = undefined; + + var env_array = std.ArrayListUnmanaged(?[*:0]const u8){ + .items = &.{}, + .capacity = 0, + }; + + var cwd = globalThis.bunVM().bundler.fs.top_level_dir; + + var stdio = [3]Stdio{ + .{ .ignore = .{} }, + .{ .inherit = .{} }, + .{ .pipe = .{} }, + }; + + var PATH = globalThis.bunVM().bundler.env.get("PATH") orelse ""; + var argv: std.ArrayListUnmanaged(?[*:0]const u8) = undefined; + { + var cmd_value = args.get(globalThis, "cmd") orelse { + globalThis.throwInvalidArguments("cmd must be an array of strings", .{}); + return JSValue.jsUndefined(); + }; + + var cmds_array = cmd_value.arrayIterator(globalThis); + argv = @TypeOf(argv).initCapacity(allocator, cmds_array.len) catch { + globalThis.throw("out of memory", .{}); + return JSValue.jsUndefined(); + }; + + if (cmd_value.isEmptyOrUndefinedOrNull()) { + globalThis.throwInvalidArguments("cmd must be an array of strings", .{}); + return JSValue.jsUndefined(); + } + + if (cmds_array.len == 0) { + globalThis.throwInvalidArguments("cmd must not be empty", .{}); + return JSValue.jsUndefined(); + } + + { + var first_cmd = cmds_array.next().?; + var arg0 = first_cmd.toSlice(globalThis, allocator); + defer arg0.deinit(); + var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + var resolved = Which.which(&path_buf, PATH, cwd, arg0.slice()) orelse { + globalThis.throwInvalidArguments("cmd not in $PATH: {s}", .{arg0}); + return JSValue.jsUndefined(); + }; + argv.appendAssumeCapacity(allocator.dupeZ(u8, bun.span(resolved)) catch { + globalThis.throw("out of memory", .{}); + return JSValue.jsUndefined(); + }); + } + + while (cmds_array.next()) |value| { + argv.appendAssumeCapacity(value.getZigString(globalThis).toOwnedSliceZ(allocator) catch { + globalThis.throw("out of memory", .{}); + return JSValue.jsUndefined(); + }); + } + + if (argv.items.len == 0) { + globalThis.throwInvalidArguments("cmd must be an array of strings", .{}); + return JSValue.jsUndefined(); + } + + if (args.get(globalThis, "cwd")) |cwd_| { + if (!cwd_.isEmptyOrUndefinedOrNull()) { + cwd = cwd_.getZigString(globalThis).toOwnedSliceZ(allocator) catch { + globalThis.throw("out of memory", .{}); + return JSValue.jsUndefined(); + }; + } + } + + if (args.get(globalThis, "env")) |object| { + if (!object.isEmptyOrUndefinedOrNull()) { + if (!object.isObject()) { + globalThis.throwInvalidArguments("env must be an object", .{}); + return JSValue.jsUndefined(); + } + + var object_iter = JSC.JSPropertyIterator(.{ + .skip_empty_name = false, + .include_value = true, + }).init(globalThis, object.asObjectRef()); + defer object_iter.deinit(); + env_array.ensureTotalCapacityPrecise(allocator, object_iter.len) catch { + globalThis.throw("out of memory", .{}); + return JSValue.jsUndefined(); + }; + + while (object_iter.next()) |key| { + var value = object_iter.value; + var line = std.fmt.allocPrintZ(allocator, "{}={}", .{ key, value.getZigString(globalThis) }) catch { + globalThis.throw("out of memory", .{}); + return JSValue.jsUndefined(); + }; + + if (key.eqlComptime("PATH")) { + PATH = bun.span(line["PATH=".len..]); + } + env_array.append(allocator, line) catch { + globalThis.throw("out of memory", .{}); + return JSValue.jsUndefined(); + }; + } + } + } + + if (args.get(globalThis, "stdio")) |stdio_val| { + if (!stdio_val.isEmptyOrUndefinedOrNull()) { + if (stdio_val.jsType().isArray()) { + var stdio_iter = stdio_val.arrayIterator(globalThis); + stdio_iter.len = @minimum(stdio_iter.len, 3); + var i: usize = 0; + while (stdio_iter.next()) |value| : (i += 1) { + if (!extractStdio(globalThis, i, value, &stdio)) + return JSC.JSValue.jsUndefined(); + } + } else { + globalThis.throwInvalidArguments("stdio must be an array", .{}); + return JSValue.jsUndefined(); + } + } + } else { + if (args.get(globalThis, "stdin")) |value| { + if (!extractStdio(globalThis, std.os.STDIN_FILENO, value, &stdio)) + return JSC.JSValue.jsUndefined(); + } + + if (args.get(globalThis, "stderr")) |value| { + if (!extractStdio(globalThis, std.os.STDERR_FILENO, value, &stdio)) + return JSC.JSValue.jsUndefined(); + } + + if (args.get(globalThis, "stdout")) |value| { + if (!extractStdio(globalThis, std.os.STDOUT_FILENO, value, &stdio)) + return JSC.JSValue.jsUndefined(); + } + } + } + + var attr = PosixSpawn.Attr.init() catch { + globalThis.throw("out of memory", .{}); + return JSValue.jsUndefined(); + }; + + defer attr.deinit(); + var actions = PosixSpawn.Actions.init() catch |err| return globalThis.handleError(err, "in posix_spawn"); + attr.set( + os.darwin.POSIX_SPAWN_CLOEXEC_DEFAULT | os.darwin.POSIX_SPAWN_SETSIGDEF | os.darwin.POSIX_SPAWN_SETSIGMASK, + ) catch |err| return globalThis.handleError(err, "in posix_spawn"); + defer actions.deinit(); + + if (env_array.items.len == 0) { + env_array.items = globalThis.bunVM().bundler.env.map.createNullDelimitedEnvMap(allocator) catch |err| return globalThis.handleError(err, "in posix_spawn"); + env_array.capacity = env_array.items.len; + } + + const any_ignore = stdio[0] == .ignore or stdio[1] == .ignore or stdio[2] == .ignore; + const dev_null_fd = @intCast( + i32, + if (any_ignore) + std.os.openZ("/dev/null", std.os.O.RDONLY | std.os.O.WRONLY, 0) catch |err| { + globalThis.throw("failed to open /dev/null: {s}", .{err}); + return JSValue.jsUndefined(); + } + else + -1, + ); + + const stdin_pipe = if (stdio[0].isPiped()) os.pipe2(os.O.NONBLOCK) catch |err| { + globalThis.throw("failed to create stdin pipe: {s}", .{err}); + return JSValue.jsUndefined(); + } else undefined; + errdefer if (stdio[0].isPiped()) destroyPipe(stdin_pipe); + + const stdout_pipe = if (stdio[1].isPiped()) os.pipe2(os.O.NONBLOCK) catch |err| { + globalThis.throw("failed to create stdout pipe: {s}", .{err}); + return JSValue.jsUndefined(); + } else undefined; + errdefer if (stdio[1].isPiped()) destroyPipe(stdout_pipe); + + const stderr_pipe = if (stdio[2].isPiped()) os.pipe2(os.O.NONBLOCK) catch |err| { + globalThis.throw("failed to create stderr pipe: {s}", .{err}); + return JSValue.jsUndefined(); + } else undefined; + errdefer if (stdio[2].isPiped()) destroyPipe(stderr_pipe); + + stdio[0].setUpChildIoPosixSpawn( + &actions, + stdin_pipe, + std.os.STDIN_FILENO, + dev_null_fd, + ) catch |err| return globalThis.handleError(err, "in configuring child stdin"); + + stdio[1].setUpChildIoPosixSpawn( + &actions, + stdout_pipe, + std.os.STDOUT_FILENO, + dev_null_fd, + ) catch |err| return globalThis.handleError(err, "in configuring child stdout"); + + stdio[2].setUpChildIoPosixSpawn( + &actions, + stderr_pipe, + std.os.STDERR_FILENO, + dev_null_fd, + ) catch |err| return globalThis.handleError(err, "in configuring child stderr"); + + actions.chdir(cwd) catch |err| return globalThis.handleError(err, "in chdir()"); + + argv.append(allocator, null) catch { + globalThis.throw("out of memory", .{}); + return JSValue.jsUndefined(); + }; + + if (env_array.items.len > 0) { + env_array.append(allocator, null) catch { + globalThis.throw("out of memory", .{}); + return JSValue.jsUndefined(); + }; + env = @ptrCast(@TypeOf(env), env_array.items.ptr); + } + + const pid = switch (PosixSpawn.spawnZ(argv.items[0].?, actions, attr, @ptrCast([*:null]?[*:0]const u8, argv.items[0..].ptr), env)) { + .err => |err| return err.toJSC(globalThis), + .result => |pid_| pid_, + }; + + var subprocess = globalThis.allocator().create(Subprocess) catch { + globalThis.throw("out of memory", .{}); + return JSValue.jsUndefined(); + }; + + subprocess.* = Subprocess{ + .globalThis = globalThis, + .pid = pid, + .stdin = Writable.init(std.meta.activeTag(stdio[std.os.STDIN_FILENO]), stdin_pipe[1], globalThis) catch { + globalThis.throw("out of memory", .{}); + return JSValue.jsUndefined(); + }, + .stdout = Readable.init(std.meta.activeTag(stdio[std.os.STDOUT_FILENO]), stdout_pipe[0], globalThis), + .stderr = Readable.init(std.meta.activeTag(stdio[std.os.STDERR_FILENO]), stderr_pipe[0], globalThis), + }; + + subprocess.this_jsvalue = subprocess.toJS(globalThis); + subprocess.this_jsvalue.ensureStillAlive(); + + switch (globalThis.bunVM().poller.watch( + @intCast(JSC.Node.FileDescriptor, pid), + .process, + Subprocess, + subprocess, + )) { + .result => {}, + .err => |err| { + if (err.getErrno() == .SRCH) { + @panic("This shouldn't happen"); + } + + // process has already exited + // https://cs.github.com/libuv/libuv/blob/b00d1bd225b602570baee82a6152eaa823a84fa6/src/unix/process.c#L1007 + subprocess.onExitNotification(); + }, + } + + return subprocess.this_jsvalue; + } + + pub fn onExitNotification( + this: *Subprocess, + ) void { + this.wait(this.globalThis.bunVM()); + } + + pub fn wait(this: *Subprocess, vm: *JSC.VirtualMachine) void { + if (this.has_waitpid_task) { + return; + } + + vm.uws_event_loop.?.active -|= 1; + + this.has_waitpid_task = true; + const pid = this.pid; + const status = PosixSpawn.waitpid(pid, 0) catch |err| { + Output.debug("waitpid({d}) failed: {s}", .{ pid, @errorName(err) }); + return; + }; + + this.exit_code = @truncate(u8, status.status); + this.waitpid_task = JSC.AnyTask.New(Subprocess, onExit).init(this); + vm.eventLoop().enqueueTask(JSC.Task.init(&this.waitpid_task)); + } + + fn onExit(this: *Subprocess) void { + this.closePorts(); + + this.has_waitpid_task = false; + + if (this.exit_promise != .zero) { + var promise = this.exit_promise; + this.exit_promise = .zero; + promise.asPromise().?.resolve(this.globalThis, JSValue.jsNumber(this.exit_code.?)); + } + + this.unref(); + + if (this.finalized) { + this.finalize(); + } + } + + const os = std.os; + fn destroyPipe(pipe: [2]os.fd_t) void { + os.close(pipe[0]); + if (pipe[0] != pipe[1]) os.close(pipe[1]); + } + + const PosixSpawn = @import("./bun/spawn.zig").PosixSpawn; + + const Stdio = union(enum) { + inherit: void, + ignore: void, + fd: JSC.Node.FileDescriptor, + path: JSC.Node.PathLike, + blob: JSC.WebCore.Blob, + pipe: void, + callback: JSC.JSValue, + + pub fn isPiped(self: Stdio) bool { + return switch (self) { + .blob, .callback, .pipe => true, + else => false, + }; + } + + fn setUpChildIoPosixSpawn( + stdio: @This(), + actions: *PosixSpawn.Actions, + pipe_fd: [2]i32, + std_fileno: i32, + _: i32, + ) !void { + switch (stdio) { + .blob, .callback, .pipe => { + const idx: usize = if (std_fileno == 0) 0 else 1; + try actions.dup2(pipe_fd[idx], std_fileno); + try actions.close(pipe_fd[1 - idx]); + }, + .fd => |fd| { + try actions.dup2(fd, std_fileno); + }, + .path => |pathlike| { + const flag = if (std_fileno == std.os.STDIN_FILENO) @as(u32, os.O.WRONLY) else @as(u32, std.os.O.RDONLY); + try actions.open(std_fileno, pathlike.slice(), flag | std.os.O.CREAT, 0o664); + }, + .inherit => { + try actions.inherit(std_fileno); + }, + .ignore => { + const flag = if (std_fileno == std.os.STDIN_FILENO) @as(u32, os.O.RDONLY) else @as(u32, std.os.O.WRONLY); + try actions.openZ(std_fileno, "/dev/null", flag, 0o664); + }, + } + } + }; + + fn extractStdio( + globalThis: *JSC.JSGlobalObject, + i: usize, + value: JSValue, + stdio_array: []Stdio, + ) bool { + if (value.isEmptyOrUndefinedOrNull()) { + return true; + } + + if (value.isString()) { + const str = value.getZigString(globalThis); + if (str.eqlComptime("inherit")) { + stdio_array[i] = Stdio{ .inherit = {} }; + } else if (str.eqlComptime("ignore")) { + stdio_array[i] = Stdio{ .ignore = {} }; + } else if (str.eqlComptime("pipe")) { + stdio_array[i] = Stdio{ .pipe = {} }; + } else { + globalThis.throwInvalidArguments("stdio must be an array of 'inherit', 'ignore', or null", .{}); + return false; + } + + return true; + } else if (value.isNumber()) { + const fd_ = value.toInt64(); + if (fd_ < 0) { + globalThis.throwInvalidArguments("file descriptor must be a positive integer", .{}); + return false; + } + + const fd = @intCast(JSC.Node.FileDescriptor, fd_); + + switch (@intCast(std.os.fd_t, i)) { + std.os.STDIN_FILENO => { + if (i == std.os.STDERR_FILENO or i == std.os.STDOUT_FILENO) { + globalThis.throwInvalidArguments("stdin cannot be used for stdout or stderr", .{}); + return false; + } + }, + + std.os.STDOUT_FILENO, std.os.STDERR_FILENO => { + if (i == std.os.STDIN_FILENO) { + globalThis.throwInvalidArguments("stdout and stderr cannot be used for stdin", .{}); + return false; + } + }, + else => {}, + } + + stdio_array[i] = Stdio{ .fd = fd }; + + return true; + } else if (value.as(JSC.WebCore.Blob)) |blob| { + var store = blob.store orelse { + globalThis.throwInvalidArguments("Blob is detached (in stdio)", .{}); + return false; + }; + + if (i == std.os.STDIN_FILENO and store.data == .bytes) { + stdio_array[i] = .{ .blob = blob.dupe() }; + return true; + } + + if (store.data != .file) { + globalThis.throwInvalidArguments("Blob is not a file (in stdio)", .{}); + return false; + } + + if (store.data.file.pathlike == .fd) { + if (store.data.file.pathlike.fd == @intCast(JSC.Node.FileDescriptor, i)) { + stdio_array[i] = Stdio{ .inherit = {} }; + } else { + switch (@intCast(std.os.fd_t, i)) { + std.os.STDIN_FILENO => { + if (i == std.os.STDERR_FILENO or i == std.os.STDOUT_FILENO) { + globalThis.throwInvalidArguments("stdin cannot be used for stdout or stderr", .{}); + return false; + } + }, + + std.os.STDOUT_FILENO, std.os.STDERR_FILENO => { + if (i == std.os.STDIN_FILENO) { + globalThis.throwInvalidArguments("stdout and stderr cannot be used for stdin", .{}); + return false; + } + }, + else => {}, + } + + stdio_array[i] = Stdio{ .fd = store.data.file.pathlike.fd }; + } + + return true; + } + + stdio_array[i] = .{ .path = store.data.file.pathlike.path }; + return true; + } else if (value.isCallable(globalThis.vm())) { + stdio_array[i] = .{ .callback = value }; + value.ensureStillAlive(); + return true; + } + + globalThis.throwInvalidArguments("stdio must be an array of 'inherit', 'ignore', or null", .{}); + return false; + } +}; diff --git a/src/bun.js/api/bun/spawn.zig b/src/bun.js/api/bun/spawn.zig new file mode 100644 index 000000000..95ce7fa73 --- /dev/null +++ b/src/bun.js/api/bun/spawn.zig @@ -0,0 +1,256 @@ +const JSC = @import("javascript_core"); +const bun = @import("../../../global.zig"); +const string = bun.string; +const std = @import("std"); +const system = std.os.system; +const Maybe = JSC.Node.Maybe; + +const fd_t = std.os.fd_t; +const pid_t = std.os.pid_t; +const toPosixPath = std.os.toPosixPath; +const errno = std.os.errno; +const mode_t = std.os.mode_t; +const unexpectedErrno = std.os.unexpectedErrno; + +pub const WaitPidResult = struct { + pid: pid_t, + status: u32, +}; + +// mostly taken from zig's posix_spawn.zig +pub const PosixSpawn = struct { + pub const Attr = struct { + attr: system.posix_spawnattr_t, + + pub fn init() !Attr { + var attr: system.posix_spawnattr_t = undefined; + switch (errno(system.posix_spawnattr_init(&attr))) { + .SUCCESS => return Attr{ .attr = attr }, + .NOMEM => return error.SystemResources, + .INVAL => unreachable, + else => |err| return unexpectedErrno(err), + } + } + + pub fn deinit(self: *Attr) void { + system.posix_spawnattr_destroy(&self.attr); + self.* = undefined; + } + + pub fn get(self: Attr) !u16 { + var flags: c_short = undefined; + switch (errno(system.posix_spawnattr_getflags(&self.attr, &flags))) { + .SUCCESS => return @bitCast(u16, flags), + .INVAL => unreachable, + else => |err| return unexpectedErrno(err), + } + } + + pub fn set(self: *Attr, flags: u16) !void { + switch (errno(system.posix_spawnattr_setflags(&self.attr, @bitCast(c_short, flags)))) { + .SUCCESS => return, + .INVAL => unreachable, + else => |err| return unexpectedErrno(err), + } + } + }; + + pub const Actions = struct { + actions: system.posix_spawn_file_actions_t, + + pub fn init() !Actions { + var actions: system.posix_spawn_file_actions_t = undefined; + switch (errno(system.posix_spawn_file_actions_init(&actions))) { + .SUCCESS => return Actions{ .actions = actions }, + .NOMEM => return error.SystemResources, + .INVAL => unreachable, + else => |err| return unexpectedErrno(err), + } + } + + pub fn deinit(self: *Actions) void { + system.posix_spawn_file_actions_destroy(&self.actions); + self.* = undefined; + } + + pub fn open(self: *Actions, fd: fd_t, path: []const u8, flags: u32, mode: mode_t) !void { + const posix_path = try toPosixPath(path); + return self.openZ(fd, &posix_path, flags, mode); + } + + pub fn openZ(self: *Actions, fd: fd_t, path: [*:0]const u8, flags: u32, mode: mode_t) !void { + switch (errno(system.posix_spawn_file_actions_addopen(&self.actions, fd, path, @bitCast(c_int, flags), mode))) { + .SUCCESS => return, + .BADF => return error.InvalidFileDescriptor, + .NOMEM => return error.SystemResources, + .NAMETOOLONG => return error.NameTooLong, + .INVAL => unreachable, // the value of file actions is invalid + else => |err| return unexpectedErrno(err), + } + } + + pub fn close(self: *Actions, fd: fd_t) !void { + switch (errno(system.posix_spawn_file_actions_addclose(&self.actions, fd))) { + .SUCCESS => return, + .BADF => return error.InvalidFileDescriptor, + .NOMEM => return error.SystemResources, + .INVAL => unreachable, // the value of file actions is invalid + .NAMETOOLONG => unreachable, + else => |err| return unexpectedErrno(err), + } + } + + pub fn dup2(self: *Actions, fd: fd_t, newfd: fd_t) !void { + switch (errno(system.posix_spawn_file_actions_adddup2(&self.actions, fd, newfd))) { + .SUCCESS => return, + .BADF => return error.InvalidFileDescriptor, + .NOMEM => return error.SystemResources, + .INVAL => unreachable, // the value of file actions is invalid + .NAMETOOLONG => unreachable, + else => |err| return unexpectedErrno(err), + } + } + + pub fn inherit(self: *Actions, fd: fd_t) !void { + switch (errno(system.posix_spawn_file_actions_addinherit_np(&self.actions, fd))) { + .SUCCESS => return, + .BADF => return error.InvalidFileDescriptor, + .NOMEM => return error.SystemResources, + .INVAL => unreachable, // the value of file actions is invalid + .NAMETOOLONG => unreachable, + else => |err| return unexpectedErrno(err), + } + } + + pub fn chdir(self: *Actions, path: []const u8) !void { + const posix_path = try toPosixPath(path); + return self.chdirZ(&posix_path); + } + + pub fn chdirZ(self: *Actions, path: [*:0]const u8) !void { + switch (errno(system.posix_spawn_file_actions_addchdir_np(&self.actions, path))) { + .SUCCESS => return, + .NOMEM => return error.SystemResources, + .NAMETOOLONG => return error.NameTooLong, + .BADF => unreachable, + .INVAL => unreachable, // the value of file actions is invalid + else => |err| return unexpectedErrno(err), + } + } + + pub fn fchdir(self: *Actions, fd: fd_t) !void { + switch (errno(system.posix_spawn_file_actions_addfchdir_np(&self.actions, fd))) { + .SUCCESS => return, + .BADF => return error.InvalidFileDescriptor, + .NOMEM => return error.SystemResources, + .INVAL => unreachable, // the value of file actions is invalid + .NAMETOOLONG => unreachable, + else => |err| return unexpectedErrno(err), + } + } + }; + + pub fn spawn( + path: []const u8, + actions: ?Actions, + attr: ?Attr, + argv: [*:null]?[*:0]const u8, + envp: [*:null]?[*:0]const u8, + ) !pid_t { + const posix_path = try toPosixPath(path); + return spawnZ(&posix_path, actions, attr, argv, envp); + } + + pub fn spawnZ( + path: [*:0]const u8, + actions: ?Actions, + attr: ?Attr, + argv: [*:null]?[*:0]const u8, + envp: [*:null]?[*:0]const u8, + ) Maybe(pid_t) { + var pid: pid_t = undefined; + const rc = system.posix_spawn( + &pid, + path, + if (actions) |a| &a.actions else null, + if (attr) |a| &a.attr else null, + argv, + envp, + ); + + if (Maybe(pid_t).errno(rc)) |err| { + return err; + } + + return Maybe(pid_t){ .result = pid }; + } + + pub fn spawnp( + file: []const u8, + actions: ?Actions, + attr: ?Attr, + argv: [*:null]?[*:0]const u8, + envp: [*:null]?[*:0]const u8, + ) !pid_t { + const posix_file = try toPosixPath(file); + return spawnpZ(&posix_file, actions, attr, argv, envp); + } + + pub fn spawnpZ( + file: [*:0]const u8, + actions: ?Actions, + attr: ?Attr, + argv: [*:null]?[*:0]const u8, + envp: [*:null]?[*:0]const u8, + ) !pid_t { + var pid: pid_t = undefined; + switch (errno(system.posix_spawnp( + &pid, + file, + if (actions) |a| &a.actions else null, + if (attr) |a| &a.attr else null, + argv, + envp, + ))) { + .SUCCESS => return pid, + .@"2BIG" => return error.TooBig, + .NOMEM => return error.SystemResources, + .BADF => return error.InvalidFileDescriptor, + .ACCES => return error.PermissionDenied, + .IO => return error.InputOutput, + .LOOP => return error.FileSystem, + .NAMETOOLONG => return error.NameTooLong, + .NOENT => return error.FileNotFound, + .NOEXEC => return error.InvalidExe, + .NOTDIR => return error.NotDir, + .TXTBSY => return error.FileBusy, + .BADARCH => return error.InvalidExe, + .BADEXEC => return error.InvalidExe, + .FAULT => unreachable, + .INVAL => unreachable, + else => |err| return unexpectedErrno(err), + } + } + + /// Use this version of the `waitpid` wrapper if you spawned your child process using `posix_spawn` + /// or `posix_spawnp` syscalls. + /// See also `std.os.waitpid` for an alternative if your child process was spawned via `fork` and + /// `execve` method. + pub fn waitpid(pid: pid_t, flags: u32) !WaitPidResult { + const Status = c_int; + var status: Status = undefined; + while (true) { + const rc = system.waitpid(pid, &status, @intCast(c_int, flags)); + switch (errno(rc)) { + .SUCCESS => return WaitPidResult{ + .pid = @intCast(pid_t, rc), + .status = @bitCast(u32, status), + }, + .INTR => continue, + .CHILD => return error.ChildExecFailed, + .INVAL => unreachable, // Invalid flags. + else => unreachable, + } + } + } +}; |