diff options
author | 2022-09-24 19:03:31 -0700 | |
---|---|---|
committer | 2022-09-25 13:14:23 -0700 | |
commit | 9833841101c75c3d511a64daf32e8c273d7d928f (patch) | |
tree | 543d3de0426447161a991b0475aa749c1305f08d | |
parent | 1cd67b62e9d012c02d1534accf295014469be7e6 (diff) | |
download | bun-9833841101c75c3d511a64daf32e8c273d7d928f.tar.gz bun-9833841101c75c3d511a64daf32e8c273d7d928f.tar.zst bun-9833841101c75c3d511a64daf32e8c273d7d928f.zip |
wip
24 files changed, 1939 insertions, 20 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, + } + } + } +}; diff --git a/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h b/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h index b4d389d46..cd67081d1 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+DOMClientIsoSubspaces.h @@ -1,4 +1,5 @@ -std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForSHA1; +std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForSubprocess; +std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForSubprocessConstructor;std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForSHA1; std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForSHA1Constructor;std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForMD5; std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForMD5Constructor;std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForMD4; std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForMD4Constructor;std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForSHA224; diff --git a/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h b/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h index b23b98853..4d1104f99 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+DOMIsoSubspaces.h @@ -1,4 +1,5 @@ -std::unique_ptr<IsoSubspace> m_subspaceForSHA1; +std::unique_ptr<IsoSubspace> m_subspaceForSubprocess; +std::unique_ptr<IsoSubspace> m_subspaceForSubprocessConstructor;std::unique_ptr<IsoSubspace> m_subspaceForSHA1; std::unique_ptr<IsoSubspace> m_subspaceForSHA1Constructor;std::unique_ptr<IsoSubspace> m_subspaceForMD5; std::unique_ptr<IsoSubspace> m_subspaceForMD5Constructor;std::unique_ptr<IsoSubspace> m_subspaceForMD4; std::unique_ptr<IsoSubspace> m_subspaceForMD4Constructor;std::unique_ptr<IsoSubspace> m_subspaceForSHA224; diff --git a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h index 31100e3a4..c37518fe5 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureHeader.h @@ -1,3 +1,9 @@ +JSC::Structure* JSSubprocessStructure() { return m_JSSubprocess.getInitializedOnMainThread(this); } + JSC::JSObject* JSSubprocessConstructor() { return m_JSSubprocess.constructorInitializedOnMainThread(this); } + JSC::JSValue JSSubprocessPrototype() { return m_JSSubprocess.prototypeInitializedOnMainThread(this); } + JSC::LazyClassStructure m_JSSubprocess; + bool hasJSSubprocessSetterValue { false }; + mutable JSC::WriteBarrier<JSC::Unknown> m_JSSubprocessSetterValue; JSC::Structure* JSSHA1Structure() { return m_JSSHA1.getInitializedOnMainThread(this); } JSC::JSObject* JSSHA1Constructor() { return m_JSSHA1.constructorInitializedOnMainThread(this); } JSC::JSValue JSSHA1Prototype() { return m_JSSHA1.prototypeInitializedOnMainThread(this); } diff --git a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h index d5d28ec81..1b6ac7ade 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h +++ b/src/bun.js/bindings/ZigGeneratedClasses+lazyStructureImpl.h @@ -1,4 +1,10 @@ void GlobalObject::initGeneratedLazyClasses() { + m_JSSubprocess.initLater( + [](LazyClassStructure::Initializer& init) { + init.setPrototype(WebCore::JSSubprocess::createPrototype(init.vm, reinterpret_cast<Zig::GlobalObject*>(init.global))); + init.setStructure(WebCore::JSSubprocess::createStructure(init.vm, init.global, init.prototype)); + init.setConstructor(WebCore::JSSubprocessConstructor::create(init.vm, init.global, WebCore::JSSubprocessConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()), jsCast<WebCore::JSSubprocessPrototype*>(init.prototype))); + }); m_JSSHA1.initLater( [](LazyClassStructure::Initializer& init) { init.setPrototype(WebCore::JSSHA1::createPrototype(init.vm, reinterpret_cast<Zig::GlobalObject*>(init.global))); @@ -75,6 +81,7 @@ void GlobalObject::initGeneratedLazyClasses() { template<typename Visitor> void GlobalObject::visitGeneratedLazyClasses(GlobalObject *thisObject, Visitor& visitor) { + thisObject->m_JSSubprocess.visit(visitor); visitor.append(thisObject->m_JSSubprocessSetterValue); thisObject->m_JSSHA1.visit(visitor); visitor.append(thisObject->m_JSSHA1SetterValue); thisObject->m_JSMD5.visit(visitor); visitor.append(thisObject->m_JSMD5SetterValue); thisObject->m_JSMD4.visit(visitor); visitor.append(thisObject->m_JSMD4SetterValue); diff --git a/src/bun.js/bindings/ZigGeneratedClasses.cpp b/src/bun.js/bindings/ZigGeneratedClasses.cpp index 90c628c01..78f1bb179 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.cpp +++ b/src/bun.js/bindings/ZigGeneratedClasses.cpp @@ -23,7 +23,384 @@ namespace WebCore { using namespace JSC; using namespace Zig; -extern "C" void* SHA1Class__construct(JSC::JSGlobalObject*, JSC::CallFrame*); +extern "C" void* SubprocessClass__construct(JSC::JSGlobalObject*, JSC::CallFrame*); +JSC_DECLARE_CUSTOM_GETTER(jsSubprocessConstructor); +extern "C" void SubprocessClass__finalize(void*); + +extern "C" JSC::EncodedJSValue SubprocessPrototype__getExitStatus(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); +JSC_DECLARE_CUSTOM_GETTER(SubprocessPrototype__exitStatusGetterWrap); + + +extern "C" EncodedJSValue SubprocessPrototype__kill(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(SubprocessPrototype__killCallback); + + +extern "C" JSC::EncodedJSValue SubprocessPrototype__getKilled(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); +JSC_DECLARE_CUSTOM_GETTER(SubprocessPrototype__killedGetterWrap); + + +extern "C" JSC::EncodedJSValue SubprocessPrototype__getPid(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); +JSC_DECLARE_CUSTOM_GETTER(SubprocessPrototype__pidGetterWrap); + + +extern "C" EncodedJSValue SubprocessPrototype__doRef(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(SubprocessPrototype__refCallback); + + +extern "C" JSC::EncodedJSValue SubprocessPrototype__getStderr(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); +JSC_DECLARE_CUSTOM_GETTER(SubprocessPrototype__stderrGetterWrap); + + +extern "C" JSC::EncodedJSValue SubprocessPrototype__getStdin(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); +JSC_DECLARE_CUSTOM_GETTER(SubprocessPrototype__stdinGetterWrap); + + +extern "C" JSC::EncodedJSValue SubprocessPrototype__getStdout(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject); +JSC_DECLARE_CUSTOM_GETTER(SubprocessPrototype__stdoutGetterWrap); + + +extern "C" EncodedJSValue SubprocessPrototype__doUnref(void* ptr, JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame); +JSC_DECLARE_HOST_FUNCTION(SubprocessPrototype__unrefCallback); + + +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSSubprocessPrototype, JSSubprocessPrototype::Base); + + + static const HashTableValue JSSubprocessPrototypeTableValues[] = { +{ "exitStatus"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, SubprocessPrototype__exitStatusGetterWrap, 0 } } , +{ "kill"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, SubprocessPrototype__killCallback, 1 } } , +{ "killed"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, SubprocessPrototype__killedGetterWrap, 0 } } , +{ "pid"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, SubprocessPrototype__pidGetterWrap, 0 } } , +{ "ref"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, SubprocessPrototype__refCallback, 0 } } , +{ "stderr"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, SubprocessPrototype__stderrGetterWrap, 0 } } , +{ "stdin"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, SubprocessPrototype__stdinGetterWrap, 0 } } , +{ "stdout"_s, static_cast<unsigned>(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, SubprocessPrototype__stdoutGetterWrap, 0 } } , +{ "unref"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, SubprocessPrototype__unrefCallback, 0 } } + }; + + +const ClassInfo JSSubprocessPrototype::s_info = { "Subprocess"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSSubprocessPrototype) }; + + + +JSC_DEFINE_CUSTOM_GETTER(jsSubprocessConstructor, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName)) +{ + VM& vm = JSC::getVM(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto* globalObject = reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject); + auto* prototype = jsDynamicCast<JSSubprocessPrototype*>(JSValue::decode(thisValue)); + + if (UNLIKELY(!prototype)) + return throwVMTypeError(lexicalGlobalObject, throwScope); + return JSValue::encode(globalObject->JSSubprocessConstructor()); +} + + + +JSC_DEFINE_CUSTOM_GETTER(SubprocessPrototype__exitStatusGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + auto& vm = lexicalGlobalObject->vm(); + Zig::GlobalObject *globalObject = reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + JSSubprocess* thisObject = jsCast<JSSubprocess*>(JSValue::decode(thisValue)); + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + + if (JSValue cachedValue = thisObject->m_exitStatus.get()) + return JSValue::encode(cachedValue); + + JSC::JSValue result = JSC::JSValue::decode( + SubprocessPrototype__getExitStatus(thisObject->wrapped(), globalObject) + ); + RETURN_IF_EXCEPTION(throwScope, {}); + thisObject->m_exitStatus.set(vm, thisObject, result); + RELEASE_AND_RETURN(throwScope, JSValue::encode(result)); +} + + +JSC_DEFINE_HOST_FUNCTION(SubprocessPrototype__killCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSSubprocess* thisObject = jsDynamicCast<JSSubprocess*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + + return SubprocessPrototype__kill(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + + +JSC_DEFINE_CUSTOM_GETTER(SubprocessPrototype__killedGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + auto& vm = lexicalGlobalObject->vm(); + Zig::GlobalObject *globalObject = reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + JSSubprocess* thisObject = jsCast<JSSubprocess*>(JSValue::decode(thisValue)); + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + JSC::EncodedJSValue result = SubprocessPrototype__getKilled(thisObject->wrapped(), globalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + RELEASE_AND_RETURN(throwScope, result); +} + + +JSC_DEFINE_CUSTOM_GETTER(SubprocessPrototype__pidGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + auto& vm = lexicalGlobalObject->vm(); + Zig::GlobalObject *globalObject = reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + JSSubprocess* thisObject = jsCast<JSSubprocess*>(JSValue::decode(thisValue)); + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + JSC::EncodedJSValue result = SubprocessPrototype__getPid(thisObject->wrapped(), globalObject); + RETURN_IF_EXCEPTION(throwScope, {}); + RELEASE_AND_RETURN(throwScope, result); +} + + +JSC_DEFINE_HOST_FUNCTION(SubprocessPrototype__refCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSSubprocess* thisObject = jsDynamicCast<JSSubprocess*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + + return SubprocessPrototype__doRef(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + + +JSC_DEFINE_CUSTOM_GETTER(SubprocessPrototype__stderrGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + auto& vm = lexicalGlobalObject->vm(); + Zig::GlobalObject *globalObject = reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + JSSubprocess* thisObject = jsCast<JSSubprocess*>(JSValue::decode(thisValue)); + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + + if (JSValue cachedValue = thisObject->m_stderr.get()) + return JSValue::encode(cachedValue); + + JSC::JSValue result = JSC::JSValue::decode( + SubprocessPrototype__getStderr(thisObject->wrapped(), globalObject) + ); + RETURN_IF_EXCEPTION(throwScope, {}); + thisObject->m_stderr.set(vm, thisObject, result); + RELEASE_AND_RETURN(throwScope, JSValue::encode(result)); +} + + +JSC_DEFINE_CUSTOM_GETTER(SubprocessPrototype__stdinGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + auto& vm = lexicalGlobalObject->vm(); + Zig::GlobalObject *globalObject = reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + JSSubprocess* thisObject = jsCast<JSSubprocess*>(JSValue::decode(thisValue)); + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + + if (JSValue cachedValue = thisObject->m_stdin.get()) + return JSValue::encode(cachedValue); + + JSC::JSValue result = JSC::JSValue::decode( + SubprocessPrototype__getStdin(thisObject->wrapped(), globalObject) + ); + RETURN_IF_EXCEPTION(throwScope, {}); + thisObject->m_stdin.set(vm, thisObject, result); + RELEASE_AND_RETURN(throwScope, JSValue::encode(result)); +} + + +JSC_DEFINE_CUSTOM_GETTER(SubprocessPrototype__stdoutGetterWrap, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName attributeName)) +{ + auto& vm = lexicalGlobalObject->vm(); + Zig::GlobalObject *globalObject = reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + JSSubprocess* thisObject = jsCast<JSSubprocess*>(JSValue::decode(thisValue)); + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + + if (JSValue cachedValue = thisObject->m_stdout.get()) + return JSValue::encode(cachedValue); + + JSC::JSValue result = JSC::JSValue::decode( + SubprocessPrototype__getStdout(thisObject->wrapped(), globalObject) + ); + RETURN_IF_EXCEPTION(throwScope, {}); + thisObject->m_stdout.set(vm, thisObject, result); + RELEASE_AND_RETURN(throwScope, JSValue::encode(result)); +} + + +JSC_DEFINE_HOST_FUNCTION(SubprocessPrototype__unrefCallback, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = lexicalGlobalObject->vm(); + + JSSubprocess* thisObject = jsDynamicCast<JSSubprocess*>(callFrame->thisValue()); + + if (UNLIKELY(!thisObject)) { + auto throwScope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(lexicalGlobalObject, throwScope); + } + + JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); + + return SubprocessPrototype__doUnref(thisObject->wrapped(), lexicalGlobalObject, callFrame); +} + + +void JSSubprocessPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSSubprocess::info(), JSSubprocessPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +void JSSubprocessConstructor::finishCreation(VM& vm, JSC::JSGlobalObject* globalObject, JSSubprocessPrototype* prototype) +{ + Base::finishCreation(vm, 0, "Subprocess"_s, PropertyAdditionMode::WithoutStructureTransition); + + putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); + ASSERT(inherits(info())); +} + +JSSubprocessConstructor* JSSubprocessConstructor::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSSubprocessPrototype* prototype) { + JSSubprocessConstructor* ptr = new (NotNull, JSC::allocateCell<JSSubprocessConstructor>(vm)) JSSubprocessConstructor(vm, structure, construct); + ptr->finishCreation(vm, globalObject, prototype); + return ptr; +} + +JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSSubprocessConstructor::construct(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame) +{ + Zig::GlobalObject *globalObject = reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject); + JSC::VM &vm = globalObject->vm(); + JSObject* newTarget = asObject(callFrame->newTarget()); + auto* constructor = globalObject->JSSubprocessConstructor(); + Structure* structure = globalObject->JSSubprocessStructure(); + if (constructor != newTarget) { + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* functionGlobalObject = reinterpret_cast<Zig::GlobalObject*>( + // ShadowRealm functions belong to a different global object. + getFunctionRealm(globalObject, newTarget) + ); + RETURN_IF_EXCEPTION(scope, {}); + structure = InternalFunction::createSubclassStructure( + globalObject, + newTarget, + functionGlobalObject->JSSubprocessStructure() + ); + } + + void* ptr = SubprocessClass__construct(globalObject, callFrame); + + if (UNLIKELY(!ptr)) { + return JSValue::encode(JSC::jsUndefined()); + } + + JSSubprocess* instance = JSSubprocess::create(vm, globalObject, structure, ptr); + + return JSValue::encode(instance); +} + +extern "C" EncodedJSValue Subprocess__create(Zig::GlobalObject* globalObject, void* ptr) { + auto &vm = globalObject->vm(); + JSC::Structure* structure = globalObject->JSSubprocessStructure(); + JSSubprocess* instance = JSSubprocess::create(vm, globalObject, structure, ptr); + return JSValue::encode(instance); +} + +void JSSubprocessConstructor::initializeProperties(VM& vm, JSC::JSGlobalObject* globalObject, JSSubprocessPrototype* prototype) +{ + +} + +const ClassInfo JSSubprocessConstructor::s_info = { "Function"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSSubprocessConstructor) }; + + + extern "C" EncodedJSValue Subprocess__getConstructor(Zig::GlobalObject* globalObject) { + return JSValue::encode(globalObject->JSSubprocessConstructor()); + } + +JSSubprocess::~JSSubprocess() +{ + if (m_ctx) { + SubprocessClass__finalize(m_ctx); + } +} +void JSSubprocess::destroy(JSCell* cell) +{ + static_cast<JSSubprocess*>(cell)->JSSubprocess::~JSSubprocess(); +} + +const ClassInfo JSSubprocess::s_info = { "Subprocess"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSSubprocess) }; + +void JSSubprocess::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSSubprocess* JSSubprocess::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx) { + JSSubprocess* ptr = new (NotNull, JSC::allocateCell<JSSubprocess>(vm)) JSSubprocess(vm, structure, ctx); + ptr->finishCreation(vm); + return ptr; +} + + +extern "C" void* Subprocess__fromJS(JSC::EncodedJSValue value) { + JSSubprocess* object = JSC::jsDynamicCast<JSSubprocess*>(JSValue::decode(value)); + if (!object) + return nullptr; + + return object->wrapped(); +} + +extern "C" bool Subprocess__dangerouslySetPtr(JSC::EncodedJSValue value, void* ptr) { + JSSubprocess* object = JSC::jsDynamicCast<JSSubprocess*>(JSValue::decode(value)); + if (!object) + return false; + + object->m_ctx = ptr; + return true; +} + + +extern "C" const size_t Subprocess__ptrOffset = JSSubprocess::offsetOfWrapped(); + +void JSSubprocess::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) +{ + auto* thisObject = jsCast<JSSubprocess*>(cell); + if (void* wrapped = thisObject->wrapped()) { + // if (thisObject->scriptExecutionContext()) + // analyzer.setLabelForCell(cell, "url " + thisObject->scriptExecutionContext()->url().string()); + } + Base::analyzeHeap(cell, analyzer); +} + +JSObject* JSSubprocess::createPrototype(VM& vm, JSDOMGlobalObject* globalObject) +{ + return JSSubprocessPrototype::create(vm, globalObject, JSSubprocessPrototype::createStructure(vm, globalObject, globalObject->objectPrototype())); +} + +template<typename Visitor> +void JSSubprocess::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + JSSubprocess* thisObject = jsCast<JSSubprocess*>(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_exitStatus); + visitor.append(thisObject->m_stderr); + visitor.append(thisObject->m_stdin); + visitor.append(thisObject->m_stdout); +} + +DEFINE_VISIT_CHILDREN(JSSubprocess);extern "C" void* SHA1Class__construct(JSC::JSGlobalObject*, JSC::CallFrame*); JSC_DECLARE_CUSTOM_GETTER(jsSHA1Constructor); extern "C" void SHA1Class__finalize(void*); diff --git a/src/bun.js/bindings/ZigGeneratedClasses.h b/src/bun.js/bindings/ZigGeneratedClasses.h index e315093c7..feae81ae0 100644 --- a/src/bun.js/bindings/ZigGeneratedClasses.h +++ b/src/bun.js/bindings/ZigGeneratedClasses.h @@ -15,6 +15,133 @@ namespace WebCore { using namespace Zig; using namespace JSC; +class JSSubprocess final : public JSC::JSDestructibleObject { + public: + using Base = JSC::JSDestructibleObject; + static JSSubprocess* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx); + + DECLARE_EXPORT_INFO; + template<typename, JSC::SubspaceAccess mode> static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl<JSSubprocess, WebCore::UseCustomHeapCellType::No>( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForSubprocess.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForSubprocess = WTFMove(space); }, + [](auto& spaces) { return spaces.m_subspaceForSubprocess.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForSubprocess = WTFMove(space); }); + } + + static void destroy(JSC::JSCell*); + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(static_cast<JSC::JSType>(0b11101110), StructureFlags), info()); + } + + static JSObject* createPrototype(VM& vm, JSDOMGlobalObject* globalObject); + + ~JSSubprocess(); + + void* wrapped() const { return m_ctx; } + + void detach() + { + m_ctx = nullptr; + } + + static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + static ptrdiff_t offsetOfWrapped() { return OBJECT_OFFSETOF(JSSubprocess, m_ctx); } + + void* m_ctx { nullptr }; + + + JSSubprocess(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr) + : Base(vm, structure) + { + m_ctx = sinkPtr; + } + + void finishCreation(JSC::VM&); + + DECLARE_VISIT_CHILDREN; + + mutable JSC::WriteBarrier<JSC::Unknown> m_exitStatus; +mutable JSC::WriteBarrier<JSC::Unknown> m_stderr; +mutable JSC::WriteBarrier<JSC::Unknown> m_stdin; +mutable JSC::WriteBarrier<JSC::Unknown> m_stdout; + }; +class JSSubprocessPrototype final : public JSC::JSNonFinalObject { + public: + using Base = JSC::JSNonFinalObject; + + static JSSubprocessPrototype* create(JSC::VM& vm, JSGlobalObject* globalObject, JSC::Structure* structure) + { + JSSubprocessPrototype* ptr = new (NotNull, JSC::allocateCell<JSSubprocessPrototype>(vm)) JSSubprocessPrototype(vm, globalObject, structure); + ptr->finishCreation(vm, globalObject); + return ptr; + } + + DECLARE_INFO; + template<typename CellType, JSC::SubspaceAccess> + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + + private: + JSSubprocessPrototype(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(JSC::VM&, JSC::JSGlobalObject*); + }; + + class JSSubprocessConstructor final : public JSC::InternalFunction { + public: + using Base = JSC::InternalFunction; + static JSSubprocessConstructor* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSSubprocessPrototype* prototype); + + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr bool needsDestruction = false; + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info()); + } + + template<typename, JSC::SubspaceAccess mode> static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl<JSSubprocessConstructor, WebCore::UseCustomHeapCellType::No>( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForSubprocessConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForSubprocessConstructor = WTFMove(space); }, + [](auto& spaces) { return spaces.m_subspaceForSubprocessConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForSubprocessConstructor = WTFMove(space); }); + } + + + void initializeProperties(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSSubprocessPrototype* prototype); + + // Must be defined for each specialization class. + static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES construct(JSC::JSGlobalObject*, JSC::CallFrame*); + DECLARE_EXPORT_INFO; + private: + JSSubprocessConstructor(JSC::VM& vm, JSC::Structure* structure, JSC::NativeFunction nativeFunction) + : Base(vm, structure, nativeFunction, nativeFunction) + + { + } + + void finishCreation(JSC::VM&, JSC::JSGlobalObject* globalObject, JSSubprocessPrototype* prototype); + }; class JSSHA1 final : public JSC::JSDestructibleObject { public: using Base = JSC::JSDestructibleObject; diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index f737c783e..3b41c9fa4 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -1896,6 +1896,27 @@ pub const JSGlobalObject = extern struct { } } + pub fn throwError( + this: *JSGlobalObject, + err: anyerror, + comptime fmt: string, + ) void { + var str = ZigString.init(std.fmt.allocPrint(this.bunVM().allocator, "{s} " ++ fmt, .{@errorName(err)}) catch return); + str.markUTF8(); + var err_value = str.toErrorInstance(this); + this.vm().throwError(this, err_value); + this.bunVM().allocator.free(ZigString.untagged(str.ptr)[0..str.len]); + } + + pub fn handleError( + this: *JSGlobalObject, + err: anyerror, + comptime fmt: string, + ) JSValue { + this.throwError(err, fmt); + return JSValue.jsUndefined(); + } + // pub fn createError(globalObject: *JSGlobalObject, error_type: ErrorType, message: *String) *JSObject { // return cppFn("createError", .{ globalObject, error_type, message }); // } diff --git a/src/bun.js/bindings/generated_classes.zig b/src/bun.js/bindings/generated_classes.zig index 494ad1270..425d70a44 100644 --- a/src/bun.js/bindings/generated_classes.zig +++ b/src/bun.js/bindings/generated_classes.zig @@ -7,6 +7,99 @@ pub const StaticGetterType = fn (*JSC.JSGlobalObject, JSC.JSValue, JSC.JSValue) pub const StaticSetterType = fn (*JSC.JSGlobalObject, JSC.JSValue, JSC.JSValue, JSC.JSValue) callconv(.C) bool; pub const StaticCallbackType = fn (*JSC.JSGlobalObject, *JSC.CallFrame) callconv(.C) JSC.JSValue; +pub const JSSubprocess = struct { + const Subprocess = Classes.Subprocess; + const GetterType = fn (*Subprocess, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; + const SetterType = fn (*Subprocess, *JSC.JSGlobalObject, JSC.JSValue) callconv(.C) bool; + const CallbackType = fn (*Subprocess, *JSC.JSGlobalObject, *JSC.CallFrame) callconv(.C) JSC.JSValue; + + /// Return the pointer to the wrapped object. + /// If the object does not match the type, return null. + pub fn fromJS(value: JSC.JSValue) ?*Subprocess { + JSC.markBinding(); + return Subprocess__fromJS(value); + } + + /// Get the Subprocess constructor value. + /// This loads lazily from the global object. + pub fn getConstructor(globalObject: *JSC.JSGlobalObject) JSC.JSValue { + JSC.markBinding(); + return Subprocess__getConstructor(globalObject); + } + + /// Create a new instance of Subprocess + pub fn toJS(this: *Subprocess, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + JSC.markBinding(); + if (comptime Environment.allow_assert) { + const value__ = Subprocess__create(globalObject, this); + std.debug.assert(value__.as(Subprocess).? == this); // If this fails, likely a C ABI issue. + return value__; + } else { + return Subprocess__create(globalObject, this); + } + } + + /// Modify the internal ptr to point to a new instance of Subprocess. + pub fn dangerouslySetPtr(value: JSC.JSValue, ptr: ?*Subprocess) bool { + JSC.markBinding(); + return Subprocess__dangerouslySetPtr(value, ptr); + } + + extern fn Subprocess__fromJS(JSC.JSValue) ?*Subprocess; + extern fn Subprocess__getConstructor(*JSC.JSGlobalObject) JSC.JSValue; + + extern fn Subprocess__create(globalObject: *JSC.JSGlobalObject, ptr: ?*Subprocess) JSC.JSValue; + + extern fn Subprocess__dangerouslySetPtr(JSC.JSValue, ?*Subprocess) bool; + + comptime { + if (@TypeOf(Subprocess.constructor) != (fn (*JSC.JSGlobalObject, *JSC.CallFrame) callconv(.C) ?*Subprocess)) { + @compileLog("Subprocess.constructor is not a constructor"); + } + + if (@TypeOf(Subprocess.finalize) != (fn (*Subprocess) callconv(.C) void)) { + @compileLog("Subprocess.finalize is not a finalizer"); + } + + if (@TypeOf(Subprocess.getExitStatus) != GetterType) + @compileLog("Expected Subprocess.getExitStatus to be a getter"); + + if (@TypeOf(Subprocess.kill) != CallbackType) + @compileLog("Expected Subprocess.kill to be a callback"); + if (@TypeOf(Subprocess.getKilled) != GetterType) + @compileLog("Expected Subprocess.getKilled to be a getter"); + + if (@TypeOf(Subprocess.getPid) != GetterType) + @compileLog("Expected Subprocess.getPid to be a getter"); + + if (@TypeOf(Subprocess.doRef) != CallbackType) + @compileLog("Expected Subprocess.doRef to be a callback"); + if (@TypeOf(Subprocess.getStderr) != GetterType) + @compileLog("Expected Subprocess.getStderr to be a getter"); + + if (@TypeOf(Subprocess.getStdin) != GetterType) + @compileLog("Expected Subprocess.getStdin to be a getter"); + + if (@TypeOf(Subprocess.getStdout) != GetterType) + @compileLog("Expected Subprocess.getStdout to be a getter"); + + if (@TypeOf(Subprocess.doUnref) != CallbackType) + @compileLog("Expected Subprocess.doUnref to be a callback"); + if (!JSC.is_bindgen) { + @export(Subprocess.constructor, .{ .name = "SubprocessClass__construct" }); + @export(Subprocess.doRef, .{ .name = "SubprocessPrototype__doRef" }); + @export(Subprocess.doUnref, .{ .name = "SubprocessPrototype__doUnref" }); + @export(Subprocess.finalize, .{ .name = "SubprocessClass__finalize" }); + @export(Subprocess.getExitStatus, .{ .name = "SubprocessPrototype__getExitStatus" }); + @export(Subprocess.getKilled, .{ .name = "SubprocessPrototype__getKilled" }); + @export(Subprocess.getPid, .{ .name = "SubprocessPrototype__getPid" }); + @export(Subprocess.getStderr, .{ .name = "SubprocessPrototype__getStderr" }); + @export(Subprocess.getStdin, .{ .name = "SubprocessPrototype__getStdin" }); + @export(Subprocess.getStdout, .{ .name = "SubprocessPrototype__getStdout" }); + @export(Subprocess.kill, .{ .name = "SubprocessPrototype__kill" }); + } + } +}; pub const JSSHA1 = struct { const SHA1 = Classes.SHA1; const GetterType = fn (*SHA1, *JSC.JSGlobalObject) callconv(.C) JSC.JSValue; @@ -1020,6 +1113,7 @@ pub const JSBlob = struct { }; comptime { + _ = JSSubprocess; _ = JSSHA1; _ = JSMD5; _ = JSMD4; diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index f90cc95d4..dbfa6c792 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -13,4 +13,5 @@ pub const Classes = struct { pub const SHA512_256 = JSC.API.Bun.Crypto.SHA512_256; pub const TextDecoder = JSC.WebCore.TextDecoder; pub const Blob = JSC.WebCore.Blob; + pub const Subprocess = JSC.Subprocess; }; diff --git a/src/bun.js/builtins/cpp/ReadableStreamBuiltins.cpp b/src/bun.js/builtins/cpp/ReadableStreamBuiltins.cpp index 2b0797381..830fbfd51 100644 --- a/src/bun.js/builtins/cpp/ReadableStreamBuiltins.cpp +++ b/src/bun.js/builtins/cpp/ReadableStreamBuiltins.cpp @@ -214,7 +214,7 @@ const char* const s_readableStreamReadableStreamToBlobCode = const JSC::ConstructAbility s_readableStreamConsumeReadableStreamCodeConstructAbility = JSC::ConstructAbility::CannotConstruct; const JSC::ConstructorKind s_readableStreamConsumeReadableStreamCodeConstructorKind = JSC::ConstructorKind::None; const JSC::ImplementationVisibility s_readableStreamConsumeReadableStreamCodeImplementationVisibility = JSC::ImplementationVisibility::Private; -const int s_readableStreamConsumeReadableStreamCodeLength = 3718; +const int s_readableStreamConsumeReadableStreamCodeLength = 3736; static const JSC::Intrinsic s_readableStreamConsumeReadableStreamCodeIntrinsic = JSC::NoIntrinsic; const char* const s_readableStreamConsumeReadableStreamCode = "(function (nativePtr, nativeType, inputStream) {\n" \ @@ -309,6 +309,8 @@ const char* const s_readableStreamConsumeReadableStreamCode = " } else {\n" \ " return -1;\n" \ " }\n" \ + "\n" \ + " \n" \ " }\n" \ "\n" \ " readMany() {\n" \ diff --git a/src/bun.js/builtins/cpp/ReadableStreamInternalsBuiltins.cpp b/src/bun.js/builtins/cpp/ReadableStreamInternalsBuiltins.cpp index e39802ee5..a862fc0b4 100644 --- a/src/bun.js/builtins/cpp/ReadableStreamInternalsBuiltins.cpp +++ b/src/bun.js/builtins/cpp/ReadableStreamInternalsBuiltins.cpp @@ -2253,7 +2253,7 @@ const char* const s_readableStreamInternalsReadableStreamDefaultControllerCanClo const JSC::ConstructAbility s_readableStreamInternalsLazyLoadStreamCodeConstructAbility = JSC::ConstructAbility::CannotConstruct; const JSC::ConstructorKind s_readableStreamInternalsLazyLoadStreamCodeConstructorKind = JSC::ConstructorKind::None; const JSC::ImplementationVisibility s_readableStreamInternalsLazyLoadStreamCodeImplementationVisibility = JSC::ImplementationVisibility::Public; -const int s_readableStreamInternalsLazyLoadStreamCodeLength = 2505; +const int s_readableStreamInternalsLazyLoadStreamCodeLength = 2701; static const JSC::Intrinsic s_readableStreamInternalsLazyLoadStreamCodeIntrinsic = JSC::NoIntrinsic; const char* const s_readableStreamInternalsLazyLoadStreamCode = "(function (stream, autoAllocateChunkSize) {\n" \ @@ -2277,6 +2277,8 @@ const char* const s_readableStreamInternalsLazyLoadStreamCode = " handleResult = function handleResult(result, controller, view) {\n" \ " \"use strict\";\n" \ "\n" \ + " console.log(\"handleResult\", result, controller, view);\n" \ + " \n" \ " if (result && @isPromise(result)) {\n" \ " return result.then(\n" \ " handleNativeReadableStreamPromiseResult.bind({\n" \ @@ -2287,8 +2289,10 @@ const char* const s_readableStreamInternalsLazyLoadStreamCode = " );\n" \ " } else if (result !== false) {\n" \ " if (view && view.byteLength === result) {\n" \ + " console.log(\"view\", result, controller.byobRequest);\n" \ " controller.byobRequest.respondWithNewView(view);\n" \ " } else {\n" \ + " console.log(\"result\", result, controller.byobRequest);\n" \ " controller.byobRequest.respond(result);\n" \ " }\n" \ " }\n" \ diff --git a/src/bun.js/builtins/js/ReadableStream.js b/src/bun.js/builtins/js/ReadableStream.js index 8496b2d4b..42c0fbdbc 100644 --- a/src/bun.js/builtins/js/ReadableStream.js +++ b/src/bun.js/builtins/js/ReadableStream.js @@ -244,6 +244,8 @@ function consumeReadableStream(nativePtr, nativeType, inputStream) { } else { return -1; } + + } readMany() { diff --git a/src/bun.js/builtins/js/ReadableStreamInternals.js b/src/bun.js/builtins/js/ReadableStreamInternals.js index 7be9410db..a1e496290 100644 --- a/src/bun.js/builtins/js/ReadableStreamInternals.js +++ b/src/bun.js/builtins/js/ReadableStreamInternals.js @@ -1855,6 +1855,8 @@ function lazyLoadStream(stream, autoAllocateChunkSize) { handleResult = function handleResult(result, controller, view) { "use strict"; + console.log("handleResult", result, controller, view); + if (result && @isPromise(result)) { return result.then( handleNativeReadableStreamPromiseResult.bind({ @@ -1865,8 +1867,10 @@ function lazyLoadStream(stream, autoAllocateChunkSize) { ); } else if (result !== false) { if (view && view.byteLength === result) { + console.log("view", result, controller.byobRequest); controller.byobRequest.respondWithNewView(view); } else { + console.log("result", result, controller.byobRequest); controller.byobRequest.respond(result); } } diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index ff3dfa9e7..1d011c509 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -212,6 +212,8 @@ pub const EventLoop = struct { waker: ?AsyncIO.Waker = null, start_server_on_next_tick: bool = false, defer_count: std.atomic.Atomic(usize) = std.atomic.Atomic(usize).init(0), + pending_processes_to_exit: std.AutoArrayHashMap(*JSC.Subprocess, void) = undefined, + pub const Queue = std.fifo.LinearFifo(Task, .Dynamic); pub fn tickWithCount(this: *EventLoop) u32 { @@ -421,6 +423,14 @@ pub const EventLoop = struct { pub fn afterUSocketsTick(this: *EventLoop) void { this.defer_count.store(0, .Monotonic); + const processes = this.pending_processes_to_exit.keys(); + if (processes.len > 0) { + for (processes) |process| { + process.onExitNotification(); + } + this.pending_processes_to_exit.clearRetainingCapacity(); + } + this.tick(); } @@ -443,7 +453,7 @@ pub const Poller = struct { /// 0 == unset loop: ?*uws.Loop = null, - pub fn dispatchKQueueEvent(loop: *uws.Loop, kqueue_event: *const std.os.Kevent) void { + pub fn dispatchKQueueEvent(loop: *uws.Loop, kqueue_event: *const std.os.system.kevent64_s) void { if (comptime !Environment.isMac) { unreachable; } @@ -455,7 +465,21 @@ pub const Poller = struct { loop.active -= 1; loader.onPoll(@bitCast(i64, kqueue_event.data), kqueue_event.flags); }, - else => unreachable, + @field(Pollable.Tag, "Subprocess") => { + var loader = ptr.as(JSC.Subprocess); + + loop.num_polls -= 1; + + // kqueue sends the same notification multiple times in the same tick potentially + // so we have to dedupe it + _ = loader.globalThis.bunVM().eventLoop().pending_processes_to_exit.getOrPut(loader) catch unreachable; + }, + else => |tag| { + bun.Output.panic( + "Internal error\nUnknown pollable tag: {d}\n", + .{@enumToInt(tag)}, + ); + }, } } @@ -476,13 +500,15 @@ pub const Poller = struct { const FileBlobLoader = JSC.WebCore.FileBlobLoader; const FileSink = JSC.WebCore.FileSink; + const Subprocess = JSC.Subprocess; + /// epoll only allows one pointer /// We unfortunately need two pointers: one for a function call and one for the context /// We use a tagged pointer union and then call the function with the context pointer pub const Pollable = TaggedPointerUnion(.{ FileBlobLoader, FileSink, - AsyncIO.Waker, + Subprocess, }); const Kevent = std.os.Kevent; const kevent = std.c.kevent; @@ -538,6 +564,15 @@ pub const Poller = struct { .flags = std.c.EV_ADD | std.c.EV_ENABLE | std.c.EV_ONESHOT, .ext = .{ 0, 0 }, }, + .process => .{ + .ident = @intCast(u64, fd), + .filter = std.os.system.EVFILT_PROC, + .data = 0, + .fflags = std.c.NOTE_EXIT, + .udata = @ptrToInt(Pollable.init(ctx).ptr()), + .flags = std.c.EV_ADD | std.c.EV_ENABLE | std.c.EV_ONESHOT, + .ext = .{ 0, 0 }, + }, }; // output events only include change errors @@ -567,6 +602,7 @@ pub const Poller = struct { } const errno = std.c.getErrno(rc); + if (errno == .SUCCESS) { this.loop.?.num_polls += 1; this.loop.?.active += 1; @@ -597,7 +633,11 @@ pub const Poller = struct { dispatchEpollEvent(loop, &loop.ready_polls[@intCast(usize, loop.current_ready_poll)]); } - pub const Flag = enum { read, write }; + pub const Flag = enum { + read, + write, + process, + }; comptime { @export(onTick, .{ .name = "Bun__internal_dispatch_ready_poll" }); diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 08aa7b418..b9c381a14 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -567,6 +567,7 @@ pub const VirtualMachine = struct { VirtualMachine.vm.regular_event_loop.tasks = EventLoop.Queue.init( default_allocator, ); + VirtualMachine.vm.regular_event_loop.pending_processes_to_exit = std.AutoArrayHashMap(*JSC.Subprocess, void).init(allocator); VirtualMachine.vm.regular_event_loop.tasks.ensureUnusedCapacity(64) catch unreachable; VirtualMachine.vm.regular_event_loop.concurrent_tasks = .{}; VirtualMachine.vm.event_loop = &VirtualMachine.vm.regular_event_loop; diff --git a/src/bun.js/node/syscall.zig b/src/bun.js/node/syscall.zig index edb6937f5..2af73ef21 100644 --- a/src/bun.js/node/syscall.zig +++ b/src/bun.js/node/syscall.zig @@ -99,6 +99,7 @@ pub const Tag = enum(u8) { kevent, kqueue, epoll_ctl, + kill, pub var strings = std.EnumMap(Tag, JSC.C.JSStringRef).initFull(null); }; const PathString = @import("../../global.zig").PathString; diff --git a/src/bun.js/webcore/streams.zig b/src/bun.js/webcore/streams.zig index dd15bf45a..abf220ad7 100644 --- a/src/bun.js/webcore/streams.zig +++ b/src/bun.js/webcore/streams.zig @@ -53,6 +53,10 @@ pub const ReadableStream = struct { value: JSValue, ptr: Source, + pub fn toJS(this: *const ReadableStream) JSValue { + return this.value; + } + pub fn done(this: *const ReadableStream) void { this.value.unprotect(); } @@ -254,7 +258,7 @@ pub const StreamStart = union(Tag) { input_path: PathOrFileDescriptor, truncate: bool = true, close: bool = false, - mode: JSC.Node.Mode = 0, + mode: JSC.Node.Mode = 0o664, }, HTTPSResponseSink: void, HTTPResponseSink: void, @@ -462,6 +466,7 @@ pub const StreamResult = union(Tag) { pub fn toPromised(globalThis: *JSGlobalObject, promise: *JSPromise, pending: *Writable.Pending) void { var frame = bun.default_allocator.create(@Frame(Writable.toPromisedWrap)) catch unreachable; + pending.state = .pending; frame.* = async Writable.toPromisedWrap(globalThis, promise, pending); pending.frame = frame; } @@ -564,6 +569,7 @@ pub const StreamResult = union(Tag) { pub fn toPromised(globalThis: *JSGlobalObject, promise: *JSPromise, pending: *Pending) void { var frame = bun.default_allocator.create(@Frame(toPromisedWrap)) catch unreachable; + pending.state = .pending; frame.* = async toPromisedWrap(globalThis, promise, pending); pending.frame = frame; } @@ -933,6 +939,9 @@ pub const FileSink = struct { head: usize = 0, requested_end: bool = false, + prevent_process_exit: bool = false, + reachable_from_js: bool = true, + pub fn prepare(this: *FileSink, input_path: PathOrFileDescriptor, mode: JSC.Node.Mode) JSC.Node.Maybe(void) { var file_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; const auto_close = this.auto_close; @@ -1083,7 +1092,7 @@ pub const FileSink = struct { this.opened_fd = std.math.maxInt(JSC.Node.FileDescriptor); } - if (this.buffer.len > 0) { + if (this.buffer.cap > 0) { this.buffer.listManaged(this.allocator).deinit(); this.buffer = bun.ByteList.init(""); this.done = true; @@ -1093,8 +1102,10 @@ pub const FileSink = struct { pub fn finalize(this: *FileSink) void { this.cleanup(); + this.reachable_from_js = false; - this.allocator.destroy(this); + if (!this.prevent_process_exit) + this.allocator.destroy(this); } pub fn init(allocator: std.mem.Allocator, next: ?Sink) !*FileSink { @@ -2878,8 +2889,8 @@ pub const FileBlobLoader = struct { return; } - this.pending.result = this.handleReadChunk(@as(usize, this.concurrent.read)); - resume this.pending.frame; + this.pending.result = this.handleReadChunk(@as(usize, this.concurrent.read), protected_view); + this.pending.run(); this.scheduled_count -= 1; if (this.pending.result.isDone()) { this.finalize(); @@ -3058,7 +3069,7 @@ pub const FileBlobLoader = struct { } } - fn handleReadChunk(this: *FileBlobLoader, result: usize) StreamResult { + fn handleReadChunk(this: *FileBlobLoader, result: usize, view: JSC.JSValue) StreamResult { std.debug.assert(this.started); this.total_read += @intCast(Blob.SizeType, result); @@ -3079,10 +3090,10 @@ pub const FileBlobLoader = struct { const has_more = remaining > 0; if (!has_more) { - return .{ .into_array_and_done = .{ .len = @truncate(Blob.SizeType, result) } }; + return .{ .into_array_and_done = .{ .len = @truncate(Blob.SizeType, result), .value = view } }; } - return .{ .into_array = .{ .len = @truncate(Blob.SizeType, result) } }; + return .{ .into_array = .{ .len = @truncate(Blob.SizeType, result), .value = view } }; } pub fn read( @@ -3125,7 +3136,7 @@ pub const FileBlobLoader = struct { return .{ .err = sys }; }, .result => |result| { - return this.handleReadChunk(result); + return this.handleReadChunk(result, view); }, } } @@ -3136,6 +3147,7 @@ pub const FileBlobLoader = struct { this.scheduled_count -= 1; const protected_view = this.protected_view; defer protected_view.unprotect(); + this.protected_view = JSValue.zero; var available_to_read: usize = std.math.maxInt(usize); @@ -3169,7 +3181,7 @@ pub const FileBlobLoader = struct { this.buf.len = @minimum(this.buf.len, available_to_read); } - this.pending.result = this.read(this.buf, this.protected_view); + this.pending.result = this.read(this.buf, protected_view); this.pending.run(); } diff --git a/src/deps/uws.zig b/src/deps/uws.zig index b26020714..497cd660c 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -287,7 +287,7 @@ pub const Loop = extern struct { /// The list of ready polls ready_polls: [1024]EventType, - const EventType = if (Environment.isLinux) std.os.linux.epoll_event else if (Environment.isMac) std.os.Kevent; + const EventType = if (Environment.isLinux) std.os.linux.epoll_event else if (Environment.isMac) std.os.system.kevent64_s; pub const InternalLoopData = extern struct { pub const us_internal_async = opaque {}; diff --git a/src/env_loader.zig b/src/env_loader.zig index 97edbc3c2..4c5f75c09 100644 --- a/src/env_loader.zig +++ b/src/env_loader.zig @@ -1004,6 +1004,26 @@ pub const Map = struct { map: HashTable, + pub fn createNullDelimitedEnvMap(this: *Map, arena: std.mem.Allocator) ![:null]?[*:0]u8 { + var env_map = &this.map; + + const envp_count = env_map.count(); + const envp_buf = try arena.allocSentinel(?[*:0]u8, envp_count, null); + { + var it = env_map.iterator(); + var i: usize = 0; + while (it.next()) |pair| : (i += 1) { + const env_buf = try arena.allocSentinel(u8, pair.key_ptr.len + pair.value_ptr.len + 1, 0); + std.mem.copy(u8, env_buf, pair.key_ptr.*); + env_buf[pair.key_ptr.len] = '='; + std.mem.copy(u8, env_buf[pair.key_ptr.len + 1 ..], pair.value_ptr.*); + envp_buf[i] = env_buf.ptr; + } + std.debug.assert(i == envp_count); + } + return envp_buf; + } + pub fn cloneToEnvMap(this: *Map, allocator: std.mem.Allocator) !std.process.EnvMap { var env_map = std.process.EnvMap.init(allocator); diff --git a/src/jsc.zig b/src/jsc.zig index 4f1265287..f4adbcc3a 100644 --- a/src/jsc.zig +++ b/src/jsc.zig @@ -48,5 +48,6 @@ pub const jsBoolean = @This().JSValue.jsBoolean; pub inline fn markBinding() void { if (comptime is_bindgen) unreachable; } +pub const Subprocess = @import("./bun.js/api/bun.zig").Subprocess; pub const Codegen = @import("./bun.js/bindings/generated_classes.zig"); diff --git a/test/bun.js/filesink.test.ts b/test/bun.js/filesink.test.ts new file mode 100644 index 000000000..28d51a173 --- /dev/null +++ b/test/bun.js/filesink.test.ts @@ -0,0 +1,129 @@ +import { ArrayBufferSink } from "bun"; +import { describe, expect, it } from "bun:test"; + +describe("FileSink", () => { + const fixtures = [ + [ + ["abcdefghijklmnopqrstuvwxyz"], + new TextEncoder().encode("abcdefghijklmnopqrstuvwxyz"), + "abcdefghijklmnopqrstuvwxyz", + ], + [ + ["abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"], + new TextEncoder().encode( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + ), + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + ], + [ + ["😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌"], + new TextEncoder().encode( + "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌" + ), + "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌", + ], + [ + [ + "abcdefghijklmnopqrstuvwxyz", + "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌", + ], + new TextEncoder().encode( + "abcdefghijklmnopqrstuvwxyz" + + "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌" + ), + "abcdefghijklmnopqrstuvwxyz" + + "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌", + ], + [ + [ + "abcdefghijklmnopqrstuvwxyz", + "😋", + " Get Emoji — All Emojis", + " to ✂️ Copy and 📋 Paste 👌", + ], + new TextEncoder().encode( + "abcdefghijklmnopqrstuvwxyz" + + "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌" + ), + "(rope) " + + "abcdefghijklmnopqrstuvwxyz" + + "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌", + ], + [ + [ + new TextEncoder().encode("abcdefghijklmnopqrstuvwxyz"), + "😋", + " Get Emoji — All Emojis", + " to ✂️ Copy and 📋 Paste 👌", + ], + new TextEncoder().encode( + "abcdefghijklmnopqrstuvwxyz" + + "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌" + ), + "(array) " + + "abcdefghijklmnopqrstuvwxyz" + + "😋 Get Emoji — All Emojis to ✂️ Copy and 📋 Paste 👌", + ], + ]; + + for (const [input, expected, label] of fixtures) { + it(`${JSON.stringify(label)}`, async () => { + const path = `/tmp/bun-test-${Bun.hash(label).toString(10)}.txt`; + try { + require("fs").unlinkSync(path); + } catch (e) {} + + const sink = Bun.file(path).writer(); + for (let i = 0; i < input.length; i++) { + sink.write(input[i]); + } + await sink.end(); + + const output = new Uint8Array(await Bun.file(path).arrayBuffer()); + for (let i = 0; i < expected.length; i++) { + expect(output[i]).toBe(expected[i]); + } + expect(output.byteLength).toBe(expected.byteLength); + }); + + it(`flushing -> ${JSON.stringify(label)}`, async () => { + const path = `/tmp/bun-test-${Bun.hash(label).toString(10)}.txt`; + try { + require("fs").unlinkSync(path); + } catch (e) {} + + const sink = Bun.file(path).writer(); + for (let i = 0; i < input.length; i++) { + sink.write(input[i]); + await sink.flush(); + } + await sink.end(); + + const output = new Uint8Array(await Bun.file(path).arrayBuffer()); + for (let i = 0; i < expected.length; i++) { + expect(output[i]).toBe(expected[i]); + } + expect(output.byteLength).toBe(expected.byteLength); + }); + + it(`highWaterMark -> ${JSON.stringify(label)}`, async () => { + const path = `/tmp/bun-test-${Bun.hash(label).toString(10)}.txt`; + try { + require("fs").unlinkSync(path); + } catch (e) {} + + const sink = Bun.file(path).writer({ highWaterMark: 1 }); + for (let i = 0; i < input.length; i++) { + sink.write(input[i]); + await sink.flush(); + } + await sink.end(); + + const output = new Uint8Array(await Bun.file(path).arrayBuffer()); + for (let i = 0; i < expected.length; i++) { + expect(output[i]).toBe(expected[i]); + } + expect(output.byteLength).toBe(expected.byteLength); + }); + } +}); |