diff --git a/src/boringssl.zig b/src/boringssl.zig index 3f8892a1aa..fe4c63e203 100644 --- a/src/boringssl.zig +++ b/src/boringssl.zig @@ -1,5 +1,9 @@ +// TODO: move all custom functions from the translated file into this file, then +// the translated file can be provided by `zig translate-c` const boring = @import("./deps/boringssl.translated.zig"); -pub usingnamespace boring; +/// BoringSSL's translated C API +pub const c = boring; + const std = @import("std"); const bun = @import("root").bun; const c_ares = @import("./deps/c_ares.zig"); diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index 6ea126e378..45b40bfd2e 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -1055,7 +1055,8 @@ pub fn indexOfLine(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) b pub const Crypto = struct { const Hashers = @import("../../sha.zig"); - const BoringSSL = bun.BoringSSL; + const BoringSSL = bun.BoringSSL.c; + pub const HMAC = struct { ctx: BoringSSL.HMAC_CTX, algorithm: EVP.Algorithm, @@ -1479,7 +1480,7 @@ pub const Crypto = struct { }; pub fn init(algorithm: Algorithm, md: *const BoringSSL.EVP_MD, engine: *BoringSSL.ENGINE) EVP { - BoringSSL.load(); + bun.BoringSSL.load(); var ctx: BoringSSL.EVP_MD_CTX = undefined; BoringSSL.EVP_MD_CTX_init(&ctx); @@ -1567,7 +1568,7 @@ pub const Crypto = struct { }; pub fn createCryptoError(globalThis: *JSC.JSGlobalObject, err_code: u32) JSValue { - return BoringSSL.ERR_toJS(globalThis, err_code); + return bun.BoringSSL.ERR_toJS(globalThis, err_code); } const unknown_password_algorithm_message = "unknown algorithm, expected one of: \"bcrypt\", \"argon2id\", \"argon2d\", \"argon2i\" (default is \"argon2id\")"; @@ -4622,7 +4623,7 @@ pub const JSZlib = struct { } }; -pub usingnamespace @import("./bun/subprocess.zig"); +pub const Subprocess = @import("./bun/subprocess.zig"); const InternalTestingAPIs = struct { pub fn BunInternalFunction__syntaxHighlighter(globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { diff --git a/src/bun.js/api/bun/process.zig b/src/bun.js/api/bun/process.zig index b6fed57c8a..4a03e7e099 100644 --- a/src/bun.js/api/bun/process.zig +++ b/src/bun.js/api/bun/process.zig @@ -1217,13 +1217,13 @@ pub fn spawnProcessPosix( var attr = try PosixSpawn.Attr.init(); defer attr.deinit(); - var flags: i32 = bun.C.POSIX_SPAWN_SETSIGDEF | bun.C.POSIX_SPAWN_SETSIGMASK; + var flags: i32 = bun.c.POSIX_SPAWN_SETSIGDEF | bun.c.POSIX_SPAWN_SETSIGMASK; if (comptime Environment.isMac) { - flags |= bun.C.POSIX_SPAWN_CLOEXEC_DEFAULT; + flags |= bun.c.POSIX_SPAWN_CLOEXEC_DEFAULT; if (options.use_execve_on_macos) { - flags |= bun.C.POSIX_SPAWN_SETEXEC; + flags |= bun.c.POSIX_SPAWN_SETEXEC; if (options.stdin == .buffer or options.stdout == .buffer or options.stderr == .buffer) { Output.panic("Internal error: stdin, stdout, and stderr cannot be buffered when use_execve_on_macos is true", .{}); @@ -1232,7 +1232,7 @@ pub fn spawnProcessPosix( } if (options.detached) { - flags |= bun.C.POSIX_SPAWN_SETSID; + flags |= bun.c.POSIX_SPAWN_SETSID; } if (options.cwd.len > 0) { diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index ec738a4c1a..352369e69c 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -15,7 +15,7 @@ const JSGlobalObject = JSC.JSGlobalObject; const Which = @import("../../../which.zig"); const uws = bun.uws; const ZigString = JSC.ZigString; -const BoringSSL = bun.BoringSSL; +const BoringSSL = bun.BoringSSL.c; const X509 = @import("./x509.zig"); const Async = bun.Async; const uv = bun.windows.libuv; @@ -3541,7 +3541,7 @@ fn NewSocket(comptime ssl: bool) type { // If BoringSSL gave us an error code, let's use it. if (err != 0 and !globalObject.hasException()) { - return globalObject.throwValue(BoringSSL.ERR_toJS(globalObject, err)); + return globalObject.throwValue(bun.BoringSSL.ERR_toJS(globalObject, err)); } // If BoringSSL did not give us an error code, let's throw a generic error. @@ -3945,7 +3945,7 @@ pub const WindowsNamedPipeListeningContext = if (Environment.isWindows) struct { }); if (ssl_config) |ssl_options| { - BoringSSL.load(); + bun.BoringSSL.load(); const ctx_opts: uws.us_bun_socket_context_options_t = JSC.API.ServerConfig.SSLConfig.asUSockets(ssl_options); var err: uws.create_bun_socket_error_t = .none; diff --git a/src/bun.js/api/bun/socket/SocketAddress.zig b/src/bun.js/api/bun/socket/SocketAddress.zig index 37d7a223aa..6b25cae1a8 100644 --- a/src/bun.js/api/bun/socket/SocketAddress.zig +++ b/src/bun.js/api/bun/socket/SocketAddress.zig @@ -594,8 +594,8 @@ const WellKnownAddress = struct { // The same types are defined in a bunch of different places. We should probably unify them. comptime { // Windows doesn't have c.socklen_t. because of course it doesn't. - const other_socklens = if (@hasDecl(bun.C.translated, "socklen_t")) - .{ std.posix.socklen_t, bun.C.translated.socklen_t } + const other_socklens = if (@hasDecl(bun.c, "socklen_t")) + .{ std.posix.socklen_t, bun.c.socklen_t } else .{std.posix.socklen_t}; for (other_socklens) |other_socklen| { @@ -638,7 +638,7 @@ win: { pub const sockaddr_in6 = std.posix.sockaddr.in6; }; } else posix: { - const C = bun.C.translated; + const C = bun.c; break :posix struct { pub const IN4ADDR_LOOPBACK = C.IN4ADDR_LOOPBACK; pub const INET6_ADDRSTRLEN = C.INET6_ADDRSTRLEN; diff --git a/src/bun.js/api/bun/spawn.zig b/src/bun.js/api/bun/spawn.zig index 0f937974b6..ffdcce65fd 100644 --- a/src/bun.js/api/bun/spawn.zig +++ b/src/bun.js/api/bun/spawn.zig @@ -4,24 +4,8 @@ const string = bun.string; const std = @import("std"); const Output = bun.Output; -fn _getSystem() type { - // this is a workaround for a Zig stage1 bug - // the "usingnamespace" is evaluating in dead branches - return brk: { - if (comptime bun.Environment.isLinux) { - const Type = bun.C.linux; - break :brk struct { - pub usingnamespace std.posix.system; - pub usingnamespace Type; - }; - } - - break :brk std.posix.system; - }; -} - const Environment = bun.Environment; -const system = _getSystem(); +const system = std.posix.system; const Maybe = JSC.Maybe; diff --git a/src/bun.js/api/bun/ssl_wrapper.zig b/src/bun.js/api/bun/ssl_wrapper.zig index 7a8a74378f..4c82267d3b 100644 --- a/src/bun.js/api/bun/ssl_wrapper.zig +++ b/src/bun.js/api/bun/ssl_wrapper.zig @@ -1,6 +1,6 @@ const bun = @import("root").bun; -const BoringSSL = bun.BoringSSL; +const BoringSSL = bun.BoringSSL.c; const X509 = @import("./x509.zig"); const JSC = bun.JSC; const uws = bun.uws; @@ -58,7 +58,7 @@ pub fn SSLWrapper(comptime T: type) type { /// Initialize the SSLWrapper with a specific SSL_CTX*, remember to call SSL_CTX_up_ref if you want to keep the SSL_CTX alive after the SSLWrapper is deinitialized pub fn initWithCTX(ctx: *BoringSSL.SSL_CTX, is_client: bool, handlers: Handlers) !This { - BoringSSL.load(); + bun.BoringSSL.load(); const ssl = BoringSSL.SSL_new(ctx) orelse return error.OutOfMemory; errdefer BoringSSL.SSL_free(ssl); @@ -94,7 +94,7 @@ pub fn SSLWrapper(comptime T: type) type { } pub fn init(ssl_options: JSC.API.ServerConfig.SSLConfig, is_client: bool, handlers: Handlers) !This { - BoringSSL.load(); + bun.BoringSSL.load(); const ctx_opts: uws.us_bun_socket_context_options_t = JSC.API.ServerConfig.SSLConfig.asUSockets(ssl_options); var err: uws.create_bun_socket_error_t = .none; diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index d99dd2dffb..d185e5a8f8 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -1,33 +1,60 @@ -const default_allocator = bun.default_allocator; -const bun = @import("root").bun; -const Environment = bun.Environment; +//! The Subprocess object is returned by `Bun.spawn`. This file also holds the +//! code for `Bun.spawnSync` +const Subprocess = @This(); +pub usingnamespace JSC.Codegen.JSSubprocess; +pub usingnamespace bun.NewRefCounted(@This(), deinit, null); -const Global = bun.Global; -const strings = bun.strings; -const string = bun.string; -const Output = bun.Output; -const MutableString = bun.MutableString; -const std = @import("std"); -const Allocator = std.mem.Allocator; -const JSC = bun.JSC; -const JSValue = JSC.JSValue; -const JSGlobalObject = JSC.JSGlobalObject; -const Which = @import("../../../which.zig"); -const Async = bun.Async; -const IPC = @import("../../ipc.zig"); -const uws = bun.uws; -const windows = bun.windows; -const uv = windows.libuv; -const LifecycleScriptSubprocess = bun.install.LifecycleScriptSubprocess; -const Body = JSC.WebCore.Body; -const IPClog = Output.scoped(.IPC, false); +process: *Process, +stdin: Writable, +stdout: Readable, +stderr: Readable, +stdio_pipes: if (Environment.isWindows) std.ArrayListUnmanaged(StdioResult) else std.ArrayListUnmanaged(bun.FileDescriptor) = .{}, +pid_rusage: ?Rusage = null, + +exit_promise: JSC.Strong = .empty, +on_exit_callback: JSC.Strong = .empty, +on_disconnect_callback: JSC.Strong = .empty, + +globalThis: *JSC.JSGlobalObject, +observable_getters: std.enums.EnumSet(enum { + stdin, + stdout, + stderr, + stdio, +}) = .{}, +closed: std.enums.EnumSet(StdioKind) = .{}, +has_pending_activity: std.atomic.Value(bool) = std.atomic.Value(bool).init(true), +this_jsvalue: JSC.JSValue = .zero, + +/// `null` indicates all of the IPC data is uninitialized. +ipc_data: ?IPC.IPCData, +ipc_callback: JSC.Strong = .empty, +flags: Flags = .{}, + +weak_file_sink_stdin_ptr: ?*JSC.WebCore.FileSink = null, +ref_count: u32 = 1, +abort_signal: ?*JSC.AbortSignal = null, + +pub const Flags = packed struct { + is_sync: bool = false, + killed: bool = false, + has_stdin_destructor_called: bool = false, + finalized: bool = false, + deref_on_stdin_destroyed: bool = false, +}; + +pub const SignalCode = bun.SignalCode; + +pub const Poll = union(enum) { + poll_ref: ?*Async.FilePoll, + wait_thread: WaitThreadPoll, +}; + +pub const WaitThreadPoll = struct { + ref_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), + poll_ref: Async.KeepAlive = .{}, +}; -const PosixSpawn = bun.posix.spawn; -const Rusage = bun.posix.spawn.Rusage; -const Process = bun.posix.spawn.Process; -const WaiterThread = bun.posix.spawn.WaiterThread; -const Stdio = bun.spawn.Stdio; -const StdioResult = if (Environment.isWindows) bun.spawn.WindowsSpawnResult.StdioResult else ?bun.FileDescriptor; pub inline fn assertStdioResult(result: StdioResult) void { if (comptime Environment.allow_assert) { if (Environment.isPosix) { @@ -37,6 +64,7 @@ pub inline fn assertStdioResult(result: StdioResult) void { } } } + pub const ResourceUsage = struct { pub usingnamespace JSC.Codegen.JSResourceUsage; rusage: Rusage, @@ -143,896 +171,721 @@ pub fn appendEnvpFromJS(globalThis: *JSC.JSGlobalObject, object: JSC.JSValue, en } } -pub const Subprocess = struct { - const log = Output.scoped(.Subprocess, false); - pub usingnamespace JSC.Codegen.JSSubprocess; - const default_max_buffer_size = 1024 * 1024 * 4; - pub const StdioKind = enum { - stdin, - stdout, - stderr, +const log = Output.scoped(.Subprocess, false); +const default_max_buffer_size = 1024 * 1024 * 4; +pub const StdioKind = enum { + stdin, + stdout, + stderr, - pub fn toFd(this: @This()) bun.FileDescriptor { - return switch (this) { - .stdin => bun.STDIN_FD, - .stdout => bun.STDOUT_FD, - .stderr => bun.STDERR_FD, - }; - } - - pub fn toNum(this: @This()) c_int { - return switch (this) { - .stdin => 0, - .stdout => 1, - .stderr => 2, - }; - } - }; - process: *Process, - stdin: Writable, - stdout: Readable, - stderr: Readable, - stdio_pipes: if (Environment.isWindows) std.ArrayListUnmanaged(StdioResult) else std.ArrayListUnmanaged(bun.FileDescriptor) = .{}, - pid_rusage: ?Rusage = null, - - exit_promise: JSC.Strong = .empty, - on_exit_callback: JSC.Strong = .empty, - on_disconnect_callback: JSC.Strong = .empty, - - globalThis: *JSC.JSGlobalObject, - observable_getters: std.enums.EnumSet(enum { - stdin, - stdout, - stderr, - stdio, - }) = .{}, - closed: std.enums.EnumSet(StdioKind) = .{}, - has_pending_activity: std.atomic.Value(bool) = std.atomic.Value(bool).init(true), - this_jsvalue: JSC.JSValue = .zero, - - /// `null` indicates all of the IPC data is uninitialized. - ipc_data: ?IPC.IPCData, - ipc_callback: JSC.Strong = .empty, - flags: Flags = .{}, - - weak_file_sink_stdin_ptr: ?*JSC.WebCore.FileSink = null, - ref_count: u32 = 1, - abort_signal: ?*JSC.AbortSignal = null, - - usingnamespace bun.NewRefCounted(@This(), deinit, null); - - pub const Flags = packed struct { - is_sync: bool = false, - killed: bool = false, - has_stdin_destructor_called: bool = false, - finalized: bool = false, - deref_on_stdin_destroyed: bool = false, - }; - - pub const SignalCode = bun.SignalCode; - - pub const Poll = union(enum) { - poll_ref: ?*Async.FilePoll, - wait_thread: WaitThreadPoll, - }; - - pub const WaitThreadPoll = struct { - ref_count: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), - poll_ref: Async.KeepAlive = .{}, - }; - - pub fn onAbortSignal(subprocess_ctx: ?*anyopaque, _: JSC.JSValue) callconv(.C) void { - var this: *Subprocess = @ptrCast(@alignCast(subprocess_ctx.?)); - this.clearAbortSignal(); - _ = this.tryKill(SignalCode.default); - } - - pub fn resourceUsage( - this: *Subprocess, - globalObject: *JSGlobalObject, - _: *JSC.CallFrame, - ) bun.JSError!JSValue { - return this.createResourceUsageObject(globalObject); - } - - pub fn createResourceUsageObject(this: *Subprocess, globalObject: *JSGlobalObject) JSValue { - const pid_rusage = this.pid_rusage orelse brk: { - if (Environment.isWindows) { - if (this.process.poller == .uv) { - this.pid_rusage = PosixSpawn.uv_getrusage(&this.process.poller.uv); - break :brk this.pid_rusage.?; - } - } - - return JSValue.jsUndefined(); + pub fn toFd(this: @This()) bun.FileDescriptor { + return switch (this) { + .stdin => bun.STDIN_FD, + .stdout => bun.STDOUT_FD, + .stderr => bun.STDERR_FD, }; + } - const resource_usage = ResourceUsage{ - .rusage = pid_rusage, + pub fn toNum(this: @This()) c_int { + return switch (this) { + .stdin => 0, + .stdout => 1, + .stderr => 2, }; - - var result = bun.default_allocator.create(ResourceUsage) catch { - return globalObject.throwOutOfMemoryValue(); - }; - result.* = resource_usage; - return result.toJS(globalObject); } +}; - pub fn hasExited(this: *const Subprocess) bool { - return this.process.hasExited(); - } +pub fn onAbortSignal(subprocess_ctx: ?*anyopaque, _: JSC.JSValue) callconv(.C) void { + var this: *Subprocess = @ptrCast(@alignCast(subprocess_ctx.?)); + this.clearAbortSignal(); + _ = this.tryKill(SignalCode.default); +} - pub fn hasPendingActivityNonThreadsafe(this: *const Subprocess) bool { - if (this.ipc_data != null) { - return true; - } +pub fn resourceUsage( + this: *Subprocess, + globalObject: *JSGlobalObject, + _: *JSC.CallFrame, +) bun.JSError!JSValue { + return this.createResourceUsageObject(globalObject); +} - if (this.hasPendingActivityStdio()) { - return true; - } - - if (!this.process.hasExited()) { - return true; - } - - return false; - } - - pub fn updateHasPendingActivity(this: *Subprocess) void { - if (comptime Environment.isDebug) { - log("updateHasPendingActivity() {any} -> {any}", .{ - this.has_pending_activity.raw, - this.hasPendingActivityNonThreadsafe(), - }); - } - this.has_pending_activity.store( - this.hasPendingActivityNonThreadsafe(), - .monotonic, - ); - } - - pub fn hasPendingActivityStdio(this: *const Subprocess) bool { - if (this.stdin.hasPendingActivity()) { - return true; - } - - inline for (.{ StdioKind.stdout, StdioKind.stderr }) |kind| { - if (@field(this, @tagName(kind)).hasPendingActivity()) { - return true; +pub fn createResourceUsageObject(this: *Subprocess, globalObject: *JSGlobalObject) JSValue { + const pid_rusage = this.pid_rusage orelse brk: { + if (Environment.isWindows) { + if (this.process.poller == .uv) { + this.pid_rusage = PosixSpawn.uv_getrusage(&this.process.poller.uv); + break :brk this.pid_rusage.?; } } - return false; - } - - pub fn onCloseIO(this: *Subprocess, kind: StdioKind) void { - switch (kind) { - .stdin => { - switch (this.stdin) { - .pipe => |pipe| { - pipe.signal.clear(); - pipe.deref(); - this.stdin = .{ .ignore = {} }; - }, - .buffer => { - this.stdin.buffer.source.detach(); - this.stdin.buffer.deref(); - this.stdin = .{ .ignore = {} }; - }, - else => {}, - } - }, - inline .stdout, .stderr => |tag| { - const out: *Readable = &@field(this, @tagName(tag)); - switch (out.*) { - .pipe => |pipe| { - if (pipe.state == .done) { - out.* = .{ .buffer = pipe.state.done }; - pipe.state = .{ .done = &.{} }; - } else { - out.* = .{ .ignore = {} }; - } - pipe.deref(); - }, - else => {}, - } - }, - } - } - - pub fn hasPendingActivity(this: *Subprocess) callconv(.C) bool { - return this.has_pending_activity.load(.acquire); - } - - pub fn jsRef(this: *Subprocess) void { - this.process.enableKeepingEventLoopAlive(); - - if (!this.hasCalledGetter(.stdin)) { - this.stdin.ref(); - } - - if (!this.hasCalledGetter(.stdout)) { - this.stdout.ref(); - } - - if (!this.hasCalledGetter(.stderr)) { - this.stderr.ref(); - } - - this.updateHasPendingActivity(); - } - - /// This disables the keeping process alive flag on the poll and also in the stdin, stdout, and stderr - pub fn jsUnref(this: *Subprocess) void { - this.process.disableKeepingEventLoopAlive(); - - if (!this.hasCalledGetter(.stdin)) { - this.stdin.unref(); - } - - if (!this.hasCalledGetter(.stdout)) { - this.stdout.unref(); - } - - if (!this.hasCalledGetter(.stderr)) { - this.stderr.unref(); - } - - this.updateHasPendingActivity(); - } - - pub fn constructor(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!*Subprocess { - return globalObject.throw("Cannot construct Subprocess", .{}); - } - - const Readable = union(enum) { - fd: bun.FileDescriptor, - memfd: bun.FileDescriptor, - pipe: *PipeReader, - inherit: void, - ignore: void, - closed: void, - buffer: []u8, - - pub fn memoryCost(this: *const Readable) usize { - return switch (this.*) { - .pipe => @sizeOf(PipeReader) + this.pipe.memoryCost(), - .buffer => this.buffer.len, - else => 0, - }; - } - - pub fn hasPendingActivity(this: *const Readable) bool { - return switch (this.*) { - .pipe => this.pipe.hasPendingActivity(), - else => false, - }; - } - - pub fn ref(this: *Readable) void { - switch (this.*) { - .pipe => { - this.pipe.updateRef(true); - }, - else => {}, - } - } - - pub fn unref(this: *Readable) void { - switch (this.*) { - .pipe => { - this.pipe.updateRef(false); - }, - else => {}, - } - } - - pub fn init(stdio: Stdio, event_loop: *JSC.EventLoop, process: *Subprocess, result: StdioResult, allocator: std.mem.Allocator, max_size: u32, is_sync: bool) Readable { - _ = allocator; // autofix - _ = max_size; // autofix - _ = is_sync; // autofix - assertStdioResult(result); - - if (Environment.isWindows) { - return switch (stdio) { - .inherit => Readable{ .inherit = {} }, - .ignore, .ipc, .path, .memfd => Readable{ .ignore = {} }, - .fd => |fd| Readable{ .fd = fd }, - .dup2 => |dup2| Readable{ .fd = dup2.out.toFd() }, - .pipe => Readable{ .pipe = PipeReader.create(event_loop, process, result) }, - .array_buffer, .blob => Output.panic("TODO: implement ArrayBuffer & Blob support in Stdio readable", .{}), - .capture => Output.panic("TODO: implement capture support in Stdio readable", .{}), - }; - } - - if (comptime Environment.isPosix) { - if (stdio == .pipe) { - _ = bun.sys.setNonblocking(result.?); - } - } - - return switch (stdio) { - .inherit => Readable{ .inherit = {} }, - .ignore, .ipc, .path => Readable{ .ignore = {} }, - .fd => Readable{ .fd = result.? }, - .memfd => Readable{ .memfd = stdio.memfd }, - .pipe => Readable{ .pipe = PipeReader.create(event_loop, process, result) }, - .array_buffer, .blob => Output.panic("TODO: implement ArrayBuffer & Blob support in Stdio readable", .{}), - .capture => Output.panic("TODO: implement capture support in Stdio readable", .{}), - .dup2 => Output.panic("TODO: implement dup2 support in Stdio readable", .{}), - }; - } - - pub fn onClose(this: *Readable, _: ?bun.sys.Error) void { - this.* = .closed; - } - - pub fn onReady(_: *Readable, _: ?JSC.WebCore.Blob.SizeType, _: ?JSC.WebCore.Blob.SizeType) void {} - - pub fn onStart(_: *Readable) void {} - - pub fn close(this: *Readable) void { - switch (this.*) { - .memfd => |fd| { - this.* = .{ .closed = {} }; - _ = bun.sys.close(fd); - }, - .fd => |_| { - this.* = .{ .closed = {} }; - }, - .pipe => { - this.pipe.close(); - }, - else => {}, - } - } - - pub fn finalize(this: *Readable) void { - switch (this.*) { - .memfd => |fd| { - this.* = .{ .closed = {} }; - _ = bun.sys.close(fd); - }, - .fd => { - this.* = .{ .closed = {} }; - }, - .pipe => |pipe| { - defer pipe.detach(); - this.* = .{ .closed = {} }; - }, - else => {}, - } - } - - pub fn toJS(this: *Readable, globalThis: *JSC.JSGlobalObject, exited: bool) JSValue { - _ = exited; // autofix - switch (this.*) { - // should only be reachable when the entire output is buffered. - .memfd => return this.toBufferedValue(globalThis) catch .zero, - - .fd => |fd| { - return fd.toJS(globalThis); - }, - .pipe => |pipe| { - defer pipe.detach(); - this.* = .{ .closed = {} }; - return pipe.toJS(globalThis); - }, - .buffer => |buffer| { - defer this.* = .{ .closed = {} }; - - if (buffer.len == 0) { - return JSC.WebCore.ReadableStream.empty(globalThis); - } - - const blob = JSC.WebCore.Blob.init(buffer, bun.default_allocator, globalThis); - return JSC.WebCore.ReadableStream.fromBlob(globalThis, &blob, 0); - }, - else => { - return JSValue.jsUndefined(); - }, - } - } - - pub fn toBufferedValue(this: *Readable, globalThis: *JSC.JSGlobalObject) bun.JSError!JSValue { - switch (this.*) { - .fd => |fd| { - return fd.toJS(globalThis); - }, - .memfd => |fd| { - if (comptime !Environment.isPosix) { - Output.panic("memfd is only supported on Linux", .{}); - } - this.* = .{ .closed = {} }; - return JSC.ArrayBuffer.toJSBufferFromMemfd(fd, globalThis); - }, - .pipe => |pipe| { - defer pipe.detach(); - this.* = .{ .closed = {} }; - return pipe.toBuffer(globalThis); - }, - .buffer => |buf| { - this.* = .{ .closed = {} }; - - return JSC.MarkedArrayBuffer.fromBytes(buf, bun.default_allocator, .Uint8Array).toNodeBuffer(globalThis); - }, - else => { - return JSValue.jsUndefined(); - }, - } - } - }; - - pub fn getStderr( - this: *Subprocess, - globalThis: *JSGlobalObject, - ) JSValue { - this.observable_getters.insert(.stderr); - return this.stderr.toJS(globalThis, this.hasExited()); - } - - pub fn getStdin( - this: *Subprocess, - globalThis: *JSGlobalObject, - ) JSValue { - this.observable_getters.insert(.stdin); - return this.stdin.toJS(globalThis, this); - } - - pub fn getStdout( - this: *Subprocess, - globalThis: *JSGlobalObject, - ) JSValue { - this.observable_getters.insert(.stdout); - return this.stdout.toJS(globalThis, this.hasExited()); - } - - pub fn asyncDispose( - this: *Subprocess, - global: *JSGlobalObject, - _: *JSC.CallFrame, - ) bun.JSError!JSValue { - if (this.process.hasExited()) { - // rely on GC to clean everything up in this case - return .undefined; - } - - // unref streams so that this disposed process will not prevent - // the process from exiting causing a hang - this.stdin.unref(); - this.stdout.unref(); - this.stderr.unref(); - - switch (this.tryKill(SignalCode.default)) { - .result => {}, - .err => |err| { - // Signal 9 should always be fine, but just in case that somehow fails. - return global.throwValue(err.toJSC(global)); - }, - } - - return this.getExited(global); - } - - pub fn kill( - this: *Subprocess, - globalThis: *JSGlobalObject, - callframe: *JSC.CallFrame, - ) bun.JSError!JSValue { - this.this_jsvalue = callframe.this(); - - var arguments = callframe.arguments_old(1); - // If signal is 0, then no actual signal is sent, but error checking - // is still performed. - const sig: i32 = brk: { - if (arguments.ptr[0].getNumber()) |sig64| { - // Node does this: - if (std.math.isNan(sig64)) { - break :brk SignalCode.default; - } - - // This matches node behavior, minus some details with the error messages: https://gist.github.com/Jarred-Sumner/23ba38682bf9d84dff2f67eb35c42ab6 - if (std.math.isInf(sig64) or @trunc(sig64) != sig64) { - return globalThis.throwInvalidArguments("Unknown signal", .{}); - } - - if (sig64 < 0) { - return globalThis.throwInvalidArguments("Invalid signal: must be >= 0", .{}); - } - - if (sig64 > 31) { - return globalThis.throwInvalidArguments("Invalid signal: must be < 32", .{}); - } - - break :brk @intFromFloat(sig64); - } else if (arguments.ptr[0].isString()) { - if (arguments.ptr[0].asString().length() == 0) { - break :brk SignalCode.default; - } - const signal_code = try arguments.ptr[0].toEnum(globalThis, "signal", SignalCode); - break :brk @intFromEnum(signal_code); - } else if (!arguments.ptr[0].isEmptyOrUndefinedOrNull()) { - return globalThis.throwInvalidArguments("Invalid signal: must be a string or an integer", .{}); - } - - break :brk SignalCode.default; - }; - - if (globalThis.hasException()) return .zero; - - switch (this.tryKill(sig)) { - .result => {}, - .err => |err| { - // EINVAL or ENOSYS means the signal is not supported in the current platform (most likely unsupported on windows) - return globalThis.throwValue(err.toJSC(globalThis)); - }, - } - return JSValue.jsUndefined(); + }; + + const resource_usage = ResourceUsage{ + .rusage = pid_rusage, + }; + + var result = bun.default_allocator.create(ResourceUsage) catch { + return globalObject.throwOutOfMemoryValue(); + }; + result.* = resource_usage; + return result.toJS(globalObject); +} + +pub fn hasExited(this: *const Subprocess) bool { + return this.process.hasExited(); +} + +pub fn hasPendingActivityNonThreadsafe(this: *const Subprocess) bool { + if (this.ipc_data != null) { + return true; } - pub fn hasKilled(this: *const Subprocess) bool { - return this.process.hasKilled(); + if (this.hasPendingActivityStdio()) { + return true; } - pub fn tryKill(this: *Subprocess, sig: i32) JSC.Maybe(void) { - if (this.hasExited()) { - return .{ .result = {} }; - } - return this.process.kill(@intCast(sig)); + if (!this.process.hasExited()) { + return true; } - fn hasCalledGetter(this: *Subprocess, comptime getter: @Type(.enum_literal)) bool { - return this.observable_getters.contains(getter); - } + return false; +} - fn closeProcess(this: *Subprocess) void { - if (comptime !Environment.isLinux) { - return; - } - this.process.close(); - } - - pub fn doRef(this: *Subprocess, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - this.jsRef(); - return .undefined; - } - - pub fn doUnref(this: *Subprocess, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - this.jsUnref(); - return .undefined; - } - - pub fn onStdinDestroyed(this: *Subprocess) void { - const must_deref = this.flags.deref_on_stdin_destroyed; - this.flags.deref_on_stdin_destroyed = false; - defer if (must_deref) this.deref(); - - this.flags.has_stdin_destructor_called = true; - this.weak_file_sink_stdin_ptr = null; - - if (!this.flags.finalized) { - // otherwise update the pending activity flag - this.updateHasPendingActivity(); - } - } - - pub fn doSend(this: *Subprocess, global: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { - IPClog("Subprocess#doSend", .{}); - const ipc_data = &(this.ipc_data orelse { - if (this.hasExited()) { - return global.throw("Subprocess.send() cannot be used after the process has exited.", .{}); - } else { - return global.throw("Subprocess.send() can only be used if an IPC channel is open.", .{}); - } +pub fn updateHasPendingActivity(this: *Subprocess) void { + if (comptime Environment.isDebug) { + log("updateHasPendingActivity() {any} -> {any}", .{ + this.has_pending_activity.raw, + this.hasPendingActivityNonThreadsafe(), }); + } + this.has_pending_activity.store( + this.hasPendingActivityNonThreadsafe(), + .monotonic, + ); +} - if (callFrame.argumentsCount() == 0) { - return global.throwInvalidArguments("Subprocess.send() requires one argument", .{}); +pub fn hasPendingActivityStdio(this: *const Subprocess) bool { + if (this.stdin.hasPendingActivity()) { + return true; + } + + inline for (.{ StdioKind.stdout, StdioKind.stderr }) |kind| { + if (@field(this, @tagName(kind)).hasPendingActivity()) { + return true; } - - const value = callFrame.argument(0); - - const success = ipc_data.serializeAndSend(global, value); - if (!success) return .zero; - - return .undefined; - } - pub fn disconnectIPC(this: *Subprocess, nextTick: bool) void { - const ipc_data = this.ipc() orelse return; - ipc_data.close(nextTick); - } - pub fn disconnect(this: *Subprocess, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { - _ = globalThis; - _ = callframe; - this.disconnectIPC(true); - return .undefined; } - pub fn getConnected(this: *Subprocess, globalThis: *JSGlobalObject) JSValue { - _ = globalThis; - const ipc_data = this.ipc(); - return JSValue.jsBoolean(ipc_data != null and ipc_data.?.disconnected == false); - } + return false; +} - pub fn pid(this: *const Subprocess) i32 { - return @intCast(this.process.pid); - } - - pub fn getPid( - this: *Subprocess, - _: *JSGlobalObject, - ) JSValue { - return JSValue.jsNumber(this.pid()); - } - - pub fn getKilled( - this: *Subprocess, - _: *JSGlobalObject, - ) JSValue { - return JSValue.jsBoolean(this.hasKilled()); - } - - pub fn getStdio( - this: *Subprocess, - global: *JSGlobalObject, - ) JSValue { - const array = JSValue.createEmptyArray(global, 0); - array.push(global, .null); - array.push(global, .null); // TODO: align this with options - array.push(global, .null); // TODO: align this with options - - this.observable_getters.insert(.stdio); - var pipes = this.stdio_pipes.items; - if (this.ipc_data != null) { - array.push(global, .null); - pipes = pipes[@min(1, pipes.len)..]; - } - - for (pipes) |item| { - if (Environment.isWindows) { - if (item == .buffer) { - const fdno: usize = @intFromPtr(item.buffer.fd().cast()); - array.push(global, JSValue.jsNumber(fdno)); - } - } else { - array.push(global, JSValue.jsNumber(item.cast())); - } - } - return array; - } - - pub const Source = union(enum) { - blob: JSC.WebCore.AnyBlob, - array_buffer: JSC.ArrayBuffer.Strong, - detached: void, - - pub fn memoryCost(this: *const Source) usize { - // Memory cost of Source and each of the particular fields is covered by @sizeOf(Subprocess). - return switch (this.*) { - .blob => this.blob.memoryCost(), - // ArrayBuffer is owned by GC. - .array_buffer => 0, - .detached => 0, - }; - } - - pub fn slice(this: *const Source) []const u8 { - return switch (this.*) { - .blob => this.blob.slice(), - .array_buffer => this.array_buffer.slice(), - else => @panic("Invalid source"), - }; - } - - pub fn detach(this: *@This()) void { - switch (this.*) { - .blob => { - this.blob.detach(); +pub fn onCloseIO(this: *Subprocess, kind: StdioKind) void { + switch (kind) { + .stdin => { + switch (this.stdin) { + .pipe => |pipe| { + pipe.signal.clear(); + pipe.deref(); + this.stdin = .{ .ignore = {} }; }, - .array_buffer => { - this.array_buffer.deinit(); + .buffer => { + this.stdin.buffer.source.detach(); + this.stdin.buffer.deref(); + this.stdin = .{ .ignore = {} }; }, else => {}, } - this.* = .detached; - } - }; - - pub const StaticPipeWriter = NewStaticPipeWriter(Subprocess); - - pub fn NewStaticPipeWriter(comptime ProcessType: type) type { - return struct { - writer: IOWriter = .{}, - stdio_result: StdioResult, - source: Source = .{ .detached = {} }, - process: *ProcessType = undefined, - event_loop: JSC.EventLoopHandle, - ref_count: u32 = 1, - buffer: []const u8 = "", - - pub usingnamespace bun.NewRefCounted(@This(), _deinit, null); - const This = @This(); - const print = bun.Output.scoped(.StaticPipeWriter, false); - - pub const IOWriter = bun.io.BufferedWriter( - This, - onWrite, - onError, - onClose, - getBuffer, - flush, - ); - pub const Poll = IOWriter; - - pub fn updateRef(this: *This, add: bool) void { - this.writer.updateRef(this.event_loop, add); + }, + inline .stdout, .stderr => |tag| { + const out: *Readable = &@field(this, @tagName(tag)); + switch (out.*) { + .pipe => |pipe| { + if (pipe.state == .done) { + out.* = .{ .buffer = pipe.state.done }; + pipe.state = .{ .done = &.{} }; + } else { + out.* = .{ .ignore = {} }; + } + pipe.deref(); + }, + else => {}, } + }, + } +} - pub fn getBuffer(this: *This) []const u8 { - return this.buffer; - } +pub fn hasPendingActivity(this: *Subprocess) callconv(.C) bool { + return this.has_pending_activity.load(.acquire); +} - pub fn close(this: *This) void { - log("StaticPipeWriter(0x{x}) close()", .{@intFromPtr(this)}); - this.writer.close(); - } +pub fn jsRef(this: *Subprocess) void { + this.process.enableKeepingEventLoopAlive(); - pub fn flush(this: *This) void { - if (this.buffer.len > 0) - this.writer.write(); - } + if (!this.hasCalledGetter(.stdin)) { + this.stdin.ref(); + } - pub fn create(event_loop: anytype, subprocess: *ProcessType, result: StdioResult, source: Source) *This { - const this = This.new(.{ - .event_loop = JSC.EventLoopHandle.init(event_loop), - .process = subprocess, - .stdio_result = result, - .source = source, - }); - if (Environment.isWindows) { - this.writer.setPipe(this.stdio_result.buffer); - } - this.writer.setParent(this); - return this; - } + if (!this.hasCalledGetter(.stdout)) { + this.stdout.ref(); + } - pub fn start(this: *This) JSC.Maybe(void) { - log("StaticPipeWriter(0x{x}) start()", .{@intFromPtr(this)}); - this.ref(); - this.buffer = this.source.slice(); - if (Environment.isWindows) { - return this.writer.startWithCurrentPipe(); - } - switch (this.writer.start(this.stdio_result.?, true)) { - .err => |err| { - return .{ .err = err }; - }, - .result => { - if (comptime Environment.isPosix) { - const poll = this.writer.handle.poll; - poll.flags.insert(.socket); - } + if (!this.hasCalledGetter(.stderr)) { + this.stderr.ref(); + } - return .{ .result = {} }; - }, - } - } + this.updateHasPendingActivity(); +} - pub fn onWrite(this: *This, amount: usize, status: bun.io.WriteStatus) void { - log("StaticPipeWriter(0x{x}) onWrite(amount={d} {})", .{ @intFromPtr(this), amount, status }); - this.buffer = this.buffer[@min(amount, this.buffer.len)..]; - if (status == .end_of_file or this.buffer.len == 0) { - this.writer.close(); - } - } +/// This disables the keeping process alive flag on the poll and also in the stdin, stdout, and stderr +pub fn jsUnref(this: *Subprocess) void { + this.process.disableKeepingEventLoopAlive(); - pub fn onError(this: *This, err: bun.sys.Error) void { - log("StaticPipeWriter(0x{x}) onError(err={any})", .{ @intFromPtr(this), err }); - this.source.detach(); - } + if (!this.hasCalledGetter(.stdin)) { + this.stdin.unref(); + } - pub fn onClose(this: *This) void { - log("StaticPipeWriter(0x{x}) onClose()", .{@intFromPtr(this)}); - this.source.detach(); - this.process.onCloseIO(.stdin); - } + if (!this.hasCalledGetter(.stdout)) { + this.stdout.unref(); + } - fn _deinit(this: *This) void { - this.writer.end(); - this.source.detach(); - this.destroy(); - } + if (!this.hasCalledGetter(.stderr)) { + this.stderr.unref(); + } - pub fn memoryCost(this: *const This) usize { - return @sizeOf(@This()) + this.source.memoryCost() + this.writer.memoryCost(); - } + this.updateHasPendingActivity(); +} - pub fn loop(this: *This) *uws.Loop { - return this.event_loop.loop(); - } +pub fn constructor(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!*Subprocess { + return globalObject.throw("Cannot construct Subprocess", .{}); +} - pub fn watch(this: *This) void { - if (this.buffer.len > 0) { - this.writer.watch(); - } - } +const Readable = union(enum) { + fd: bun.FileDescriptor, + memfd: bun.FileDescriptor, + pipe: *PipeReader, + inherit: void, + ignore: void, + closed: void, + buffer: []u8, - pub fn eventLoop(this: *This) JSC.EventLoopHandle { - return this.event_loop; - } + pub fn memoryCost(this: *const Readable) usize { + return switch (this.*) { + .pipe => @sizeOf(PipeReader) + this.pipe.memoryCost(), + .buffer => this.buffer.len, + else => 0, }; } - pub const PipeReader = struct { - reader: IOReader = undefined, - process: ?*Subprocess = null, - event_loop: *JSC.EventLoop = undefined, - ref_count: u32 = 1, - state: union(enum) { - pending: void, - done: []u8, - err: bun.sys.Error, - } = .{ .pending = {} }, + pub fn hasPendingActivity(this: *const Readable) bool { + return switch (this.*) { + .pipe => this.pipe.hasPendingActivity(), + else => false, + }; + } + + pub fn ref(this: *Readable) void { + switch (this.*) { + .pipe => { + this.pipe.updateRef(true); + }, + else => {}, + } + } + + pub fn unref(this: *Readable) void { + switch (this.*) { + .pipe => { + this.pipe.updateRef(false); + }, + else => {}, + } + } + + pub fn init(stdio: Stdio, event_loop: *JSC.EventLoop, process: *Subprocess, result: StdioResult, allocator: std.mem.Allocator, max_size: u32, is_sync: bool) Readable { + _ = allocator; // autofix + _ = max_size; // autofix + _ = is_sync; // autofix + assertStdioResult(result); + + if (Environment.isWindows) { + return switch (stdio) { + .inherit => Readable{ .inherit = {} }, + .ignore, .ipc, .path, .memfd => Readable{ .ignore = {} }, + .fd => |fd| Readable{ .fd = fd }, + .dup2 => |dup2| Readable{ .fd = dup2.out.toFd() }, + .pipe => Readable{ .pipe = PipeReader.create(event_loop, process, result) }, + .array_buffer, .blob => Output.panic("TODO: implement ArrayBuffer & Blob support in Stdio readable", .{}), + .capture => Output.panic("TODO: implement capture support in Stdio readable", .{}), + }; + } + + if (comptime Environment.isPosix) { + if (stdio == .pipe) { + _ = bun.sys.setNonblocking(result.?); + } + } + + return switch (stdio) { + .inherit => Readable{ .inherit = {} }, + .ignore, .ipc, .path => Readable{ .ignore = {} }, + .fd => Readable{ .fd = result.? }, + .memfd => Readable{ .memfd = stdio.memfd }, + .pipe => Readable{ .pipe = PipeReader.create(event_loop, process, result) }, + .array_buffer, .blob => Output.panic("TODO: implement ArrayBuffer & Blob support in Stdio readable", .{}), + .capture => Output.panic("TODO: implement capture support in Stdio readable", .{}), + .dup2 => Output.panic("TODO: implement dup2 support in Stdio readable", .{}), + }; + } + + pub fn onClose(this: *Readable, _: ?bun.sys.Error) void { + this.* = .closed; + } + + pub fn onReady(_: *Readable, _: ?JSC.WebCore.Blob.SizeType, _: ?JSC.WebCore.Blob.SizeType) void {} + + pub fn onStart(_: *Readable) void {} + + pub fn close(this: *Readable) void { + switch (this.*) { + .memfd => |fd| { + this.* = .{ .closed = {} }; + _ = bun.sys.close(fd); + }, + .fd => |_| { + this.* = .{ .closed = {} }; + }, + .pipe => { + this.pipe.close(); + }, + else => {}, + } + } + + pub fn finalize(this: *Readable) void { + switch (this.*) { + .memfd => |fd| { + this.* = .{ .closed = {} }; + _ = bun.sys.close(fd); + }, + .fd => { + this.* = .{ .closed = {} }; + }, + .pipe => |pipe| { + defer pipe.detach(); + this.* = .{ .closed = {} }; + }, + else => {}, + } + } + + pub fn toJS(this: *Readable, globalThis: *JSC.JSGlobalObject, exited: bool) JSValue { + _ = exited; // autofix + switch (this.*) { + // should only be reachable when the entire output is buffered. + .memfd => return this.toBufferedValue(globalThis) catch .zero, + + .fd => |fd| { + return fd.toJS(globalThis); + }, + .pipe => |pipe| { + defer pipe.detach(); + this.* = .{ .closed = {} }; + return pipe.toJS(globalThis); + }, + .buffer => |buffer| { + defer this.* = .{ .closed = {} }; + + if (buffer.len == 0) { + return JSC.WebCore.ReadableStream.empty(globalThis); + } + + const blob = JSC.WebCore.Blob.init(buffer, bun.default_allocator, globalThis); + return JSC.WebCore.ReadableStream.fromBlob(globalThis, &blob, 0); + }, + else => { + return JSValue.jsUndefined(); + }, + } + } + + pub fn toBufferedValue(this: *Readable, globalThis: *JSC.JSGlobalObject) bun.JSError!JSValue { + switch (this.*) { + .fd => |fd| { + return fd.toJS(globalThis); + }, + .memfd => |fd| { + if (comptime !Environment.isPosix) { + Output.panic("memfd is only supported on Linux", .{}); + } + this.* = .{ .closed = {} }; + return JSC.ArrayBuffer.toJSBufferFromMemfd(fd, globalThis); + }, + .pipe => |pipe| { + defer pipe.detach(); + this.* = .{ .closed = {} }; + return pipe.toBuffer(globalThis); + }, + .buffer => |buf| { + this.* = .{ .closed = {} }; + + return JSC.MarkedArrayBuffer.fromBytes(buf, bun.default_allocator, .Uint8Array).toNodeBuffer(globalThis); + }, + else => { + return JSValue.jsUndefined(); + }, + } + } +}; + +pub fn getStderr( + this: *Subprocess, + globalThis: *JSGlobalObject, +) JSValue { + this.observable_getters.insert(.stderr); + return this.stderr.toJS(globalThis, this.hasExited()); +} + +pub fn getStdin( + this: *Subprocess, + globalThis: *JSGlobalObject, +) JSValue { + this.observable_getters.insert(.stdin); + return this.stdin.toJS(globalThis, this); +} + +pub fn getStdout( + this: *Subprocess, + globalThis: *JSGlobalObject, +) JSValue { + this.observable_getters.insert(.stdout); + return this.stdout.toJS(globalThis, this.hasExited()); +} + +pub fn asyncDispose( + this: *Subprocess, + global: *JSGlobalObject, + _: *JSC.CallFrame, +) bun.JSError!JSValue { + if (this.process.hasExited()) { + // rely on GC to clean everything up in this case + return .undefined; + } + + // unref streams so that this disposed process will not prevent + // the process from exiting causing a hang + this.stdin.unref(); + this.stdout.unref(); + this.stderr.unref(); + + switch (this.tryKill(SignalCode.default)) { + .result => {}, + .err => |err| { + // Signal 9 should always be fine, but just in case that somehow fails. + return global.throwValue(err.toJSC(global)); + }, + } + + return this.getExited(global); +} + +pub fn kill( + this: *Subprocess, + globalThis: *JSGlobalObject, + callframe: *JSC.CallFrame, +) bun.JSError!JSValue { + this.this_jsvalue = callframe.this(); + + var arguments = callframe.arguments_old(1); + // If signal is 0, then no actual signal is sent, but error checking + // is still performed. + const sig: i32 = brk: { + if (arguments.ptr[0].getNumber()) |sig64| { + // Node does this: + if (std.math.isNan(sig64)) { + break :brk SignalCode.default; + } + + // This matches node behavior, minus some details with the error messages: https://gist.github.com/Jarred-Sumner/23ba38682bf9d84dff2f67eb35c42ab6 + if (std.math.isInf(sig64) or @trunc(sig64) != sig64) { + return globalThis.throwInvalidArguments("Unknown signal", .{}); + } + + if (sig64 < 0) { + return globalThis.throwInvalidArguments("Invalid signal: must be >= 0", .{}); + } + + if (sig64 > 31) { + return globalThis.throwInvalidArguments("Invalid signal: must be < 32", .{}); + } + + break :brk @intFromFloat(sig64); + } else if (arguments.ptr[0].isString()) { + if (arguments.ptr[0].asString().length() == 0) { + break :brk SignalCode.default; + } + const signal_code = try arguments.ptr[0].toEnum(globalThis, "signal", SignalCode); + break :brk @intFromEnum(signal_code); + } else if (!arguments.ptr[0].isEmptyOrUndefinedOrNull()) { + return globalThis.throwInvalidArguments("Invalid signal: must be a string or an integer", .{}); + } + + break :brk SignalCode.default; + }; + + if (globalThis.hasException()) return .zero; + + switch (this.tryKill(sig)) { + .result => {}, + .err => |err| { + // EINVAL or ENOSYS means the signal is not supported in the current platform (most likely unsupported on windows) + return globalThis.throwValue(err.toJSC(globalThis)); + }, + } + + return JSValue.jsUndefined(); +} + +pub fn hasKilled(this: *const Subprocess) bool { + return this.process.hasKilled(); +} + +pub fn tryKill(this: *Subprocess, sig: i32) JSC.Maybe(void) { + if (this.hasExited()) { + return .{ .result = {} }; + } + return this.process.kill(@intCast(sig)); +} + +fn hasCalledGetter(this: *Subprocess, comptime getter: @Type(.enum_literal)) bool { + return this.observable_getters.contains(getter); +} + +fn closeProcess(this: *Subprocess) void { + if (comptime !Environment.isLinux) { + return; + } + this.process.close(); +} + +pub fn doRef(this: *Subprocess, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + this.jsRef(); + return .undefined; +} + +pub fn doUnref(this: *Subprocess, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + this.jsUnref(); + return .undefined; +} + +pub fn onStdinDestroyed(this: *Subprocess) void { + const must_deref = this.flags.deref_on_stdin_destroyed; + this.flags.deref_on_stdin_destroyed = false; + defer if (must_deref) this.deref(); + + this.flags.has_stdin_destructor_called = true; + this.weak_file_sink_stdin_ptr = null; + + if (!this.flags.finalized) { + // otherwise update the pending activity flag + this.updateHasPendingActivity(); + } +} + +pub fn doSend(this: *Subprocess, global: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { + IPClog("Subprocess#doSend", .{}); + const ipc_data = &(this.ipc_data orelse { + if (this.hasExited()) { + return global.throw("Subprocess.send() cannot be used after the process has exited.", .{}); + } else { + return global.throw("Subprocess.send() can only be used if an IPC channel is open.", .{}); + } + }); + + if (callFrame.argumentsCount() == 0) { + return global.throwInvalidArguments("Subprocess.send() requires one argument", .{}); + } + + const value = callFrame.argument(0); + + const success = ipc_data.serializeAndSend(global, value); + if (!success) return .zero; + + return .undefined; +} +pub fn disconnectIPC(this: *Subprocess, nextTick: bool) void { + const ipc_data = this.ipc() orelse return; + ipc_data.close(nextTick); +} +pub fn disconnect(this: *Subprocess, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + _ = globalThis; + _ = callframe; + this.disconnectIPC(true); + return .undefined; +} + +pub fn getConnected(this: *Subprocess, globalThis: *JSGlobalObject) JSValue { + _ = globalThis; + const ipc_data = this.ipc(); + return JSValue.jsBoolean(ipc_data != null and ipc_data.?.disconnected == false); +} + +pub fn pid(this: *const Subprocess) i32 { + return @intCast(this.process.pid); +} + +pub fn getPid( + this: *Subprocess, + _: *JSGlobalObject, +) JSValue { + return JSValue.jsNumber(this.pid()); +} + +pub fn getKilled( + this: *Subprocess, + _: *JSGlobalObject, +) JSValue { + return JSValue.jsBoolean(this.hasKilled()); +} + +pub fn getStdio( + this: *Subprocess, + global: *JSGlobalObject, +) JSValue { + const array = JSValue.createEmptyArray(global, 0); + array.push(global, .null); + array.push(global, .null); // TODO: align this with options + array.push(global, .null); // TODO: align this with options + + this.observable_getters.insert(.stdio); + var pipes = this.stdio_pipes.items; + if (this.ipc_data != null) { + array.push(global, .null); + pipes = pipes[@min(1, pipes.len)..]; + } + + for (pipes) |item| { + if (Environment.isWindows) { + if (item == .buffer) { + const fdno: usize = @intFromPtr(item.buffer.fd().cast()); + array.push(global, JSValue.jsNumber(fdno)); + } + } else { + array.push(global, JSValue.jsNumber(item.cast())); + } + } + return array; +} + +pub const Source = union(enum) { + blob: JSC.WebCore.AnyBlob, + array_buffer: JSC.ArrayBuffer.Strong, + detached: void, + + pub fn memoryCost(this: *const Source) usize { + // Memory cost of Source and each of the particular fields is covered by @sizeOf(Subprocess). + return switch (this.*) { + .blob => this.blob.memoryCost(), + // ArrayBuffer is owned by GC. + .array_buffer => 0, + .detached => 0, + }; + } + + pub fn slice(this: *const Source) []const u8 { + return switch (this.*) { + .blob => this.blob.slice(), + .array_buffer => this.array_buffer.slice(), + else => @panic("Invalid source"), + }; + } + + pub fn detach(this: *@This()) void { + switch (this.*) { + .blob => { + this.blob.detach(); + }, + .array_buffer => { + this.array_buffer.deinit(); + }, + else => {}, + } + this.* = .detached; + } +}; + +pub const StaticPipeWriter = NewStaticPipeWriter(Subprocess); + +pub fn NewStaticPipeWriter(comptime ProcessType: type) type { + return struct { + writer: IOWriter = .{}, stdio_result: StdioResult, + source: Source = .{ .detached = {} }, + process: *ProcessType = undefined, + event_loop: JSC.EventLoopHandle, + ref_count: u32 = 1, + buffer: []const u8 = "", - pub const IOReader = bun.io.BufferedReader; - pub const Poll = IOReader; + pub usingnamespace bun.NewRefCounted(@This(), _deinit, null); + const This = @This(); + const print = bun.Output.scoped(.StaticPipeWriter, false); - pub usingnamespace bun.NewRefCounted(PipeReader, _deinit, null); + pub const IOWriter = bun.io.BufferedWriter( + This, + onWrite, + onError, + onClose, + getBuffer, + flush, + ); + pub const Poll = IOWriter; - pub fn memoryCost(this: *const PipeReader) usize { - return this.reader.memoryCost(); + pub fn updateRef(this: *This, add: bool) void { + this.writer.updateRef(this.event_loop, add); } - pub fn hasPendingActivity(this: *const PipeReader) bool { - if (this.state == .pending) - return true; - - return this.reader.hasPendingActivity(); + pub fn getBuffer(this: *This) []const u8 { + return this.buffer; } - pub fn detach(this: *PipeReader) void { - this.process = null; - this.deref(); + pub fn close(this: *This) void { + log("StaticPipeWriter(0x{x}) close()", .{@intFromPtr(this)}); + this.writer.close(); } - pub fn create(event_loop: *JSC.EventLoop, process: *Subprocess, result: StdioResult) *PipeReader { - var this = PipeReader.new(.{ - .process = process, - .reader = IOReader.init(@This()), - .event_loop = event_loop, + pub fn flush(this: *This) void { + if (this.buffer.len > 0) + this.writer.write(); + } + + pub fn create(event_loop: anytype, subprocess: *ProcessType, result: StdioResult, source: Source) *This { + const this = This.new(.{ + .event_loop = JSC.EventLoopHandle.init(event_loop), + .process = subprocess, .stdio_result = result, + .source = source, }); if (Environment.isWindows) { - this.reader.source = .{ .pipe = this.stdio_result.buffer }; + this.writer.setPipe(this.stdio_result.buffer); } - this.reader.setParent(this); + this.writer.setParent(this); return this; } - pub fn readAll(this: *PipeReader) void { - if (this.state == .pending) - this.reader.read(); - } - - pub fn start(this: *PipeReader, process: *Subprocess, event_loop: *JSC.EventLoop) JSC.Maybe(void) { + pub fn start(this: *This) JSC.Maybe(void) { + log("StaticPipeWriter(0x{x}) start()", .{@intFromPtr(this)}); this.ref(); - this.process = process; - this.event_loop = event_loop; + this.buffer = this.source.slice(); if (Environment.isWindows) { - return this.reader.startWithCurrentPipe(); + return this.writer.startWithCurrentPipe(); } - - switch (this.reader.start(this.stdio_result.?, true)) { + switch (this.writer.start(this.stdio_result.?, true)) { .err => |err| { return .{ .err = err }; }, .result => { if (comptime Environment.isPosix) { - const poll = this.reader.handle.poll; + const poll = this.writer.handle.poll; poll.flags.insert(.socket); - this.reader.flags.socket = true; } return .{ .result = {} }; @@ -1040,304 +893,365 @@ pub const Subprocess = struct { } } - pub const toJS = toReadableStream; - - pub fn onReaderDone(this: *PipeReader) void { - const owned = this.toOwnedSlice(); - this.state = .{ .done = owned }; - if (this.process) |process| { - this.process = null; - process.onCloseIO(this.kind(process)); - this.deref(); + pub fn onWrite(this: *This, amount: usize, status: bun.io.WriteStatus) void { + log("StaticPipeWriter(0x{x}) onWrite(amount={d} {})", .{ @intFromPtr(this), amount, status }); + this.buffer = this.buffer[@min(amount, this.buffer.len)..]; + if (status == .end_of_file or this.buffer.len == 0) { + this.writer.close(); } } - pub fn kind(reader: *const PipeReader, process: *const Subprocess) StdioKind { - if (process.stdout == .pipe and process.stdout.pipe == reader) { - return .stdout; - } - - if (process.stderr == .pipe and process.stderr.pipe == reader) { - return .stderr; - } - - @panic("We should be either stdout or stderr"); + pub fn onError(this: *This, err: bun.sys.Error) void { + log("StaticPipeWriter(0x{x}) onError(err={any})", .{ @intFromPtr(this), err }); + this.source.detach(); } - pub fn toOwnedSlice(this: *PipeReader) []u8 { - if (this.state == .done) { - return this.state.done; - } - // we do not use .toOwnedSlice() because we don't want to reallocate memory. - const out = this.reader._buffer; - this.reader._buffer.items = &.{}; - this.reader._buffer.capacity = 0; - return out.items; + pub fn onClose(this: *This) void { + log("StaticPipeWriter(0x{x}) onClose()", .{@intFromPtr(this)}); + this.source.detach(); + this.process.onCloseIO(.stdin); } - pub fn updateRef(this: *PipeReader, add: bool) void { - this.reader.updateRef(add); - } - - pub fn watch(this: *PipeReader) void { - if (!this.reader.isDone()) - this.reader.watch(); - } - - pub fn toReadableStream(this: *PipeReader, globalObject: *JSC.JSGlobalObject) JSC.JSValue { - defer this.detach(); - - switch (this.state) { - .pending => { - const stream = JSC.WebCore.ReadableStream.fromPipe(globalObject, this, &this.reader); - this.state = .{ .done = &.{} }; - return stream; - }, - .done => |bytes| { - const blob = JSC.WebCore.Blob.init(bytes, bun.default_allocator, globalObject); - this.state = .{ .done = &.{} }; - return JSC.WebCore.ReadableStream.fromBlob(globalObject, &blob, 0); - }, - .err => |err| { - _ = err; // autofix - const empty = JSC.WebCore.ReadableStream.empty(globalObject); - JSC.WebCore.ReadableStream.cancel(&JSC.WebCore.ReadableStream.fromJS(empty, globalObject).?, globalObject); - return empty; - }, - } - } - - pub fn toBuffer(this: *PipeReader, globalThis: *JSC.JSGlobalObject) JSC.JSValue { - switch (this.state) { - .done => |bytes| { - defer this.state = .{ .done = &.{} }; - return JSC.MarkedArrayBuffer.fromBytes(bytes, bun.default_allocator, .Uint8Array).toNodeBuffer(globalThis); - }, - else => { - return JSC.JSValue.undefined; - }, - } - } - - pub fn onReaderError(this: *PipeReader, err: bun.sys.Error) void { - if (this.state == .done) { - bun.default_allocator.free(this.state.done); - } - this.state = .{ .err = err }; - if (this.process) |process| - process.onCloseIO(this.kind(process)); - } - - pub fn close(this: *PipeReader) void { - switch (this.state) { - .pending => { - this.reader.close(); - }, - .done => {}, - .err => {}, - } - } - - pub fn eventLoop(this: *PipeReader) *JSC.EventLoop { - return this.event_loop; - } - - pub fn loop(this: *PipeReader) *uws.Loop { - return this.event_loop.virtual_machine.uwsLoop(); - } - - fn _deinit(this: *PipeReader) void { - if (comptime Environment.isPosix) { - bun.assert(this.reader.isDone()); - } - - if (comptime Environment.isWindows) { - bun.assert(this.reader.source == null or this.reader.source.?.isClosed()); - } - - if (this.state == .done) { - bun.default_allocator.free(this.state.done); - } - - this.reader.deinit(); + fn _deinit(this: *This) void { + this.writer.end(); + this.source.detach(); this.destroy(); } + + pub fn memoryCost(this: *const This) usize { + return @sizeOf(@This()) + this.source.memoryCost() + this.writer.memoryCost(); + } + + pub fn loop(this: *This) *uws.Loop { + return this.event_loop.loop(); + } + + pub fn watch(this: *This) void { + if (this.buffer.len > 0) { + this.writer.watch(); + } + } + + pub fn eventLoop(this: *This) JSC.EventLoopHandle { + return this.event_loop; + } }; +} - const Writable = union(enum) { - pipe: *JSC.WebCore.FileSink, - fd: bun.FileDescriptor, - buffer: *StaticPipeWriter, - memfd: bun.FileDescriptor, - inherit: void, - ignore: void, +pub const PipeReader = struct { + reader: IOReader = undefined, + process: ?*Subprocess = null, + event_loop: *JSC.EventLoop = undefined, + ref_count: u32 = 1, + state: union(enum) { + pending: void, + done: []u8, + err: bun.sys.Error, + } = .{ .pending = {} }, + stdio_result: StdioResult, - pub fn memoryCost(this: *const Writable) usize { - return switch (this.*) { - .pipe => |pipe| pipe.memoryCost(), - .buffer => |buffer| buffer.memoryCost(), - // TODO: memfd - else => 0, - }; + pub const IOReader = bun.io.BufferedReader; + pub const Poll = IOReader; + + pub usingnamespace bun.NewRefCounted(PipeReader, _deinit, null); + + pub fn memoryCost(this: *const PipeReader) usize { + return this.reader.memoryCost(); + } + + pub fn hasPendingActivity(this: *const PipeReader) bool { + if (this.state == .pending) + return true; + + return this.reader.hasPendingActivity(); + } + + pub fn detach(this: *PipeReader) void { + this.process = null; + this.deref(); + } + + pub fn create(event_loop: *JSC.EventLoop, process: *Subprocess, result: StdioResult) *PipeReader { + var this = PipeReader.new(.{ + .process = process, + .reader = IOReader.init(@This()), + .event_loop = event_loop, + .stdio_result = result, + }); + if (Environment.isWindows) { + this.reader.source = .{ .pipe = this.stdio_result.buffer }; + } + this.reader.setParent(this); + return this; + } + + pub fn readAll(this: *PipeReader) void { + if (this.state == .pending) + this.reader.read(); + } + + pub fn start(this: *PipeReader, process: *Subprocess, event_loop: *JSC.EventLoop) JSC.Maybe(void) { + this.ref(); + this.process = process; + this.event_loop = event_loop; + if (Environment.isWindows) { + return this.reader.startWithCurrentPipe(); } - pub fn hasPendingActivity(this: *const Writable) bool { - return switch (this.*) { - .pipe => false, - - // we mark them as .ignore when they are closed, so this must be true - .buffer => true, - else => false, - }; - } - - pub fn ref(this: *Writable) void { - switch (this.*) { - .pipe => { - this.pipe.updateRef(true); - }, - .buffer => { - this.buffer.updateRef(true); - }, - else => {}, - } - } - - pub fn unref(this: *Writable) void { - switch (this.*) { - .pipe => { - this.pipe.updateRef(false); - }, - .buffer => { - this.buffer.updateRef(false); - }, - else => {}, - } - } - - // When the stream has closed we need to be notified to prevent a use-after-free - // We can test for this use-after-free by enabling hot module reloading on a file and then saving it twice - pub fn onClose(this: *Writable, _: ?bun.sys.Error) void { - const process: *Subprocess = @fieldParentPtr("stdin", this); - - if (process.this_jsvalue != .zero) { - if (Subprocess.stdinGetCached(process.this_jsvalue)) |existing_value| { - JSC.WebCore.FileSink.JSSink.setDestroyCallback(existing_value, 0); + switch (this.reader.start(this.stdio_result.?, true)) { + .err => |err| { + return .{ .err = err }; + }, + .result => { + if (comptime Environment.isPosix) { + const poll = this.reader.handle.poll; + poll.flags.insert(.socket); + this.reader.flags.socket = true; } - } - switch (this.*) { - .buffer => { - this.buffer.deref(); - }, - .pipe => { - this.pipe.deref(); - }, - else => {}, - } - - process.onStdinDestroyed(); - - this.* = .{ - .ignore = {}, - }; + return .{ .result = {} }; + }, } - pub fn onReady(_: *Writable, _: ?JSC.WebCore.Blob.SizeType, _: ?JSC.WebCore.Blob.SizeType) void {} - pub fn onStart(_: *Writable) void {} + } - pub fn init( - stdio: Stdio, - event_loop: *JSC.EventLoop, - subprocess: *Subprocess, - result: StdioResult, - ) !Writable { - assertStdioResult(result); + pub const toJS = toReadableStream; - if (Environment.isWindows) { - switch (stdio) { - .pipe => { - if (result == .buffer) { - const pipe = JSC.WebCore.FileSink.createWithPipe(event_loop, result.buffer); + pub fn onReaderDone(this: *PipeReader) void { + const owned = this.toOwnedSlice(); + this.state = .{ .done = owned }; + if (this.process) |process| { + this.process = null; + process.onCloseIO(this.kind(process)); + this.deref(); + } + } - switch (pipe.writer.startWithCurrentPipe()) { - .result => {}, - .err => |err| { - _ = err; // autofix - pipe.deref(); - return error.UnexpectedCreatingStdin; - }, - } - pipe.writer.setParent(pipe); - subprocess.weak_file_sink_stdin_ptr = pipe; - subprocess.ref(); - subprocess.flags.deref_on_stdin_destroyed = true; - subprocess.flags.has_stdin_destructor_called = false; + pub fn kind(reader: *const PipeReader, process: *const Subprocess) StdioKind { + if (process.stdout == .pipe and process.stdout.pipe == reader) { + return .stdout; + } - return Writable{ - .pipe = pipe, - }; - } - return Writable{ .inherit = {} }; - }, + if (process.stderr == .pipe and process.stderr.pipe == reader) { + return .stderr; + } - .blob => |blob| { - return Writable{ - .buffer = StaticPipeWriter.create(event_loop, subprocess, result, .{ .blob = blob }), - }; - }, - .array_buffer => |array_buffer| { - return Writable{ - .buffer = StaticPipeWriter.create(event_loop, subprocess, result, .{ .array_buffer = array_buffer }), - }; - }, - .fd => |fd| { - return Writable{ .fd = fd }; - }, - .dup2 => |dup2| { - return Writable{ .fd = dup2.to.toFd() }; - }, - .inherit => { - return Writable{ .inherit = {} }; - }, - .memfd, .path, .ignore => { - return Writable{ .ignore = {} }; - }, - .ipc, .capture => { - return Writable{ .ignore = {} }; - }, - } + @panic("We should be either stdout or stderr"); + } + + pub fn toOwnedSlice(this: *PipeReader) []u8 { + if (this.state == .done) { + return this.state.done; + } + // we do not use .toOwnedSlice() because we don't want to reallocate memory. + const out = this.reader._buffer; + this.reader._buffer.items = &.{}; + this.reader._buffer.capacity = 0; + return out.items; + } + + pub fn updateRef(this: *PipeReader, add: bool) void { + this.reader.updateRef(add); + } + + pub fn watch(this: *PipeReader) void { + if (!this.reader.isDone()) + this.reader.watch(); + } + + pub fn toReadableStream(this: *PipeReader, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + defer this.detach(); + + switch (this.state) { + .pending => { + const stream = JSC.WebCore.ReadableStream.fromPipe(globalObject, this, &this.reader); + this.state = .{ .done = &.{} }; + return stream; + }, + .done => |bytes| { + const blob = JSC.WebCore.Blob.init(bytes, bun.default_allocator, globalObject); + this.state = .{ .done = &.{} }; + return JSC.WebCore.ReadableStream.fromBlob(globalObject, &blob, 0); + }, + .err => |err| { + _ = err; // autofix + const empty = JSC.WebCore.ReadableStream.empty(globalObject); + JSC.WebCore.ReadableStream.cancel(&JSC.WebCore.ReadableStream.fromJS(empty, globalObject).?, globalObject); + return empty; + }, + } + } + + pub fn toBuffer(this: *PipeReader, globalThis: *JSC.JSGlobalObject) JSC.JSValue { + switch (this.state) { + .done => |bytes| { + defer this.state = .{ .done = &.{} }; + return JSC.MarkedArrayBuffer.fromBytes(bytes, bun.default_allocator, .Uint8Array).toNodeBuffer(globalThis); + }, + else => { + return JSC.JSValue.undefined; + }, + } + } + + pub fn onReaderError(this: *PipeReader, err: bun.sys.Error) void { + if (this.state == .done) { + bun.default_allocator.free(this.state.done); + } + this.state = .{ .err = err }; + if (this.process) |process| + process.onCloseIO(this.kind(process)); + } + + pub fn close(this: *PipeReader) void { + switch (this.state) { + .pending => { + this.reader.close(); + }, + .done => {}, + .err => {}, + } + } + + pub fn eventLoop(this: *PipeReader) *JSC.EventLoop { + return this.event_loop; + } + + pub fn loop(this: *PipeReader) *uws.Loop { + return this.event_loop.virtual_machine.uwsLoop(); + } + + fn _deinit(this: *PipeReader) void { + if (comptime Environment.isPosix) { + bun.assert(this.reader.isDone()); + } + + if (comptime Environment.isWindows) { + bun.assert(this.reader.source == null or this.reader.source.?.isClosed()); + } + + if (this.state == .done) { + bun.default_allocator.free(this.state.done); + } + + this.reader.deinit(); + this.destroy(); + } +}; + +const Writable = union(enum) { + pipe: *JSC.WebCore.FileSink, + fd: bun.FileDescriptor, + buffer: *StaticPipeWriter, + memfd: bun.FileDescriptor, + inherit: void, + ignore: void, + + pub fn memoryCost(this: *const Writable) usize { + return switch (this.*) { + .pipe => |pipe| pipe.memoryCost(), + .buffer => |buffer| buffer.memoryCost(), + // TODO: memfd + else => 0, + }; + } + + pub fn hasPendingActivity(this: *const Writable) bool { + return switch (this.*) { + .pipe => false, + + // we mark them as .ignore when they are closed, so this must be true + .buffer => true, + else => false, + }; + } + + pub fn ref(this: *Writable) void { + switch (this.*) { + .pipe => { + this.pipe.updateRef(true); + }, + .buffer => { + this.buffer.updateRef(true); + }, + else => {}, + } + } + + pub fn unref(this: *Writable) void { + switch (this.*) { + .pipe => { + this.pipe.updateRef(false); + }, + .buffer => { + this.buffer.updateRef(false); + }, + else => {}, + } + } + + // When the stream has closed we need to be notified to prevent a use-after-free + // We can test for this use-after-free by enabling hot module reloading on a file and then saving it twice + pub fn onClose(this: *Writable, _: ?bun.sys.Error) void { + const process: *Subprocess = @fieldParentPtr("stdin", this); + + if (process.this_jsvalue != .zero) { + if (Subprocess.stdinGetCached(process.this_jsvalue)) |existing_value| { + JSC.WebCore.FileSink.JSSink.setDestroyCallback(existing_value, 0); } + } - if (comptime Environment.isPosix) { - if (stdio == .pipe) { - _ = bun.sys.setNonblocking(result.?); - } - } + switch (this.*) { + .buffer => { + this.buffer.deref(); + }, + .pipe => { + this.pipe.deref(); + }, + else => {}, + } + process.onStdinDestroyed(); + + this.* = .{ + .ignore = {}, + }; + } + pub fn onReady(_: *Writable, _: ?JSC.WebCore.Blob.SizeType, _: ?JSC.WebCore.Blob.SizeType) void {} + pub fn onStart(_: *Writable) void {} + + pub fn init( + stdio: Stdio, + event_loop: *JSC.EventLoop, + subprocess: *Subprocess, + result: StdioResult, + ) !Writable { + assertStdioResult(result); + + if (Environment.isWindows) { switch (stdio) { - .dup2 => @panic("TODO dup2 stdio"), .pipe => { - const pipe = JSC.WebCore.FileSink.create(event_loop, result.?); + if (result == .buffer) { + const pipe = JSC.WebCore.FileSink.createWithPipe(event_loop, result.buffer); - switch (pipe.writer.start(pipe.fd, true)) { - .result => {}, - .err => |err| { - _ = err; // autofix - pipe.deref(); - return error.UnexpectedCreatingStdin; - }, + switch (pipe.writer.startWithCurrentPipe()) { + .result => {}, + .err => |err| { + _ = err; // autofix + pipe.deref(); + return error.UnexpectedCreatingStdin; + }, + } + pipe.writer.setParent(pipe); + subprocess.weak_file_sink_stdin_ptr = pipe; + subprocess.ref(); + subprocess.flags.deref_on_stdin_destroyed = true; + subprocess.flags.has_stdin_destructor_called = false; + + return Writable{ + .pipe = pipe, + }; } - - subprocess.weak_file_sink_stdin_ptr = pipe; - subprocess.ref(); - subprocess.flags.has_stdin_destructor_called = false; - subprocess.flags.deref_on_stdin_destroyed = true; - - pipe.writer.handle.poll.flags.insert(.socket); - - return Writable{ - .pipe = pipe, - }; + return Writable{ .inherit = {} }; }, .blob => |blob| { @@ -1350,17 +1264,16 @@ pub const Subprocess = struct { .buffer = StaticPipeWriter.create(event_loop, subprocess, result, .{ .array_buffer = array_buffer }), }; }, - .memfd => |memfd| { - bun.assert(memfd != bun.invalid_fd); - return Writable{ .memfd = memfd }; + .fd => |fd| { + return Writable{ .fd = fd }; }, - .fd => { - return Writable{ .fd = result.? }; + .dup2 => |dup2| { + return Writable{ .fd = dup2.to.toFd() }; }, .inherit => { return Writable{ .inherit = {} }; }, - .path, .ignore => { + .memfd, .path, .ignore => { return Writable{ .ignore = {} }; }, .ipc, .capture => { @@ -1369,1133 +1282,1224 @@ pub const Subprocess = struct { } } - pub fn toJS(this: *Writable, globalThis: *JSC.JSGlobalObject, subprocess: *Subprocess) JSValue { - return switch (this.*) { - .fd => |fd| fd.toJS(globalThis), - .memfd, .ignore => JSValue.jsUndefined(), - .buffer, .inherit => JSValue.jsUndefined(), - .pipe => |pipe| { - this.* = .{ .ignore = {} }; - if (subprocess.process.hasExited() and !subprocess.flags.has_stdin_destructor_called) { - // onAttachedProcessExit() can call deref on the - // subprocess. Since we never called ref(), it would be - // unbalanced to do so, leading to a use-after-free. - // So, let's not do that. - // https://github.com/oven-sh/bun/pull/14092 - bun.debugAssert(!subprocess.flags.deref_on_stdin_destroyed); - const debug_ref_count: if (Environment.isDebug) u32 else u0 = if (Environment.isDebug) subprocess.ref_count else 0; - pipe.onAttachedProcessExit(); - if (comptime Environment.isDebug) { - bun.debugAssert(subprocess.ref_count == debug_ref_count); - } - return pipe.toJS(globalThis); - } else { - subprocess.flags.has_stdin_destructor_called = false; - subprocess.weak_file_sink_stdin_ptr = pipe; - subprocess.ref(); - subprocess.flags.deref_on_stdin_destroyed = true; - if (@intFromPtr(pipe.signal.ptr) == @intFromPtr(subprocess)) { - pipe.signal.clear(); - } - return pipe.toJSWithDestructor( - globalThis, - JSC.WebCore.SinkDestructor.Ptr.init(subprocess), - ); - } - }, - }; - } - - pub fn finalize(this: *Writable) void { - const subprocess: *Subprocess = @fieldParentPtr("stdin", this); - if (subprocess.this_jsvalue != .zero) { - if (JSC.Codegen.JSSubprocess.stdinGetCached(subprocess.this_jsvalue)) |existing_value| { - JSC.WebCore.FileSink.JSSink.setDestroyCallback(existing_value, 0); - } - } - - return switch (this.*) { - .pipe => |pipe| { - if (pipe.signal.ptr == @as(*anyopaque, @ptrCast(this))) { - pipe.signal.clear(); - } - - pipe.deref(); - - this.* = .{ .ignore = {} }; - }, - .buffer => { - this.buffer.updateRef(false); - this.buffer.deref(); - }, - .memfd => |fd| { - _ = bun.sys.close(fd); - this.* = .{ .ignore = {} }; - }, - .ignore => {}, - .fd, .inherit => {}, - }; - } - - pub fn close(this: *Writable) void { - switch (this.*) { - .pipe => |pipe| { - _ = pipe.end(null); - }, - .memfd => |fd| { - _ = bun.sys.close(fd); - this.* = .{ .ignore = {} }; - }, - .fd => { - this.* = .{ .ignore = {} }; - }, - .buffer => { - this.buffer.close(); - }, - .ignore => {}, - .inherit => {}, - } - } - }; - - pub fn memoryCost(this: *const Subprocess) usize { - return @sizeOf(@This()) + - this.process.memoryCost() + - this.stdin.memoryCost() + - this.stdout.memoryCost() + - this.stderr.memoryCost(); - } - - pub fn onProcessExit(this: *Subprocess, process: *Process, status: bun.spawn.Status, rusage: *const Rusage) void { - log("onProcessExit()", .{}); - const this_jsvalue = this.this_jsvalue; - const globalThis = this.globalThis; - const jsc_vm = globalThis.bunVM(); - this_jsvalue.ensureStillAlive(); - this.pid_rusage = rusage.*; - const is_sync = this.flags.is_sync; - this.clearAbortSignal(); - defer this.deref(); - defer this.disconnectIPC(true); - - jsc_vm.onSubprocessExit(process); - - var stdin: ?*JSC.WebCore.FileSink = this.weak_file_sink_stdin_ptr; - var existing_stdin_value = JSC.JSValue.zero; - if (this_jsvalue != .zero) { - if (JSC.Codegen.JSSubprocess.stdinGetCached(this_jsvalue)) |existing_value| { - if (existing_stdin_value.isCell()) { - if (stdin == null) { - // TODO: review this cast - stdin = @alignCast(@ptrCast(JSC.WebCore.FileSink.JSSink.fromJS(existing_value))); - } - - existing_stdin_value = existing_value; - } + if (comptime Environment.isPosix) { + if (stdio == .pipe) { + _ = bun.sys.setNonblocking(result.?); } } - if (this.stdin == .buffer) { - this.stdin.buffer.close(); - } + switch (stdio) { + .dup2 => @panic("TODO dup2 stdio"), + .pipe => { + const pipe = JSC.WebCore.FileSink.create(event_loop, result.?); - if (existing_stdin_value != .zero) { - JSC.WebCore.FileSink.JSSink.setDestroyCallback(existing_stdin_value, 0); - } - - if (stdin) |pipe| { - this.weak_file_sink_stdin_ptr = null; - this.flags.has_stdin_destructor_called = true; - // It is okay if it does call deref() here, as in that case it was truly ref'd. - pipe.onAttachedProcessExit(); - } - - var did_update_has_pending_activity = false; - defer if (!did_update_has_pending_activity) this.updateHasPendingActivity(); - - const loop = jsc_vm.eventLoop(); - - if (!is_sync) { - if (this.exit_promise.trySwap()) |promise| { - loop.enter(); - defer loop.exit(); - - if (!did_update_has_pending_activity) { - this.updateHasPendingActivity(); - did_update_has_pending_activity = true; - } - - switch (status) { - .exited => |exited| promise.asAnyPromise().?.resolve(globalThis, JSValue.jsNumber(exited.code)), - .err => |err| promise.asAnyPromise().?.reject(globalThis, err.toJSC(globalThis)), - .signaled => promise.asAnyPromise().?.resolve(globalThis, JSValue.jsNumber(128 +% @intFromEnum(status.signaled))), - else => { - // crash in debug mode - if (comptime Environment.allow_assert) - unreachable; + switch (pipe.writer.start(pipe.fd, true)) { + .result => {}, + .err => |err| { + _ = err; // autofix + pipe.deref(); + return error.UnexpectedCreatingStdin; }, } - } - if (this.on_exit_callback.trySwap()) |callback| { - const waitpid_value: JSValue = - if (status == .err) - status.err.toJSC(globalThis) - else - .undefined; + subprocess.weak_file_sink_stdin_ptr = pipe; + subprocess.ref(); + subprocess.flags.has_stdin_destructor_called = false; + subprocess.flags.deref_on_stdin_destroyed = true; - const this_value = if (this_jsvalue.isEmptyOrUndefinedOrNull()) .undefined else this_jsvalue; - this_value.ensureStillAlive(); + pipe.writer.handle.poll.flags.insert(.socket); - const args = [_]JSValue{ - this_value, - this.getExitCode(globalThis), - this.getSignalCode(globalThis), - waitpid_value, + return Writable{ + .pipe = pipe, }; + }, - if (!did_update_has_pending_activity) { - this.updateHasPendingActivity(); - did_update_has_pending_activity = true; - } - - loop.runCallback( - callback, - globalThis, - this_value, - &args, - ); - } + .blob => |blob| { + return Writable{ + .buffer = StaticPipeWriter.create(event_loop, subprocess, result, .{ .blob = blob }), + }; + }, + .array_buffer => |array_buffer| { + return Writable{ + .buffer = StaticPipeWriter.create(event_loop, subprocess, result, .{ .array_buffer = array_buffer }), + }; + }, + .memfd => |memfd| { + bun.assert(memfd != bun.invalid_fd); + return Writable{ .memfd = memfd }; + }, + .fd => { + return Writable{ .fd = result.? }; + }, + .inherit => { + return Writable{ .inherit = {} }; + }, + .path, .ignore => { + return Writable{ .ignore = {} }; + }, + .ipc, .capture => { + return Writable{ .ignore = {} }; + }, } } - fn closeIO(this: *Subprocess, comptime io: @Type(.enum_literal)) void { - if (this.closed.contains(io)) return; - this.closed.insert(io); - - // If you never referenced stdout/stderr, they won't be garbage collected. - // - // That means: - // 1. We need to stop watching them - // 2. We need to free the memory - // 3. We need to halt any pending reads (1) - - if (!this.hasCalledGetter(io)) { - @field(this, @tagName(io)).finalize(); - } else { - @field(this, @tagName(io)).close(); - } - } - - fn onPipeClose(this: *uv.Pipe) callconv(.C) void { - // safely free the pipes - bun.default_allocator.destroy(this); - } - - // This must only be run once per Subprocess - pub fn finalizeStreams(this: *Subprocess) void { - log("finalizeStreams", .{}); - this.closeProcess(); - - this.closeIO(.stdin); - this.closeIO(.stdout); - this.closeIO(.stderr); - - close_stdio_pipes: { - if (!this.observable_getters.contains(.stdio)) { - break :close_stdio_pipes; - } - - for (this.stdio_pipes.items) |item| { - if (Environment.isWindows) { - if (item == .buffer) { - item.buffer.close(onPipeClose); + pub fn toJS(this: *Writable, globalThis: *JSC.JSGlobalObject, subprocess: *Subprocess) JSValue { + return switch (this.*) { + .fd => |fd| fd.toJS(globalThis), + .memfd, .ignore => JSValue.jsUndefined(), + .buffer, .inherit => JSValue.jsUndefined(), + .pipe => |pipe| { + this.* = .{ .ignore = {} }; + if (subprocess.process.hasExited() and !subprocess.flags.has_stdin_destructor_called) { + // onAttachedProcessExit() can call deref on the + // subprocess. Since we never called ref(), it would be + // unbalanced to do so, leading to a use-after-free. + // So, let's not do that. + // https://github.com/oven-sh/bun/pull/14092 + bun.debugAssert(!subprocess.flags.deref_on_stdin_destroyed); + const debug_ref_count: if (Environment.isDebug) u32 else u0 = if (Environment.isDebug) subprocess.ref_count else 0; + pipe.onAttachedProcessExit(); + if (comptime Environment.isDebug) { + bun.debugAssert(subprocess.ref_count == debug_ref_count); } + return pipe.toJS(globalThis); } else { - _ = bun.sys.close(item); + subprocess.flags.has_stdin_destructor_called = false; + subprocess.weak_file_sink_stdin_ptr = pipe; + subprocess.ref(); + subprocess.flags.deref_on_stdin_destroyed = true; + if (@intFromPtr(pipe.signal.ptr) == @intFromPtr(subprocess)) { + pipe.signal.clear(); + } + return pipe.toJSWithDestructor( + globalThis, + JSC.WebCore.SinkDestructor.Ptr.init(subprocess), + ); } + }, + }; + } + + pub fn finalize(this: *Writable) void { + const subprocess: *Subprocess = @fieldParentPtr("stdin", this); + if (subprocess.this_jsvalue != .zero) { + if (JSC.Codegen.JSSubprocess.stdinGetCached(subprocess.this_jsvalue)) |existing_value| { + JSC.WebCore.FileSink.JSSink.setDestroyCallback(existing_value, 0); } - this.stdio_pipes.clearAndFree(bun.default_allocator); } - this.exit_promise.deinit(); - this.on_exit_callback.deinit(); - this.on_disconnect_callback.deinit(); + return switch (this.*) { + .pipe => |pipe| { + if (pipe.signal.ptr == @as(*anyopaque, @ptrCast(this))) { + pipe.signal.clear(); + } + + pipe.deref(); + + this.* = .{ .ignore = {} }; + }, + .buffer => { + this.buffer.updateRef(false); + this.buffer.deref(); + }, + .memfd => |fd| { + _ = bun.sys.close(fd); + this.* = .{ .ignore = {} }; + }, + .ignore => {}, + .fd, .inherit => {}, + }; } - pub fn deinit(this: *Subprocess) void { - log("deinit", .{}); - this.destroy(); + pub fn close(this: *Writable) void { + switch (this.*) { + .pipe => |pipe| { + _ = pipe.end(null); + }, + .memfd => |fd| { + _ = bun.sys.close(fd); + this.* = .{ .ignore = {} }; + }, + .fd => { + this.* = .{ .ignore = {} }; + }, + .buffer => { + this.buffer.close(); + }, + .ignore => {}, + .inherit => {}, + } + } +}; + +pub fn memoryCost(this: *const Subprocess) usize { + return @sizeOf(@This()) + + this.process.memoryCost() + + this.stdin.memoryCost() + + this.stdout.memoryCost() + + this.stderr.memoryCost(); +} + +pub fn onProcessExit(this: *Subprocess, process: *Process, status: bun.spawn.Status, rusage: *const Rusage) void { + log("onProcessExit()", .{}); + const this_jsvalue = this.this_jsvalue; + const globalThis = this.globalThis; + const jsc_vm = globalThis.bunVM(); + this_jsvalue.ensureStillAlive(); + this.pid_rusage = rusage.*; + const is_sync = this.flags.is_sync; + this.clearAbortSignal(); + defer this.deref(); + defer this.disconnectIPC(true); + + jsc_vm.onSubprocessExit(process); + + var stdin: ?*JSC.WebCore.FileSink = this.weak_file_sink_stdin_ptr; + var existing_stdin_value = JSC.JSValue.zero; + if (this_jsvalue != .zero) { + if (JSC.Codegen.JSSubprocess.stdinGetCached(this_jsvalue)) |existing_value| { + if (existing_stdin_value.isCell()) { + if (stdin == null) { + // TODO: review this cast + stdin = @alignCast(@ptrCast(JSC.WebCore.FileSink.JSSink.fromJS(existing_value))); + } + + existing_stdin_value = existing_value; + } + } } - fn clearAbortSignal(this: *Subprocess) void { - if (this.abort_signal) |signal| { - this.abort_signal = null; - signal.pendingActivityUnref(); - signal.cleanNativeBindings(this); + if (this.stdin == .buffer) { + this.stdin.buffer.close(); + } + + if (existing_stdin_value != .zero) { + JSC.WebCore.FileSink.JSSink.setDestroyCallback(existing_stdin_value, 0); + } + + if (stdin) |pipe| { + this.weak_file_sink_stdin_ptr = null; + this.flags.has_stdin_destructor_called = true; + // It is okay if it does call deref() here, as in that case it was truly ref'd. + pipe.onAttachedProcessExit(); + } + + var did_update_has_pending_activity = false; + defer if (!did_update_has_pending_activity) this.updateHasPendingActivity(); + + const loop = jsc_vm.eventLoop(); + + if (!is_sync) { + if (this.exit_promise.trySwap()) |promise| { + loop.enter(); + defer loop.exit(); + + if (!did_update_has_pending_activity) { + this.updateHasPendingActivity(); + did_update_has_pending_activity = true; + } + + switch (status) { + .exited => |exited| promise.asAnyPromise().?.resolve(globalThis, JSValue.jsNumber(exited.code)), + .err => |err| promise.asAnyPromise().?.reject(globalThis, err.toJSC(globalThis)), + .signaled => promise.asAnyPromise().?.resolve(globalThis, JSValue.jsNumber(128 +% @intFromEnum(status.signaled))), + else => { + // crash in debug mode + if (comptime Environment.allow_assert) + unreachable; + }, + } + } + + if (this.on_exit_callback.trySwap()) |callback| { + const waitpid_value: JSValue = + if (status == .err) + status.err.toJSC(globalThis) + else + .undefined; + + const this_value = if (this_jsvalue.isEmptyOrUndefinedOrNull()) .undefined else this_jsvalue; + this_value.ensureStillAlive(); + + const args = [_]JSValue{ + this_value, + this.getExitCode(globalThis), + this.getSignalCode(globalThis), + waitpid_value, + }; + + if (!did_update_has_pending_activity) { + this.updateHasPendingActivity(); + did_update_has_pending_activity = true; + } + + loop.runCallback( + callback, + globalThis, + this_value, + &args, + ); + } + } +} + +fn closeIO(this: *Subprocess, comptime io: @Type(.enum_literal)) void { + if (this.closed.contains(io)) return; + this.closed.insert(io); + + // If you never referenced stdout/stderr, they won't be garbage collected. + // + // That means: + // 1. We need to stop watching them + // 2. We need to free the memory + // 3. We need to halt any pending reads (1) + + if (!this.hasCalledGetter(io)) { + @field(this, @tagName(io)).finalize(); + } else { + @field(this, @tagName(io)).close(); + } +} + +fn onPipeClose(this: *uv.Pipe) callconv(.C) void { + // safely free the pipes + bun.default_allocator.destroy(this); +} + +// This must only be run once per Subprocess +pub fn finalizeStreams(this: *Subprocess) void { + log("finalizeStreams", .{}); + this.closeProcess(); + + this.closeIO(.stdin); + this.closeIO(.stdout); + this.closeIO(.stderr); + + close_stdio_pipes: { + if (!this.observable_getters.contains(.stdio)) { + break :close_stdio_pipes; + } + + for (this.stdio_pipes.items) |item| { + if (Environment.isWindows) { + if (item == .buffer) { + item.buffer.close(onPipeClose); + } + } else { + _ = bun.sys.close(item); + } + } + this.stdio_pipes.clearAndFree(bun.default_allocator); + } + + this.exit_promise.deinit(); + this.on_exit_callback.deinit(); + this.on_disconnect_callback.deinit(); +} + +pub fn deinit(this: *Subprocess) void { + log("deinit", .{}); + this.destroy(); +} + +fn clearAbortSignal(this: *Subprocess) void { + if (this.abort_signal) |signal| { + this.abort_signal = null; + signal.pendingActivityUnref(); + signal.cleanNativeBindings(this); + signal.unref(); + } +} + +pub fn finalize(this: *Subprocess) callconv(.C) void { + log("finalize", .{}); + // Ensure any code which references the "this" value doesn't attempt to + // access it after it's been freed We cannot call any methods which + // access GC'd values during the finalizer + this.this_jsvalue = .zero; + + this.clearAbortSignal(); + + bun.assert(!this.hasPendingActivity() or JSC.VirtualMachine.get().isShuttingDown()); + this.finalizeStreams(); + + this.process.detach(); + this.process.deref(); + + this.flags.finalized = true; + this.deref(); +} + +pub fn getExited( + this: *Subprocess, + globalThis: *JSGlobalObject, +) JSValue { + switch (this.process.status) { + .exited => |exit| { + return JSC.JSPromise.resolvedPromiseValue(globalThis, JSValue.jsNumber(exit.code)); + }, + .signaled => |signal| { + return JSC.JSPromise.resolvedPromiseValue(globalThis, JSValue.jsNumber(signal.toExitCode() orelse 254)); + }, + .err => |err| { + return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); + }, + else => { + if (!this.exit_promise.has()) { + this.exit_promise.set(globalThis, JSC.JSPromise.create(globalThis).asValue(globalThis)); + } + + return this.exit_promise.get().?; + }, + } +} + +pub fn getExitCode( + this: *Subprocess, + _: *JSGlobalObject, +) JSValue { + if (this.process.status == .exited) { + return JSC.JSValue.jsNumber(this.process.status.exited.code); + } + return JSC.JSValue.jsNull(); +} + +pub fn getSignalCode( + this: *Subprocess, + global: *JSGlobalObject, +) JSValue { + if (this.process.signalCode()) |signal| { + if (signal.name()) |name| + return JSC.ZigString.init(name).toJS(global) + else + return JSC.JSValue.jsNumber(@intFromEnum(signal)); + } + + return JSC.JSValue.jsNull(); +} + +pub fn spawn(globalThis: *JSC.JSGlobalObject, args: JSValue, secondaryArgsValue: ?JSValue) bun.JSError!JSValue { + return spawnMaybeSync(globalThis, args, secondaryArgsValue, false); +} + +pub fn spawnSync(globalThis: *JSC.JSGlobalObject, args: JSValue, secondaryArgsValue: ?JSValue) bun.JSError!JSValue { + return spawnMaybeSync(globalThis, args, secondaryArgsValue, true); +} + +extern "C" const BUN_DEFAULT_PATH_FOR_SPAWN: [*:0]const u8; + +// This is split into a separate function to conserve stack space. +// On Windows, a single path buffer can take 64 KB. +fn getArgv0(globalThis: *JSC.JSGlobalObject, PATH: []const u8, cwd: []const u8, argv0: ?[*:0]const u8, first_cmd: JSValue, allocator: std.mem.Allocator) bun.JSError!struct { + argv0: [:0]const u8, + arg0: [:0]u8, +} { + var arg0 = try first_cmd.toSliceOrNullWithAllocator(globalThis, allocator); + defer arg0.deinit(); + // Heap allocate it to ensure we don't run out of stack space. + const path_buf: *bun.PathBuffer = try bun.default_allocator.create(bun.PathBuffer); + defer bun.default_allocator.destroy(path_buf); + + var actual_argv0: [:0]const u8 = ""; + + const argv0_to_use: []const u8 = if (argv0) |_argv0| + bun.sliceTo(_argv0, 0) + else + arg0.slice(); + + // This mimicks libuv's behavior, which mimicks execvpe + // Only resolve from $PATH when the command is not an absolute path + const PATH_to_use: []const u8 = if (strings.containsChar(argv0_to_use, '/')) + "" + // If no $PATH is provided, we fallback to the one from environ + // This is already the behavior of the PATH passed in here. + else if (PATH.len > 0) + PATH + else if (comptime Environment.isPosix) + // If the user explicitly passed an empty $PATH, we fallback to the OS-specific default (which libuv also does) + bun.sliceTo(BUN_DEFAULT_PATH_FOR_SPAWN, 0) + else + ""; + + if (PATH_to_use.len == 0) { + actual_argv0 = try allocator.dupeZ(u8, argv0_to_use); + } else { + const resolved = Which.which(path_buf, PATH_to_use, cwd, argv0_to_use) orelse { + return throwCommandNotFound(globalThis, argv0_to_use); + }; + actual_argv0 = try allocator.dupeZ(u8, resolved); + } + + return .{ + .argv0 = actual_argv0, + .arg0 = try allocator.dupeZ(u8, arg0.slice()), + }; +} + +fn getArgv(globalThis: *JSC.JSGlobalObject, args: JSValue, PATH: []const u8, cwd: []const u8, argv0: *?[*:0]const u8, allocator: std.mem.Allocator, argv: *std.ArrayList(?[*:0]const u8)) bun.JSError!void { + var cmds_array = args.arrayIterator(globalThis); + // + 1 for argv0 + // + 1 for null terminator + argv.* = try @TypeOf(argv.*).initCapacity(allocator, cmds_array.len + 2); + + if (args.isEmptyOrUndefinedOrNull()) { + return globalThis.throwInvalidArguments("cmd must be an array of strings", .{}); + } + + if (cmds_array.len == 0) { + return globalThis.throwInvalidArguments("cmd must not be empty", .{}); + } + + const argv0_result = try getArgv0(globalThis, PATH, cwd, argv0.*, cmds_array.next().?, allocator); + + argv0.* = argv0_result.argv0.ptr; + argv.appendAssumeCapacity(argv0_result.arg0.ptr); + + while (cmds_array.next()) |value| { + const arg = try value.toBunString(globalThis); + defer arg.deref(); + + argv.appendAssumeCapacity(try arg.toOwnedSliceZ(allocator)); + } + + if (argv.items.len == 0) { + return globalThis.throwInvalidArguments("cmd must be an array of strings", .{}); + } +} + +pub fn spawnMaybeSync( + globalThis: *JSC.JSGlobalObject, + args_: JSValue, + secondaryArgsValue: ?JSValue, + comptime is_sync: bool, +) bun.JSError!JSValue { + if (comptime is_sync) { + // We skip this on Windows due to test failures. + if (comptime !Environment.isWindows) { + // Since the event loop is recursively called, we need to check if it's safe to recurse. + if (!bun.StackCheck.init().isSafeToRecurse()) { + globalThis.throwStackOverflow(); + return error.JSError; + } + } + } + + var arena = bun.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var override_env = false; + var env_array = std.ArrayListUnmanaged(?[*:0]const u8){}; + var jsc_vm = globalThis.bunVM(); + + var cwd = jsc_vm.transpiler.fs.top_level_dir; + + var stdio = [3]Stdio{ + .{ .ignore = {} }, + .{ .pipe = {} }, + .{ .inherit = {} }, + }; + + if (comptime is_sync) { + stdio[1] = .{ .pipe = {} }; + stdio[2] = .{ .pipe = {} }; + } + var lazy = false; + var on_exit_callback = JSValue.zero; + var on_disconnect_callback = JSValue.zero; + var PATH = jsc_vm.transpiler.env.get("PATH") orelse ""; + var argv = std.ArrayList(?[*:0]const u8).init(allocator); + var cmd_value = JSValue.zero; + var detached = false; + var args = args_; + var maybe_ipc_mode: if (is_sync) void else ?IPC.Mode = if (is_sync) {} else null; + var ipc_callback: JSValue = .zero; + var extra_fds = std.ArrayList(bun.spawn.SpawnOptions.Stdio).init(bun.default_allocator); + var argv0: ?[*:0]const u8 = null; + var ipc_channel: i32 = -1; + + var windows_hide: bool = false; + var windows_verbatim_arguments: bool = false; + var abort_signal: ?*JSC.WebCore.AbortSignal = null; + defer { + // Ensure we clean it up on error. + if (abort_signal) |signal| { signal.unref(); } } - pub fn finalize(this: *Subprocess) callconv(.C) void { - log("finalize", .{}); - // Ensure any code which references the "this" value doesn't attempt to - // access it after it's been freed We cannot call any methods which - // access GC'd values during the finalizer - this.this_jsvalue = .zero; - - this.clearAbortSignal(); - - bun.assert(!this.hasPendingActivity() or JSC.VirtualMachine.get().isShuttingDown()); - this.finalizeStreams(); - - this.process.detach(); - this.process.deref(); - - this.flags.finalized = true; - this.deref(); - } - - pub fn getExited( - this: *Subprocess, - globalThis: *JSGlobalObject, - ) JSValue { - switch (this.process.status) { - .exited => |exit| { - return JSC.JSPromise.resolvedPromiseValue(globalThis, JSValue.jsNumber(exit.code)); - }, - .signaled => |signal| { - return JSC.JSPromise.resolvedPromiseValue(globalThis, JSValue.jsNumber(signal.toExitCode() orelse 254)); - }, - .err => |err| { - return JSC.JSPromise.rejectedPromiseValue(globalThis, err.toJSC(globalThis)); - }, - else => { - if (!this.exit_promise.has()) { - this.exit_promise.set(globalThis, JSC.JSPromise.create(globalThis).asValue(globalThis)); - } - - return this.exit_promise.get().?; - }, - } - } - - pub fn getExitCode( - this: *Subprocess, - _: *JSGlobalObject, - ) JSValue { - if (this.process.status == .exited) { - return JSC.JSValue.jsNumber(this.process.status.exited.code); - } - return JSC.JSValue.jsNull(); - } - - pub fn getSignalCode( - this: *Subprocess, - global: *JSGlobalObject, - ) JSValue { - if (this.process.signalCode()) |signal| { - if (signal.name()) |name| - return JSC.ZigString.init(name).toJS(global) - else - return JSC.JSValue.jsNumber(@intFromEnum(signal)); - } - - return JSC.JSValue.jsNull(); - } - - pub fn spawn(globalThis: *JSC.JSGlobalObject, args: JSValue, secondaryArgsValue: ?JSValue) bun.JSError!JSValue { - return spawnMaybeSync(globalThis, args, secondaryArgsValue, false); - } - - pub fn spawnSync(globalThis: *JSC.JSGlobalObject, args: JSValue, secondaryArgsValue: ?JSValue) bun.JSError!JSValue { - return spawnMaybeSync(globalThis, args, secondaryArgsValue, true); - } - - extern "C" const BUN_DEFAULT_PATH_FOR_SPAWN: [*:0]const u8; - - // This is split into a separate function to conserve stack space. - // On Windows, a single path buffer can take 64 KB. - fn getArgv0(globalThis: *JSC.JSGlobalObject, PATH: []const u8, cwd: []const u8, argv0: ?[*:0]const u8, first_cmd: JSValue, allocator: std.mem.Allocator) bun.JSError!struct { - argv0: [:0]const u8, - arg0: [:0]u8, - } { - var arg0 = try first_cmd.toSliceOrNullWithAllocator(globalThis, allocator); - defer arg0.deinit(); - // Heap allocate it to ensure we don't run out of stack space. - const path_buf: *bun.PathBuffer = try bun.default_allocator.create(bun.PathBuffer); - defer bun.default_allocator.destroy(path_buf); - - var actual_argv0: [:0]const u8 = ""; - - const argv0_to_use: []const u8 = if (argv0) |_argv0| - bun.sliceTo(_argv0, 0) - else - arg0.slice(); - - // This mimicks libuv's behavior, which mimicks execvpe - // Only resolve from $PATH when the command is not an absolute path - const PATH_to_use: []const u8 = if (strings.containsChar(argv0_to_use, '/')) - "" - // If no $PATH is provided, we fallback to the one from environ - // This is already the behavior of the PATH passed in here. - else if (PATH.len > 0) - PATH - else if (comptime Environment.isPosix) - // If the user explicitly passed an empty $PATH, we fallback to the OS-specific default (which libuv also does) - bun.sliceTo(BUN_DEFAULT_PATH_FOR_SPAWN, 0) - else - ""; - - if (PATH_to_use.len == 0) { - actual_argv0 = try allocator.dupeZ(u8, argv0_to_use); - } else { - const resolved = Which.which(path_buf, PATH_to_use, cwd, argv0_to_use) orelse { - return throwCommandNotFound(globalThis, argv0_to_use); - }; - actual_argv0 = try allocator.dupeZ(u8, resolved); - } - - return .{ - .argv0 = actual_argv0, - .arg0 = try allocator.dupeZ(u8, arg0.slice()), - }; - } - - fn getArgv(globalThis: *JSC.JSGlobalObject, args: JSValue, PATH: []const u8, cwd: []const u8, argv0: *?[*:0]const u8, allocator: std.mem.Allocator, argv: *std.ArrayList(?[*:0]const u8)) bun.JSError!void { - var cmds_array = args.arrayIterator(globalThis); - // + 1 for argv0 - // + 1 for null terminator - argv.* = try @TypeOf(argv.*).initCapacity(allocator, cmds_array.len + 2); - + { if (args.isEmptyOrUndefinedOrNull()) { - return globalThis.throwInvalidArguments("cmd must be an array of strings", .{}); + return globalThis.throwInvalidArguments("cmd must be an array", .{}); } - if (cmds_array.len == 0) { - return globalThis.throwInvalidArguments("cmd must not be empty", .{}); + const args_type = args.jsType(); + if (args_type.isArray()) { + cmd_value = args; + args = secondaryArgsValue orelse JSValue.zero; + } else if (!args.isObject()) { + return globalThis.throwInvalidArguments("cmd must be an array", .{}); + } else if (try args.getTruthy(globalThis, "cmd")) |cmd_value_| { + cmd_value = cmd_value_; + } else { + return globalThis.throwInvalidArguments("cmd must be an array", .{}); } - const argv0_result = try getArgv0(globalThis, PATH, cwd, argv0.*, cmds_array.next().?, allocator); + if (args.isObject()) { + if (try args.getTruthy(globalThis, "argv0")) |argv0_| { + const argv0_str = try argv0_.getZigString(globalThis); + if (argv0_str.len > 0) { + argv0 = try argv0_str.toOwnedSliceZ(allocator); + } + } - argv0.* = argv0_result.argv0.ptr; - argv.appendAssumeCapacity(argv0_result.arg0.ptr); - - while (cmds_array.next()) |value| { - const arg = try value.toBunString(globalThis); - defer arg.deref(); - - argv.appendAssumeCapacity(try arg.toOwnedSliceZ(allocator)); + // need to update `cwd` before searching for executable with `Which.which` + if (try args.getTruthy(globalThis, "cwd")) |cwd_| { + const cwd_str = try cwd_.getZigString(globalThis); + if (cwd_str.len > 0) { + cwd = try cwd_str.toOwnedSliceZ(allocator); + } + } } - if (argv.items.len == 0) { - return globalThis.throwInvalidArguments("cmd must be an array of strings", .{}); + if (args != .zero and args.isObject()) { + // This must run before the stdio parsing happens + if (!is_sync) { + if (try args.getTruthy(globalThis, "ipc")) |val| { + if (val.isCell() and val.isCallable(globalThis.vm())) { + maybe_ipc_mode = ipc_mode: { + if (try args.getTruthy(globalThis, "serialization")) |mode_val| { + if (mode_val.isString()) { + break :ipc_mode IPC.Mode.fromJS(globalThis, mode_val) orelse { + if (!globalThis.hasException()) { + return globalThis.throwInvalidArguments("serialization must be \"json\" or \"advanced\"", .{}); + } + return error.JSError; + }; + } else { + if (!globalThis.hasException()) { + return globalThis.throwInvalidArgumentType("spawn", "serialization", "string"); + } + return .zero; + } + } + break :ipc_mode .advanced; + }; + + ipc_callback = val.withAsyncContextIfNeeded(globalThis); + } + } + } + + if (try args.getTruthy(globalThis, "signal")) |signal_val| { + if (signal_val.as(JSC.WebCore.AbortSignal)) |signal| { + abort_signal = signal.ref(); + } else { + return globalThis.throwInvalidArgumentTypeValue("signal", "AbortSignal", signal_val); + } + } + + if (try args.getTruthy(globalThis, "onDisconnect")) |onDisconnect_| { + if (!onDisconnect_.isCell() or !onDisconnect_.isCallable(globalThis.vm())) { + return globalThis.throwInvalidArguments("onDisconnect must be a function or undefined", .{}); + } + + on_disconnect_callback = if (comptime is_sync) + onDisconnect_ + else + onDisconnect_.withAsyncContextIfNeeded(globalThis); + } + + if (try args.getTruthy(globalThis, "onExit")) |onExit_| { + if (!onExit_.isCell() or !onExit_.isCallable(globalThis.vm())) { + return globalThis.throwInvalidArguments("onExit must be a function or undefined", .{}); + } + + on_exit_callback = if (comptime is_sync) + onExit_ + else + onExit_.withAsyncContextIfNeeded(globalThis); + } + + if (try args.getTruthy(globalThis, "env")) |object| { + if (!object.isObject()) { + return globalThis.throwInvalidArguments("env must be an object", .{}); + } + + override_env = true; + // If the env object does not include a $PATH, it must disable path lookup for argv[0] + var NEW_PATH: []const u8 = ""; + var envp_managed = env_array.toManaged(allocator); + try appendEnvpFromJS(globalThis, object, &envp_managed, &NEW_PATH); + env_array = envp_managed.moveToUnmanaged(); + PATH = NEW_PATH; + } + + try getArgv(globalThis, cmd_value, PATH, cwd, &argv0, allocator, &argv); + + if (try args.get(globalThis, "stdio")) |stdio_val| { + if (!stdio_val.isEmptyOrUndefinedOrNull()) { + if (stdio_val.jsType().isArray()) { + var stdio_iter = stdio_val.arrayIterator(globalThis); + var i: u32 = 0; + while (stdio_iter.next()) |value| : (i += 1) { + try stdio[i].extract(globalThis, i, value); + if (i == 2) + break; + } + i += 1; + + while (stdio_iter.next()) |value| : (i += 1) { + var new_item: Stdio = undefined; + try new_item.extract(globalThis, i, value); + + const opt = switch (new_item.asSpawnOption(i)) { + .result => |opt| opt, + .err => |e| { + return e.throwJS(globalThis); + }, + }; + if (opt == .ipc) { + ipc_channel = @intCast(extra_fds.items.len); + } + try extra_fds.append(opt); + } + } else { + return globalThis.throwInvalidArguments("stdio must be an array", .{}); + } + } + } else { + if (try args.get(globalThis, "stdin")) |value| { + try stdio[0].extract(globalThis, 0, value); + } + + if (try args.get(globalThis, "stderr")) |value| { + try stdio[2].extract(globalThis, 2, value); + } + + if (try args.get(globalThis, "stdout")) |value| { + try stdio[1].extract(globalThis, 1, value); + } + } + + if (comptime !is_sync) { + if (try args.get(globalThis, "lazy")) |lazy_val| { + if (lazy_val.isBoolean()) { + lazy = lazy_val.toBoolean(); + } + } + } + + if (try args.get(globalThis, "detached")) |detached_val| { + if (detached_val.isBoolean()) { + detached = detached_val.toBoolean(); + } + } + + if (Environment.isWindows) { + if (try args.get(globalThis, "windowsHide")) |val| { + if (val.isBoolean()) { + windows_hide = val.asBoolean(); + } + } + + if (try args.get(globalThis, "windowsVerbatimArguments")) |val| { + if (val.isBoolean()) { + windows_verbatim_arguments = val.asBoolean(); + } + } + } + } else { + try getArgv(globalThis, cmd_value, PATH, cwd, &argv0, allocator, &argv); } } - pub fn spawnMaybeSync( - globalThis: *JSC.JSGlobalObject, - args_: JSValue, - secondaryArgsValue: ?JSValue, - comptime is_sync: bool, - ) bun.JSError!JSValue { - if (comptime is_sync) { - // We skip this on Windows due to test failures. - if (comptime !Environment.isWindows) { - // Since the event loop is recursively called, we need to check if it's safe to recurse. - if (!bun.StackCheck.init().isSafeToRecurse()) { - globalThis.throwStackOverflow(); - return error.JSError; + if (!override_env and env_array.items.len == 0) { + env_array.items = jsc_vm.transpiler.env.map.createNullDelimitedEnvMap(allocator) catch |err| return globalThis.throwError(err, "in Bun.spawn") catch return .zero; + env_array.capacity = env_array.items.len; + } + + inline for (0..stdio.len) |fd_index| { + if (stdio[fd_index].canUseMemfd(is_sync)) { + stdio[fd_index].useMemfd(fd_index); + } + } + var should_close_memfd = Environment.isLinux; + + defer { + if (should_close_memfd) { + inline for (0..stdio.len) |fd_index| { + if (stdio[fd_index] == .memfd) { + _ = bun.sys.close(stdio[fd_index].memfd); + stdio[fd_index] = .ignore; } } } - - var arena = bun.ArenaAllocator.init(bun.default_allocator); - defer arena.deinit(); - const allocator = arena.allocator(); - - var override_env = false; - var env_array = std.ArrayListUnmanaged(?[*:0]const u8){}; - var jsc_vm = globalThis.bunVM(); - - var cwd = jsc_vm.transpiler.fs.top_level_dir; - - var stdio = [3]Stdio{ - .{ .ignore = {} }, - .{ .pipe = {} }, - .{ .inherit = {} }, - }; - - if (comptime is_sync) { - stdio[1] = .{ .pipe = {} }; - stdio[2] = .{ .pipe = {} }; - } - var lazy = false; - var on_exit_callback = JSValue.zero; - var on_disconnect_callback = JSValue.zero; - var PATH = jsc_vm.transpiler.env.get("PATH") orelse ""; - var argv = std.ArrayList(?[*:0]const u8).init(allocator); - var cmd_value = JSValue.zero; - var detached = false; - var args = args_; - var maybe_ipc_mode: if (is_sync) void else ?IPC.Mode = if (is_sync) {} else null; - var ipc_callback: JSValue = .zero; - var extra_fds = std.ArrayList(bun.spawn.SpawnOptions.Stdio).init(bun.default_allocator); - var argv0: ?[*:0]const u8 = null; - var ipc_channel: i32 = -1; - - var windows_hide: bool = false; - var windows_verbatim_arguments: bool = false; - var abort_signal: ?*JSC.WebCore.AbortSignal = null; - defer { - // Ensure we clean it up on error. - if (abort_signal) |signal| { - signal.unref(); - } - } - - { - if (args.isEmptyOrUndefinedOrNull()) { - return globalThis.throwInvalidArguments("cmd must be an array", .{}); - } - - const args_type = args.jsType(); - if (args_type.isArray()) { - cmd_value = args; - args = secondaryArgsValue orelse JSValue.zero; - } else if (!args.isObject()) { - return globalThis.throwInvalidArguments("cmd must be an array", .{}); - } else if (try args.getTruthy(globalThis, "cmd")) |cmd_value_| { - cmd_value = cmd_value_; - } else { - return globalThis.throwInvalidArguments("cmd must be an array", .{}); - } - - if (args.isObject()) { - if (try args.getTruthy(globalThis, "argv0")) |argv0_| { - const argv0_str = try argv0_.getZigString(globalThis); - if (argv0_str.len > 0) { - argv0 = try argv0_str.toOwnedSliceZ(allocator); - } - } - - // need to update `cwd` before searching for executable with `Which.which` - if (try args.getTruthy(globalThis, "cwd")) |cwd_| { - const cwd_str = try cwd_.getZigString(globalThis); - if (cwd_str.len > 0) { - cwd = try cwd_str.toOwnedSliceZ(allocator); - } - } - } - - if (args != .zero and args.isObject()) { - // This must run before the stdio parsing happens - if (!is_sync) { - if (try args.getTruthy(globalThis, "ipc")) |val| { - if (val.isCell() and val.isCallable(globalThis.vm())) { - maybe_ipc_mode = ipc_mode: { - if (try args.getTruthy(globalThis, "serialization")) |mode_val| { - if (mode_val.isString()) { - break :ipc_mode IPC.Mode.fromJS(globalThis, mode_val) orelse { - if (!globalThis.hasException()) { - return globalThis.throwInvalidArguments("serialization must be \"json\" or \"advanced\"", .{}); - } - return error.JSError; - }; - } else { - if (!globalThis.hasException()) { - return globalThis.throwInvalidArgumentType("spawn", "serialization", "string"); - } - return .zero; - } - } - break :ipc_mode .advanced; - }; - - ipc_callback = val.withAsyncContextIfNeeded(globalThis); - } - } - } - - if (try args.getTruthy(globalThis, "signal")) |signal_val| { - if (signal_val.as(JSC.WebCore.AbortSignal)) |signal| { - abort_signal = signal.ref(); - } else { - return globalThis.throwInvalidArgumentTypeValue("signal", "AbortSignal", signal_val); - } - } - - if (try args.getTruthy(globalThis, "onDisconnect")) |onDisconnect_| { - if (!onDisconnect_.isCell() or !onDisconnect_.isCallable(globalThis.vm())) { - return globalThis.throwInvalidArguments("onDisconnect must be a function or undefined", .{}); - } - - on_disconnect_callback = if (comptime is_sync) - onDisconnect_ - else - onDisconnect_.withAsyncContextIfNeeded(globalThis); - } - - if (try args.getTruthy(globalThis, "onExit")) |onExit_| { - if (!onExit_.isCell() or !onExit_.isCallable(globalThis.vm())) { - return globalThis.throwInvalidArguments("onExit must be a function or undefined", .{}); - } - - on_exit_callback = if (comptime is_sync) - onExit_ - else - onExit_.withAsyncContextIfNeeded(globalThis); - } - - if (try args.getTruthy(globalThis, "env")) |object| { - if (!object.isObject()) { - return globalThis.throwInvalidArguments("env must be an object", .{}); - } - - override_env = true; - // If the env object does not include a $PATH, it must disable path lookup for argv[0] - var NEW_PATH: []const u8 = ""; - var envp_managed = env_array.toManaged(allocator); - try appendEnvpFromJS(globalThis, object, &envp_managed, &NEW_PATH); - env_array = envp_managed.moveToUnmanaged(); - PATH = NEW_PATH; - } - - try getArgv(globalThis, cmd_value, PATH, cwd, &argv0, allocator, &argv); - - if (try args.get(globalThis, "stdio")) |stdio_val| { - if (!stdio_val.isEmptyOrUndefinedOrNull()) { - if (stdio_val.jsType().isArray()) { - var stdio_iter = stdio_val.arrayIterator(globalThis); - var i: u32 = 0; - while (stdio_iter.next()) |value| : (i += 1) { - try stdio[i].extract(globalThis, i, value); - if (i == 2) - break; - } - i += 1; - - while (stdio_iter.next()) |value| : (i += 1) { - var new_item: Stdio = undefined; - try new_item.extract(globalThis, i, value); - - const opt = switch (new_item.asSpawnOption(i)) { - .result => |opt| opt, - .err => |e| { - return e.throwJS(globalThis); - }, - }; - if (opt == .ipc) { - ipc_channel = @intCast(extra_fds.items.len); - } - try extra_fds.append(opt); - } - } else { - return globalThis.throwInvalidArguments("stdio must be an array", .{}); - } - } - } else { - if (try args.get(globalThis, "stdin")) |value| { - try stdio[0].extract(globalThis, 0, value); - } - - if (try args.get(globalThis, "stderr")) |value| { - try stdio[2].extract(globalThis, 2, value); - } - - if (try args.get(globalThis, "stdout")) |value| { - try stdio[1].extract(globalThis, 1, value); - } - } - - if (comptime !is_sync) { - if (try args.get(globalThis, "lazy")) |lazy_val| { - if (lazy_val.isBoolean()) { - lazy = lazy_val.toBoolean(); - } - } - } - - if (try args.get(globalThis, "detached")) |detached_val| { - if (detached_val.isBoolean()) { - detached = detached_val.toBoolean(); - } - } - - if (Environment.isWindows) { - if (try args.get(globalThis, "windowsHide")) |val| { - if (val.isBoolean()) { - windows_hide = val.asBoolean(); - } - } - - if (try args.get(globalThis, "windowsVerbatimArguments")) |val| { - if (val.isBoolean()) { - windows_verbatim_arguments = val.asBoolean(); - } - } - } - } else { - try getArgv(globalThis, cmd_value, PATH, cwd, &argv0, allocator, &argv); - } - } - - if (!override_env and env_array.items.len == 0) { - env_array.items = jsc_vm.transpiler.env.map.createNullDelimitedEnvMap(allocator) catch |err| return globalThis.throwError(err, "in Bun.spawn") catch return .zero; - env_array.capacity = env_array.items.len; - } - - inline for (0..stdio.len) |fd_index| { - if (stdio[fd_index].canUseMemfd(is_sync)) { - stdio[fd_index].useMemfd(fd_index); - } - } - var should_close_memfd = Environment.isLinux; - - defer { - if (should_close_memfd) { - inline for (0..stdio.len) |fd_index| { - if (stdio[fd_index] == .memfd) { - _ = bun.sys.close(stdio[fd_index].memfd); - stdio[fd_index] = .ignore; - } - } - } - } - //"NODE_CHANNEL_FD=" is 16 bytes long, 15 bytes for the number, and 1 byte for the null terminator should be enough/safe - var ipc_env_buf: [32]u8 = undefined; - if (!is_sync) if (maybe_ipc_mode) |ipc_mode| { - // IPC is currently implemented in a very limited way. - // - // Node lets you pass as many fds as you want, they all become be sockets; then, IPC is just a special - // runtime-owned version of "pipe" (in which pipe is a misleading name since they're bidirectional sockets). - // - // Bun currently only supports three fds: stdin, stdout, and stderr, which are all unidirectional - // - // And then one fd is assigned specifically and only for IPC. If the user dont specify it, we add one (default: 3). - // - // When Bun.spawn() is given an `.ipc` callback, it enables IPC as follows: - env_array.ensureUnusedCapacity(allocator, 3) catch |err| return globalThis.throwError(err, "in Bun.spawn") catch return .zero; - const ipc_fd: u32 = brk: { - if (ipc_channel == -1) { - // If the user didn't specify an IPC channel, we need to add one - ipc_channel = @intCast(extra_fds.items.len); - var ipc_extra_fd_default = Stdio{ .ipc = {} }; - const fd: u32 = @intCast(ipc_channel + 3); - switch (ipc_extra_fd_default.asSpawnOption(fd)) { - .result => |opt| { - try extra_fds.append(opt); - }, - .err => |e| { - return e.throwJS(globalThis); - }, - } - break :brk fd; - } else { - break :brk @intCast(ipc_channel + 3); - } - }; - - const pipe_env = std.fmt.bufPrintZ( - &ipc_env_buf, - "NODE_CHANNEL_FD={d}", - .{ipc_fd}, - ) catch { - return globalThis.throwOutOfMemory(); - }; - env_array.appendAssumeCapacity(pipe_env); - - env_array.appendAssumeCapacity(switch (ipc_mode) { - inline else => |t| "NODE_CHANNEL_SERIALIZATION_MODE=" ++ @tagName(t), - }); - }; - - try env_array.append(allocator, null); - try argv.append(null); - - if (comptime is_sync) { - for (&stdio, 0..) |*io, i| { - io.toSync(@truncate(i)); - } - } - - const spawn_options = bun.spawn.SpawnOptions{ - .cwd = cwd, - .detached = detached, - .stdin = switch (stdio[0].asSpawnOption(0)) { - .result => |opt| opt, - .err => |e| return e.throwJS(globalThis), - }, - .stdout = switch (stdio[1].asSpawnOption(1)) { - .result => |opt| opt, - .err => |e| return e.throwJS(globalThis), - }, - .stderr = switch (stdio[2].asSpawnOption(2)) { - .result => |opt| opt, - .err => |e| return e.throwJS(globalThis), - }, - .extra_fds = extra_fds.items, - .argv0 = argv0, - - .windows = if (Environment.isWindows) .{ - .hide_window = windows_hide, - .verbatim_arguments = windows_verbatim_arguments, - .loop = JSC.EventLoopHandle.init(jsc_vm), - }, - }; - - var spawned = switch (bun.spawn.spawnProcess( - &spawn_options, - @ptrCast(argv.items.ptr), - @ptrCast(env_array.items.ptr), - ) catch |err| { - spawn_options.deinit(); - return globalThis.throwError(err, ": failed to spawn process") catch return .zero; - }) { - .err => |err| { - spawn_options.deinit(); - switch (err.getErrno()) { - .ACCES, .NOENT, .PERM, .ISDIR, .NOTDIR => { - const display_path: [:0]const u8 = if (argv0 != null) - std.mem.sliceTo(argv0.?, 0) - else if (argv.items.len > 0 and argv.items[0] != null) - std.mem.sliceTo(argv.items[0].?, 0) - else - ""; - if (display_path.len > 0) - return globalThis.throwValue(err.withPath(display_path).toJSC(globalThis)); + } + //"NODE_CHANNEL_FD=" is 16 bytes long, 15 bytes for the number, and 1 byte for the null terminator should be enough/safe + var ipc_env_buf: [32]u8 = undefined; + if (!is_sync) if (maybe_ipc_mode) |ipc_mode| { + // IPC is currently implemented in a very limited way. + // + // Node lets you pass as many fds as you want, they all become be sockets; then, IPC is just a special + // runtime-owned version of "pipe" (in which pipe is a misleading name since they're bidirectional sockets). + // + // Bun currently only supports three fds: stdin, stdout, and stderr, which are all unidirectional + // + // And then one fd is assigned specifically and only for IPC. If the user dont specify it, we add one (default: 3). + // + // When Bun.spawn() is given an `.ipc` callback, it enables IPC as follows: + env_array.ensureUnusedCapacity(allocator, 3) catch |err| return globalThis.throwError(err, "in Bun.spawn") catch return .zero; + const ipc_fd: u32 = brk: { + if (ipc_channel == -1) { + // If the user didn't specify an IPC channel, we need to add one + ipc_channel = @intCast(extra_fds.items.len); + var ipc_extra_fd_default = Stdio{ .ipc = {} }; + const fd: u32 = @intCast(ipc_channel + 3); + switch (ipc_extra_fd_default.asSpawnOption(fd)) { + .result => |opt| { + try extra_fds.append(opt); + }, + .err => |e| { + return e.throwJS(globalThis); }, - else => {}, - } - - return globalThis.throwValue(err.toJSC(globalThis)); - }, - .result => |result| result, - }; - - const loop = jsc_vm.eventLoop(); - - const process = spawned.toProcess(loop, is_sync); - - var subprocess = Subprocess.new(.{ - .globalThis = globalThis, - .process = process, - .pid_rusage = null, - .stdin = .{ .ignore = {} }, - .stdout = .{ .ignore = {} }, - .stderr = .{ .ignore = {} }, - .stdio_pipes = .{}, - .on_exit_callback = .empty, - .on_disconnect_callback = .empty, - .ipc_data = null, - .ipc_callback = .empty, - .flags = .{ - .is_sync = is_sync, - }, - }); - - const posix_ipc_fd = if (Environment.isPosix and !is_sync and maybe_ipc_mode != null) - spawned.extra_pipes.items[@intCast(ipc_channel)] - else - bun.invalid_fd; - - // When run synchronously, subprocess isn't garbage collected - subprocess.* = Subprocess{ - .globalThis = globalThis, - .process = process, - .pid_rusage = null, - .stdin = Writable.init( - stdio[0], - loop, - subprocess, - spawned.stdin, - ) catch { - subprocess.deref(); - return globalThis.throwOutOfMemory(); - }, - .stdout = Readable.init( - stdio[1], - loop, - subprocess, - spawned.stdout, - jsc_vm.allocator, - default_max_buffer_size, - is_sync, - ), - .stderr = Readable.init( - stdio[2], - loop, - subprocess, - spawned.stderr, - jsc_vm.allocator, - default_max_buffer_size, - is_sync, - ), - // 1. JavaScript. - // 2. Process. - .ref_count = 2, - .stdio_pipes = spawned.extra_pipes.moveToUnmanaged(), - .on_exit_callback = JSC.Strong.create(on_exit_callback, globalThis), - .on_disconnect_callback = JSC.Strong.create(on_disconnect_callback, globalThis), - .ipc_data = if (!is_sync and comptime Environment.isWindows) - if (maybe_ipc_mode) |ipc_mode| .{ - .mode = ipc_mode, - } else null - else - null, - .ipc_callback = JSC.Strong.create(ipc_callback, globalThis), - .flags = .{ - .is_sync = is_sync, - }, - }; - - subprocess.process.setExitHandler(subprocess); - - var posix_ipc_info: if (Environment.isPosix) IPC.Socket else void = undefined; - if (Environment.isPosix and !is_sync) { - if (maybe_ipc_mode) |mode| { - if (uws.us_socket_from_fd( - jsc_vm.rareData().spawnIPCContext(jsc_vm), - @sizeOf(*Subprocess), - posix_ipc_fd.cast(), - )) |socket| { - posix_ipc_info = IPC.Socket.from(socket); - subprocess.ipc_data = .{ - .socket = posix_ipc_info, - .mode = mode, - }; - } - } - } - - if (subprocess.ipc_data) |*ipc_data| { - if (Environment.isPosix) { - if (posix_ipc_info.ext(*Subprocess)) |ctx| { - ctx.* = subprocess; - subprocess.ref(); // + one ref for the IPC } + break :brk fd; } else { - subprocess.ref(); // + one ref for the IPC - - if (ipc_data.configureServer( - Subprocess, - subprocess, - subprocess.stdio_pipes.items[@intCast(ipc_channel)].buffer, - ).asErr()) |err| { - subprocess.deref(); - return globalThis.throwValue(err.toJSC(globalThis)); - } - subprocess.stdio_pipes.items[@intCast(ipc_channel)] = .unavailable; + break :brk @intCast(ipc_channel + 3); } - ipc_data.writeVersionPacket(); - } + }; - if (subprocess.stdin == .pipe) { - subprocess.stdin.pipe.signal = JSC.WebCore.Signal.init(&subprocess.stdin); - } + const pipe_env = std.fmt.bufPrintZ( + &ipc_env_buf, + "NODE_CHANNEL_FD={d}", + .{ipc_fd}, + ) catch { + return globalThis.throwOutOfMemory(); + }; + env_array.appendAssumeCapacity(pipe_env); - const out = if (comptime !is_sync) - subprocess.toJS(globalThis) + env_array.appendAssumeCapacity(switch (ipc_mode) { + inline else => |t| "NODE_CHANNEL_SERIALIZATION_MODE=" ++ @tagName(t), + }); + }; + + try env_array.append(allocator, null); + try argv.append(null); + + if (comptime is_sync) { + for (&stdio, 0..) |*io, i| { + io.toSync(@truncate(i)); + } + } + + const spawn_options = bun.spawn.SpawnOptions{ + .cwd = cwd, + .detached = detached, + .stdin = switch (stdio[0].asSpawnOption(0)) { + .result => |opt| opt, + .err => |e| return e.throwJS(globalThis), + }, + .stdout = switch (stdio[1].asSpawnOption(1)) { + .result => |opt| opt, + .err => |e| return e.throwJS(globalThis), + }, + .stderr = switch (stdio[2].asSpawnOption(2)) { + .result => |opt| opt, + .err => |e| return e.throwJS(globalThis), + }, + .extra_fds = extra_fds.items, + .argv0 = argv0, + + .windows = if (Environment.isWindows) .{ + .hide_window = windows_hide, + .verbatim_arguments = windows_verbatim_arguments, + .loop = JSC.EventLoopHandle.init(jsc_vm), + }, + }; + + var spawned = switch (bun.spawn.spawnProcess( + &spawn_options, + @ptrCast(argv.items.ptr), + @ptrCast(env_array.items.ptr), + ) catch |err| { + spawn_options.deinit(); + return globalThis.throwError(err, ": failed to spawn process") catch return .zero; + }) { + .err => |err| { + spawn_options.deinit(); + switch (err.getErrno()) { + .ACCES, .NOENT, .PERM, .ISDIR, .NOTDIR => { + const display_path: [:0]const u8 = if (argv0 != null) + std.mem.sliceTo(argv0.?, 0) + else if (argv.items.len > 0 and argv.items[0] != null) + std.mem.sliceTo(argv.items[0].?, 0) + else + ""; + if (display_path.len > 0) + return globalThis.throwValue(err.withPath(display_path).toJSC(globalThis)); + }, + else => {}, + } + + return globalThis.throwValue(err.toJSC(globalThis)); + }, + .result => |result| result, + }; + + const loop = jsc_vm.eventLoop(); + + const process = spawned.toProcess(loop, is_sync); + + var subprocess = Subprocess.new(.{ + .globalThis = globalThis, + .process = process, + .pid_rusage = null, + .stdin = .{ .ignore = {} }, + .stdout = .{ .ignore = {} }, + .stderr = .{ .ignore = {} }, + .stdio_pipes = .{}, + .on_exit_callback = .empty, + .on_disconnect_callback = .empty, + .ipc_data = null, + .ipc_callback = .empty, + .flags = .{ + .is_sync = is_sync, + }, + }); + + const posix_ipc_fd = if (Environment.isPosix and !is_sync and maybe_ipc_mode != null) + spawned.extra_pipes.items[@intCast(ipc_channel)] + else + bun.invalid_fd; + + // When run synchronously, subprocess isn't garbage collected + subprocess.* = Subprocess{ + .globalThis = globalThis, + .process = process, + .pid_rusage = null, + .stdin = Writable.init( + stdio[0], + loop, + subprocess, + spawned.stdin, + ) catch { + subprocess.deref(); + return globalThis.throwOutOfMemory(); + }, + .stdout = Readable.init( + stdio[1], + loop, + subprocess, + spawned.stdout, + jsc_vm.allocator, + default_max_buffer_size, + is_sync, + ), + .stderr = Readable.init( + stdio[2], + loop, + subprocess, + spawned.stderr, + jsc_vm.allocator, + default_max_buffer_size, + is_sync, + ), + // 1. JavaScript. + // 2. Process. + .ref_count = 2, + .stdio_pipes = spawned.extra_pipes.moveToUnmanaged(), + .on_exit_callback = JSC.Strong.create(on_exit_callback, globalThis), + .on_disconnect_callback = JSC.Strong.create(on_disconnect_callback, globalThis), + .ipc_data = if (!is_sync and comptime Environment.isWindows) + if (maybe_ipc_mode) |ipc_mode| .{ + .mode = ipc_mode, + } else null else - JSValue.zero; - subprocess.this_jsvalue = out; + null, + .ipc_callback = JSC.Strong.create(ipc_callback, globalThis), + .flags = .{ + .is_sync = is_sync, + }, + }; - var send_exit_notification = false; + subprocess.process.setExitHandler(subprocess); - if (comptime !is_sync) { - switch (subprocess.process.watch()) { - .result => {}, - .err => { - send_exit_notification = true; - lazy = false; - }, + var posix_ipc_info: if (Environment.isPosix) IPC.Socket else void = undefined; + if (Environment.isPosix and !is_sync) { + if (maybe_ipc_mode) |mode| { + if (uws.us_socket_from_fd( + jsc_vm.rareData().spawnIPCContext(jsc_vm), + @sizeOf(*Subprocess), + posix_ipc_fd.cast(), + )) |socket| { + posix_ipc_info = IPC.Socket.from(socket); + subprocess.ipc_data = .{ + .socket = posix_ipc_info, + .mode = mode, + }; } } + } - defer { - if (send_exit_notification) { - if (subprocess.process.hasExited()) { - // process has already exited, we called wait4(), but we did not call onProcessExit() - subprocess.process.onExit(subprocess.process.status, &std.mem.zeroes(Rusage)); - } else { - // process has already exited, but we haven't called wait4() yet - // https://cs.github.com/libuv/libuv/blob/b00d1bd225b602570baee82a6152eaa823a84fa6/src/unix/process.c#L1007 - subprocess.process.wait(is_sync); - } + if (subprocess.ipc_data) |*ipc_data| { + if (Environment.isPosix) { + if (posix_ipc_info.ext(*Subprocess)) |ctx| { + ctx.* = subprocess; + subprocess.ref(); // + one ref for the IPC + } + } else { + subprocess.ref(); // + one ref for the IPC + + if (ipc_data.configureServer( + Subprocess, + subprocess, + subprocess.stdio_pipes.items[@intCast(ipc_channel)].buffer, + ).asErr()) |err| { + subprocess.deref(); + return globalThis.throwValue(err.toJSC(globalThis)); + } + subprocess.stdio_pipes.items[@intCast(ipc_channel)] = .unavailable; + } + ipc_data.writeVersionPacket(); + } + + if (subprocess.stdin == .pipe) { + subprocess.stdin.pipe.signal = JSC.WebCore.Signal.init(&subprocess.stdin); + } + + const out = if (comptime !is_sync) + subprocess.toJS(globalThis) + else + JSValue.zero; + subprocess.this_jsvalue = out; + + var send_exit_notification = false; + + if (comptime !is_sync) { + switch (subprocess.process.watch()) { + .result => {}, + .err => { + send_exit_notification = true; + lazy = false; + }, + } + } + + defer { + if (send_exit_notification) { + if (subprocess.process.hasExited()) { + // process has already exited, we called wait4(), but we did not call onProcessExit() + subprocess.process.onExit(subprocess.process.status, &std.mem.zeroes(Rusage)); + } else { + // process has already exited, but we haven't called wait4() yet + // https://cs.github.com/libuv/libuv/blob/b00d1bd225b602570baee82a6152eaa823a84fa6/src/unix/process.c#L1007 + subprocess.process.wait(is_sync); } } + } - if (subprocess.stdin == .buffer) { - subprocess.stdin.buffer.start().assert(); + if (subprocess.stdin == .buffer) { + subprocess.stdin.buffer.start().assert(); + } + + if (subprocess.stdout == .pipe) { + subprocess.stdout.pipe.start(subprocess, loop).assert(); + if ((is_sync or !lazy) and subprocess.stdout == .pipe) { + subprocess.stdout.pipe.readAll(); } + } - if (subprocess.stdout == .pipe) { - subprocess.stdout.pipe.start(subprocess, loop).assert(); - if ((is_sync or !lazy) and subprocess.stdout == .pipe) { - subprocess.stdout.pipe.readAll(); - } + if (subprocess.stderr == .pipe) { + subprocess.stderr.pipe.start(subprocess, loop).assert(); + + if ((is_sync or !lazy) and subprocess.stderr == .pipe) { + subprocess.stderr.pipe.readAll(); } + } - if (subprocess.stderr == .pipe) { - subprocess.stderr.pipe.start(subprocess, loop).assert(); + should_close_memfd = false; - if ((is_sync or !lazy) and subprocess.stderr == .pipe) { - subprocess.stderr.pipe.readAll(); - } + if (comptime !is_sync) { + // Once everything is set up, we can add the abort listener + // Adding the abort listener may call the onAbortSignal callback immediately if it was already aborted + // Therefore, we must do this at the very end. + if (abort_signal) |signal| { + signal.pendingActivityRef(); + subprocess.abort_signal = signal.addListener(subprocess, onAbortSignal); + abort_signal = null; } - - should_close_memfd = false; - - if (comptime !is_sync) { - // Once everything is set up, we can add the abort listener - // Adding the abort listener may call the onAbortSignal callback immediately if it was already aborted - // Therefore, we must do this at the very end. - if (abort_signal) |signal| { - signal.pendingActivityRef(); - subprocess.abort_signal = signal.addListener(subprocess, onAbortSignal); - abort_signal = null; - } - if (!subprocess.process.hasExited()) { - jsc_vm.onSubprocessSpawn(subprocess.process); - } - return out; - } - - if (comptime is_sync) { - switch (subprocess.process.watchOrReap()) { - .result => { - // Once everything is set up, we can add the abort listener - // Adding the abort listener may call the onAbortSignal callback immediately if it was already aborted - // Therefore, we must do this at the very end. - if (abort_signal) |signal| { - signal.pendingActivityRef(); - subprocess.abort_signal = signal.addListener(subprocess, onAbortSignal); - abort_signal = null; - } - }, - .err => { - subprocess.process.wait(true); - }, - } - } - if (!subprocess.process.hasExited()) { jsc_vm.onSubprocessSpawn(subprocess.process); } + return out; + } - // We cannot release heap access while JS is running - { - const old_vm = jsc_vm.uwsLoop().internal_loop_data.jsc_vm; - jsc_vm.uwsLoop().internal_loop_data.jsc_vm = null; - defer { - jsc_vm.uwsLoop().internal_loop_data.jsc_vm = old_vm; + if (comptime is_sync) { + switch (subprocess.process.watchOrReap()) { + .result => { + // Once everything is set up, we can add the abort listener + // Adding the abort listener may call the onAbortSignal callback immediately if it was already aborted + // Therefore, we must do this at the very end. + if (abort_signal) |signal| { + signal.pendingActivityRef(); + subprocess.abort_signal = signal.addListener(subprocess, onAbortSignal); + abort_signal = null; + } + }, + .err => { + subprocess.process.wait(true); + }, + } + } + + if (!subprocess.process.hasExited()) { + jsc_vm.onSubprocessSpawn(subprocess.process); + } + + // We cannot release heap access while JS is running + { + const old_vm = jsc_vm.uwsLoop().internal_loop_data.jsc_vm; + jsc_vm.uwsLoop().internal_loop_data.jsc_vm = null; + defer { + jsc_vm.uwsLoop().internal_loop_data.jsc_vm = old_vm; + } + while (subprocess.hasPendingActivityNonThreadsafe()) { + if (subprocess.stdin == .buffer) { + subprocess.stdin.buffer.watch(); } - while (subprocess.hasPendingActivityNonThreadsafe()) { - if (subprocess.stdin == .buffer) { - subprocess.stdin.buffer.watch(); - } - if (subprocess.stderr == .pipe) { - subprocess.stderr.pipe.watch(); - } - - if (subprocess.stdout == .pipe) { - subprocess.stdout.pipe.watch(); - } - - jsc_vm.tick(); - jsc_vm.eventLoop().autoTick(); + if (subprocess.stderr == .pipe) { + subprocess.stderr.pipe.watch(); } - } - subprocess.updateHasPendingActivity(); + if (subprocess.stdout == .pipe) { + subprocess.stdout.pipe.watch(); + } - const signalCode = subprocess.getSignalCode(globalThis); - const exitCode = subprocess.getExitCode(globalThis); - const stdout = try subprocess.stdout.toBufferedValue(globalThis); - const stderr = try subprocess.stderr.toBufferedValue(globalThis); - const resource_usage: JSValue = if (!globalThis.hasException()) subprocess.createResourceUsageObject(globalThis) else .zero; - subprocess.finalize(); - - if (globalThis.hasException()) { - // e.g. a termination exception. - return .zero; - } - - const sync_value = JSC.JSValue.createEmptyObject(globalThis, 5 + @as(usize, @intFromBool(!signalCode.isEmptyOrUndefinedOrNull()))); - sync_value.put(globalThis, JSC.ZigString.static("exitCode"), exitCode); - if (!signalCode.isEmptyOrUndefinedOrNull()) { - sync_value.put(globalThis, JSC.ZigString.static("signalCode"), signalCode); - } - sync_value.put(globalThis, JSC.ZigString.static("stdout"), stdout); - sync_value.put(globalThis, JSC.ZigString.static("stderr"), stderr); - sync_value.put(globalThis, JSC.ZigString.static("success"), JSValue.jsBoolean(exitCode.isInt32() and exitCode.asInt32() == 0)); - sync_value.put(globalThis, JSC.ZigString.static("resourceUsage"), resource_usage); - - return sync_value; - } - - fn throwCommandNotFound(globalThis: *JSC.JSGlobalObject, command: []const u8) bun.JSError { - const err = JSC.SystemError{ - .message = bun.String.createFormat("Executable not found in $PATH: \"{s}\"", .{command}) catch bun.outOfMemory(), - .code = bun.String.static("ENOENT"), - .path = bun.String.createUTF8(command), - }; - return globalThis.throwValue(err.toErrorInstance(globalThis)); - } - - const node_cluster_binding = @import("./../../node/node_cluster_binding.zig"); - - pub fn handleIPCMessage( - this: *Subprocess, - message: IPC.DecodedIPCMessage, - ) void { - IPClog("Subprocess#handleIPCMessage", .{}); - switch (message) { - // In future versions we can read this in order to detect version mismatches, - // or disable future optimizations if the subprocess is old. - .version => |v| { - IPC.log("Child IPC version is {d}", .{v}); - }, - .data => |data| { - IPC.log("Received IPC message from child", .{}); - if (this.ipc_callback.get()) |cb| { - this.globalThis.bunVM().eventLoop().runCallback( - cb, - this.globalThis, - this.this_jsvalue, - &[_]JSValue{ data, this.this_jsvalue }, - ); - } - }, - .internal => |data| { - IPC.log("Received IPC internal message from child", .{}); - node_cluster_binding.handleInternalMessagePrimary(this.globalThis, this, data) catch {}; - }, + jsc_vm.tick(); + jsc_vm.eventLoop().autoTick(); } } - pub fn handleIPCClose(this: *Subprocess) void { - IPClog("Subprocess#handleIPCClose", .{}); - this.updateHasPendingActivity(); - defer this.deref(); - var ok = false; - if (this.ipc()) |ipc_data| { - ok = true; - ipc_data.internal_msg_queue.deinit(); - } - this.ipc_data = null; + subprocess.updateHasPendingActivity(); - const this_jsvalue = this.this_jsvalue; - this_jsvalue.ensureStillAlive(); - if (this.on_disconnect_callback.trySwap()) |callback| { - this.globalThis.bunVM().eventLoop().runCallback(callback, this.globalThis, this_jsvalue, &.{JSValue.jsBoolean(ok)}); - } + const signalCode = subprocess.getSignalCode(globalThis); + const exitCode = subprocess.getExitCode(globalThis); + const stdout = try subprocess.stdout.toBufferedValue(globalThis); + const stderr = try subprocess.stderr.toBufferedValue(globalThis); + const resource_usage: JSValue = if (!globalThis.hasException()) subprocess.createResourceUsageObject(globalThis) else .zero; + subprocess.finalize(); + + if (globalThis.hasException()) { + // e.g. a termination exception. + return .zero; } - pub fn ipc(this: *Subprocess) ?*IPC.IPCData { - return &(this.ipc_data orelse return null); + const sync_value = JSC.JSValue.createEmptyObject(globalThis, 5 + @as(usize, @intFromBool(!signalCode.isEmptyOrUndefinedOrNull()))); + sync_value.put(globalThis, JSC.ZigString.static("exitCode"), exitCode); + if (!signalCode.isEmptyOrUndefinedOrNull()) { + sync_value.put(globalThis, JSC.ZigString.static("signalCode"), signalCode); } + sync_value.put(globalThis, JSC.ZigString.static("stdout"), stdout); + sync_value.put(globalThis, JSC.ZigString.static("stderr"), stderr); + sync_value.put(globalThis, JSC.ZigString.static("success"), JSValue.jsBoolean(exitCode.isInt32() and exitCode.asInt32() == 0)); + sync_value.put(globalThis, JSC.ZigString.static("resourceUsage"), resource_usage); - pub const IPCHandler = IPC.NewIPCHandler(Subprocess); -}; + return sync_value; +} + +fn throwCommandNotFound(globalThis: *JSC.JSGlobalObject, command: []const u8) bun.JSError { + const err = JSC.SystemError{ + .message = bun.String.createFormat("Executable not found in $PATH: \"{s}\"", .{command}) catch bun.outOfMemory(), + .code = bun.String.static("ENOENT"), + .path = bun.String.createUTF8(command), + }; + return globalThis.throwValue(err.toErrorInstance(globalThis)); +} + +const node_cluster_binding = @import("./../../node/node_cluster_binding.zig"); + +pub fn handleIPCMessage( + this: *Subprocess, + message: IPC.DecodedIPCMessage, +) void { + IPClog("Subprocess#handleIPCMessage", .{}); + switch (message) { + // In future versions we can read this in order to detect version mismatches, + // or disable future optimizations if the subprocess is old. + .version => |v| { + IPC.log("Child IPC version is {d}", .{v}); + }, + .data => |data| { + IPC.log("Received IPC message from child", .{}); + if (this.ipc_callback.get()) |cb| { + this.globalThis.bunVM().eventLoop().runCallback( + cb, + this.globalThis, + this.this_jsvalue, + &[_]JSValue{ data, this.this_jsvalue }, + ); + } + }, + .internal => |data| { + IPC.log("Received IPC internal message from child", .{}); + node_cluster_binding.handleInternalMessagePrimary(this.globalThis, this, data) catch {}; + }, + } +} + +pub fn handleIPCClose(this: *Subprocess) void { + IPClog("Subprocess#handleIPCClose", .{}); + this.updateHasPendingActivity(); + defer this.deref(); + var ok = false; + if (this.ipc()) |ipc_data| { + ok = true; + ipc_data.internal_msg_queue.deinit(); + } + this.ipc_data = null; + + const this_jsvalue = this.this_jsvalue; + this_jsvalue.ensureStillAlive(); + if (this.on_disconnect_callback.trySwap()) |callback| { + this.globalThis.bunVM().eventLoop().runCallback(callback, this.globalThis, this_jsvalue, &.{JSValue.jsBoolean(ok)}); + } +} + +pub fn ipc(this: *Subprocess) ?*IPC.IPCData { + return &(this.ipc_data orelse return null); +} + +pub const IPCHandler = IPC.NewIPCHandler(Subprocess); + +const default_allocator = bun.default_allocator; +const bun = @import("root").bun; +const Environment = bun.Environment; + +const Global = bun.Global; +const strings = bun.strings; +const string = bun.string; +const Output = bun.Output; +const MutableString = bun.MutableString; +const std = @import("std"); +const Allocator = std.mem.Allocator; +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const Which = @import("../../../which.zig"); +const Async = bun.Async; +const IPC = @import("../../ipc.zig"); +const uws = bun.uws; +const windows = bun.windows; +const uv = windows.libuv; +const LifecycleScriptSubprocess = bun.install.LifecycleScriptSubprocess; +const Body = JSC.WebCore.Body; +const IPClog = Output.scoped(.IPC, false); + +const PosixSpawn = bun.posix.spawn; +const Rusage = bun.posix.spawn.Rusage; +const Process = bun.posix.spawn.Process; +const WaiterThread = bun.posix.spawn.WaiterThread; +const Stdio = bun.spawn.Stdio; +const StdioResult = if (Environment.isWindows) bun.spawn.WindowsSpawnResult.StdioResult else ?bun.FileDescriptor; diff --git a/src/bun.js/api/bun/udp_socket.zig b/src/bun.js/api/bun/udp_socket.zig index 42c7b4447c..655106e171 100644 --- a/src/bun.js/api/bun/udp_socket.zig +++ b/src/bun.js/api/bun/udp_socket.zig @@ -96,12 +96,8 @@ fn onData(socket: *uws.udp.Socket, buf: *uws.udp.PacketBuffer, packets: c_int) c const span = std.mem.span(hostname.?); var hostname_string = if (scope_id) |id| blk: { if (comptime !bun.Environment.isWindows) { - const net_if = @cImport({ - @cInclude("net/if.h"); - }); - - var buffer = std.mem.zeroes([net_if.IF_NAMESIZE:0]u8); - if (net_if.if_indextoname(id, &buffer) != null) { + var buffer = std.mem.zeroes([bun.c.IF_NAMESIZE:0]u8); + if (bun.c.if_indextoname(id, &buffer) != null) { break :blk bun.String.createFormat("{s}%{s}", .{ span, std.mem.span(@as([*:0]u8, &buffer)) }) catch bun.outOfMemory(); } } diff --git a/src/bun.js/api/bun/x509.zig b/src/bun.js/api/bun/x509.zig index fc2f09aac4..09f0801dab 100644 --- a/src/bun.js/api/bun/x509.zig +++ b/src/bun.js/api/bun/x509.zig @@ -1,4 +1,4 @@ -const BoringSSL = bun.BoringSSL; +const BoringSSL = bun.BoringSSL.c; const bun = @import("root").bun; const ZigString = JSC.ZigString; const std = @import("std"); diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 493e6b0621..9058630389 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -73,7 +73,7 @@ const uws = bun.uws; const Fallback = Runtime.Fallback; const MimeType = HTTP.MimeType; const Blob = JSC.WebCore.Blob; -const BoringSSL = bun.BoringSSL; +const BoringSSL = bun.BoringSSL.c; const Arena = @import("../../allocators/mimalloc_arena.zig").Arena; const SendfileContext = struct { fd: bun.FileDescriptor, @@ -8101,7 +8101,7 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp const globalThis = this.globalThis; var route_list_value = JSC.JSValue.zero; if (ssl_enabled) { - BoringSSL.load(); + bun.BoringSSL.load(); const ssl_config = this.config.ssl_config orelse @panic("Assertion failure: ssl_config"); const ssl_options = ssl_config.asUSockets(); diff --git a/src/bun.js/bindings/CPUFeatures.zig b/src/bun.js/bindings/CPUFeatures.zig index 76cb6b7d78..668d9144b5 100644 --- a/src/bun.js/bindings/CPUFeatures.zig +++ b/src/bun.js/bindings/CPUFeatures.zig @@ -1,89 +1,71 @@ -const bun = @import("root").bun; -const std = @import("std"); +const CPUFeatures = @This(); -fn Impl(comptime T: type) type { - return struct { - pub fn format(this: T, comptime _: []const u8, _: anytype, writer: anytype) !void { - var is_first = true; - inline for (comptime std.meta.fieldNames(T)) |fieldName| { - if (comptime bun.strings.eqlComptime(fieldName, "padding") or bun.strings.eqlComptime(fieldName, "none")) - continue; - - const value = @field(this, fieldName); - if (value) { - if (!is_first) - try writer.writeAll(" "); - is_first = false; - try writer.writeAll(fieldName); - } - } - } - - pub fn isEmpty(this: T) bool { - return @as(u8, @bitCast(this)) == 0; - } - - pub fn get() T { - const this: T = @bitCast(bun_cpu_features()); - - // sanity check - assert(this.none == false and this.padding == 0); - - if (bun.Environment.isX64) { - bun.analytics.Features.no_avx += @as(usize, @intFromBool(!this.avx)); - bun.analytics.Features.no_avx2 += @as(usize, @intFromBool(!this.avx2)); - } - - return this; - } - }; -} - -const X86CPUFeatures = packed struct(u8) { - none: bool = false, - - sse42: bool = false, - popcnt: bool = false, - avx: bool = false, - avx2: bool = false, - avx512: bool = false, - - padding: u2 = 0, - - pub usingnamespace Impl(@This()); -}; -const AArch64CPUFeatures = packed struct(u8) { - none: bool = false, - - neon: bool = false, - fp: bool = false, - aes: bool = false, - crc32: bool = false, - atomics: bool = false, - sve: bool = false, - - padding: u1 = 0, - - pub usingnamespace Impl(@This()); -}; - -pub const CPUFeatures = if (bun.Environment.isX64) - X86CPUFeatures -else if (bun.Environment.isAarch64) - AArch64CPUFeatures -else - struct { - pub fn get() @This() { - return .{}; - } - - pub fn format(_: @This(), comptime _: []const u8, _: anytype, _: anytype) !void {} - - pub fn isEmpty(_: @This()) bool { - return true; - } - }; +flags: Flags, extern "c" fn bun_cpu_features() u8; -const assert = bun.debugAssert; +pub const Flags = switch (@import("builtin").cpu.arch) { + .x86_64 => packed struct(u8) { + none: bool, + + sse42: bool, + popcnt: bool, + avx: bool, + avx2: bool, + avx512: bool, + + padding: u2 = 0, + }, + .aarch64 => packed struct(u8) { + none: bool, + + neon: bool, + fp: bool, + aes: bool, + crc32: bool, + atomics: bool, + sve: bool, + + padding: u1 = 0, + }, + else => unreachable, +}; + +pub fn format(features: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + var is_first = true; + inline for (@typeInfo(Flags).@"struct".fields) |field| brk: { + if (comptime (bun.strings.eql(field.name, "padding") or + bun.strings.eql(field.name, "none"))) + break :brk; + + if (@field(features.flags, field.name)) { + if (!is_first) + try writer.writeAll(" "); + is_first = false; + try writer.writeAll(field.name); + } + } +} + +pub fn isEmpty(features: CPUFeatures) bool { + return @as(u8, @bitCast(features.flags)) == 0; +} + +pub fn hasAnyAVX(features: CPUFeatures) bool { + return features.flags.avx or features.flags.avx2 or features.flags.avx512; +} + +pub fn get() CPUFeatures { + const flags: Flags = @bitCast(bun_cpu_features()); + bun.debugAssert(flags.none == false and flags.padding == 0); // sanity check + + if (bun.Environment.isX64) { + bun.analytics.Features.no_avx += @as(usize, @intFromBool(!flags.avx)); + bun.analytics.Features.no_avx2 += @as(usize, @intFromBool(!flags.avx2)); + } + + return .{ .flags = flags }; +} + +const std = @import("std"); +const bun = @import("root").bun; diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index 101f23aab4..8ca0331d5d 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -46,8 +46,8 @@ pub const Classes = struct { pub const SHA512 = JSC.API.Bun.Crypto.SHA512; pub const SHA512_256 = JSC.API.Bun.Crypto.SHA512_256; pub const ServerWebSocket = JSC.API.ServerWebSocket; - pub const Subprocess = JSC.Subprocess; - pub const ResourceUsage = JSC.ResourceUsage; + pub const Subprocess = JSC.API.Bun.Subprocess; + pub const ResourceUsage = JSC.API.Bun.Subprocess.ResourceUsage; pub const TCPSocket = JSC.API.TCPSocket; pub const TLSSocket = JSC.API.TLSSocket; pub const UDPSocket = JSC.API.UDPSocket; diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 4f76f98e7e..6bd91e070e 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -2123,7 +2123,7 @@ pub const ModuleLoader = struct { if (bun.Watcher.requires_file_descriptors) { switch (bun.sys.open( &(std.posix.toPosixPath(path.text) catch break :auto_watch), - bun.C.O_EVTONLY, + bun.c.O_EVTONLY, 0, )) { .err => break :auto_watch, diff --git a/src/bun.js/node/node_crypto_binding.zig b/src/bun.js/node/node_crypto_binding.zig index ebdf65a76d..368344a71d 100644 --- a/src/bun.js/node/node_crypto_binding.zig +++ b/src/bun.js/node/node_crypto_binding.zig @@ -6,7 +6,7 @@ const string = bun.string; const Output = bun.Output; const ZigString = JSC.ZigString; const Crypto = JSC.API.Bun.Crypto; -const BoringSSL = bun.BoringSSL; +const BoringSSL = bun.BoringSSL.c; const assert = bun.assert; const EVP = Crypto.EVP; const PBKDF2 = EVP.PBKDF2; diff --git a/src/bun.js/node/node_net_binding.zig b/src/bun.js/node/node_net_binding.zig index 3967d67503..23f1cb0be0 100644 --- a/src/bun.js/node/node_net_binding.zig +++ b/src/bun.js/node/node_net_binding.zig @@ -1,6 +1,6 @@ const std = @import("std"); const bun = @import("root").bun; -const C = bun.C.translated; +const C = bun.c; const Environment = bun.Environment; const JSC = bun.JSC; const string = bun.string; diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig index 22e4a9a06d..5cea7a38df 100644 --- a/src/bun.js/node/types.zig +++ b/src/bun.js/node/types.zig @@ -1327,7 +1327,7 @@ fn timeLikeFromNow() TimeLike { // timestamps are not modified, but other error conditions may still return .{ .sec = 0, - .nsec = if (Environment.isLinux) std.os.linux.UTIME.NOW else bun.C.translated.UTIME_NOW, + .nsec = if (Environment.isLinux) std.os.linux.UTIME.NOW else bun.c.UTIME_NOW, }; } diff --git a/src/bun.js/rare_data.zig b/src/bun.js/rare_data.zig index 3e3369c752..f69bfca2f5 100644 --- a/src/bun.js/rare_data.zig +++ b/src/bun.js/rare_data.zig @@ -6,7 +6,7 @@ const RareData = @This(); const Syscall = bun.sys; const JSC = bun.JSC; const std = @import("std"); -const BoringSSL = bun.BoringSSL; +const BoringSSL = bun.BoringSSL.c; const bun = @import("root").bun; const FDImpl = bun.FDImpl; const Environment = bun.Environment; @@ -429,8 +429,6 @@ pub export fn Bun__Process__getStdinFdType(vm: *JSC.VirtualMachine, fd: i32) Std } } -const Subprocess = @import("./api/bun/subprocess.zig").Subprocess; - pub fn spawnIPCContext(rare: *RareData, vm: *JSC.VirtualMachine) *uws.SocketContext { if (rare.spawn_ipc_usockets_context) |ctx| { return ctx; @@ -438,7 +436,7 @@ pub fn spawnIPCContext(rare: *RareData, vm: *JSC.VirtualMachine) *uws.SocketCont const opts: uws.us_socket_context_options_t = .{}; const ctx = uws.us_create_socket_context(0, vm.event_loop_handle.?, @sizeOf(usize), opts).?; - IPC.Socket.configure(ctx, true, *Subprocess, Subprocess.IPCHandler); + IPC.Socket.configure(ctx, true, *JSC.Subprocess, JSC.Subprocess.IPCHandler); rare.spawn_ipc_usockets_context = ctx; return ctx; } @@ -487,7 +485,7 @@ pub fn deinit(this: *RareData) void { this.s3_default_client.deinit(); if (this.boring_ssl_engine) |engine| { - _ = bun.BoringSSL.ENGINE_free(engine); + _ = bun.BoringSSL.c.ENGINE_free(engine); } this.cleanup_hooks.clearAndFree(bun.default_allocator); diff --git a/src/bun.js/webcore.zig b/src/bun.js/webcore.zig index 5f55258ecf..6c16aec506 100644 --- a/src/bun.js/webcore.zig +++ b/src/bun.js/webcore.zig @@ -361,7 +361,7 @@ pub const Prompt = struct { pub const Crypto = struct { garbage: i32 = 0, - const BoringSSL = bun.BoringSSL; + const BoringSSL = bun.BoringSSL.c; pub const doScryptSync = JSC.wrapInstanceMethod(Crypto, "scryptSync", false); @@ -560,7 +560,7 @@ pub const Crypto = struct { if (b.len != len) { return globalThis.throw("Input buffers must have the same byte length", .{}); } - return JSC.jsBoolean(len == 0 or bun.BoringSSL.CRYPTO_memcmp(a.ptr, b.ptr, len) == 0); + return JSC.jsBoolean(len == 0 or bun.BoringSSL.c.CRYPTO_memcmp(a.ptr, b.ptr, len) == 0); } pub fn timingSafeEqualWithoutTypeChecks( @@ -577,7 +577,7 @@ pub const Crypto = struct { return globalThis.throw("Input buffers must have the same byte length", .{}); } - return JSC.jsBoolean(len == 0 or bun.BoringSSL.CRYPTO_memcmp(a.ptr, b.ptr, len) == 0); + return JSC.jsBoolean(len == 0 or bun.BoringSSL.c.CRYPTO_memcmp(a.ptr, b.ptr, len) == 0); } pub fn getRandomValues( diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 8ed132a1cf..8aad6f609e 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -58,14 +58,12 @@ pub const Blob = struct { pub usingnamespace bun.New(@This()); pub usingnamespace JSC.Codegen.JSBlob; - // pub usingnamespace @import("./blob/ReadFile.zig"); const rf = @import("./blob/ReadFile.zig"); pub const ReadFile = rf.ReadFile; pub const ReadFileUV = rf.ReadFileUV; pub const ReadFileTask = rf.ReadFileTask; pub const ReadFileResultType = rf.ReadFileResultType; - // pub usingnamespace @import("./blob/WriteFile.zig"); const wf = @import("./blob/WriteFile.zig"); pub const WriteFile = wf.WriteFile; pub const WriteFileWindows = wf.WriteFileWindows; diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index afca0d5c0c..4bb11f991b 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -52,7 +52,7 @@ const Request = JSC.WebCore.Request; const Blob = JSC.WebCore.Blob; const Async = bun.Async; -const BoringSSL = bun.BoringSSL; +const BoringSSL = bun.BoringSSL.c; const X509 = @import("../api/bun/x509.zig"); const PosixToWinNormalizer = bun.path.PosixToWinNormalizer; const s3 = bun.S3; diff --git a/src/bun.zig b/src/bun.zig index 948d1997d8..e9d793ed4a 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -102,7 +102,20 @@ pub const JSOOM = OOM || JSError; pub const detectCI = @import("./ci_info.zig").detectCI; +/// Adding to this namespace is considered deprecated. +/// +/// If the declaration truly came from C, it should be perfectly possible to +/// translate the definition and put it in `c-headers-for-zig.h`, and available +/// via the lowercase `c` namespace. Wrappers around functions should go in a +/// more specific namespace, such as `bun.spawn`, `bun.strings` or `bun.sys` +/// +/// By avoiding manual transcription of C headers into Zig, we avoid bugs due to +/// different definitions between platforms, as well as very common mistakes +/// that can be made when porting definitions. It also keeps code much cleaner. pub const C = @import("c.zig"); +/// Translated from `c-headers-for-zig.h` for the current platform. +pub const c = @import("translated-c-headers"); + pub const sha = @import("./sha.zig"); pub const FeatureFlags = @import("feature_flags.zig"); pub const meta = @import("./meta.zig"); @@ -607,7 +620,7 @@ pub fn hash32(content: []const u8) u32 { pub const HiveArray = @import("./hive_array.zig").HiveArray; pub fn rand(bytes: []u8) void { - _ = BoringSSL.RAND_bytes(bytes.ptr, bytes.len); + _ = BoringSSL.c.RAND_bytes(bytes.ptr, bytes.len); } pub const ObjectPool = @import("./pool.zig").ObjectPool; @@ -1586,7 +1599,14 @@ pub const ImportKind = @import("./import_record.zig").ImportKind; pub const Watcher = @import("./Watcher.zig"); -pub usingnamespace @import("./util.zig"); +pub fn concat(comptime T: type, dest: []T, src: []const []const T) void { + var remain = dest; + for (src) |group| { + bun.copy(T, remain[0..group.len], group); + remain = remain[group.len..]; + } +} + pub const fast_debug_build_cmd = .None; pub const fast_debug_build_mode = fast_debug_build_cmd != .None and Environment.isDebug; @@ -1601,7 +1621,6 @@ pub const sourcemap = @import("./sourcemap/sourcemap.zig"); /// Attempt to coerce some value into a byte slice. pub fn asByteSlice(buffer: anytype) []const u8 { return switch (@TypeOf(buffer)) { - []const u8, []u8, [:0]const u8, [:0]u8 => buffer.ptr[0..buffer.len], [*:0]u8, [*:0]const u8 => buffer[0..len(buffer)], [*c]const u8, [*c]u8 => span(buffer), @@ -1700,6 +1719,8 @@ pub noinline fn maybeHandlePanicDuringProcessReload() void { } } +extern "c" fn on_before_reload_process_linux() void; + /// Reload Bun's process. This clones envp, argv, and gets the current /// executable path. /// @@ -1787,13 +1808,13 @@ pub fn reloadProcess( attrs.resetSignals() catch {}; attrs.set( - C.POSIX_SPAWN_CLOEXEC_DEFAULT | + C.translated.POSIX_SPAWN_CLOEXEC_DEFAULT | // Apple Extension: If this bit is set, rather // than returning to the caller, posix_spawn(2) // and posix_spawnp(2) will behave as a more // featureful execve(2). - C.POSIX_SPAWN_SETEXEC | - C.POSIX_SPAWN_SETSIGDEF | C.POSIX_SPAWN_SETSIGMASK, + C.translated.POSIX_SPAWN_SETEXEC | + C.translated.POSIX_SPAWN_SETSIGDEF | C.translated.POSIX_SPAWN_SETSIGMASK, ) catch unreachable; switch (PosixSpawn.spawnZ(exec_path, actions, attrs, @as([*:null]?[*:0]const u8, @ptrCast(newargv)), @as([*:null]?[*:0]const u8, @ptrCast(envp)))) { .err => |err| { @@ -1812,10 +1833,6 @@ pub fn reloadProcess( }, } } else if (comptime Environment.isPosix) { - const on_before_reload_process_linux = struct { - pub extern "c" fn on_before_reload_process_linux() void; - }.on_before_reload_process_linux; - on_before_reload_process_linux(); const err = std.posix.execveZ( exec_path, @@ -2720,8 +2737,8 @@ pub inline fn pathLiteral(comptime literal: anytype) *const [literal.len:0]u8 { if (!Environment.isWindows) return @ptrCast(literal); return comptime { var buf: [literal.len:0]u8 = undefined; - for (literal, 0..) |c, i| { - buf[i] = if (c == '/') '\\' else c; + for (literal, 0..) |char, i| { + buf[i] = if (char == '/') '\\' else char; assert(buf[i] != 0 and buf[i] < 128); } buf[buf.len] = 0; @@ -2735,8 +2752,8 @@ pub inline fn OSPathLiteral(comptime literal: anytype) *const [literal.len:0]OSP if (!Environment.isWindows) return @ptrCast(literal); return comptime { var buf: [literal.len:0]OSPathChar = undefined; - for (literal, 0..) |c, i| { - buf[i] = if (c == '/') '\\' else c; + for (literal, 0..) |char, i| { + buf[i] = if (char == '/') '\\' else char; assert(buf[i] != 0 and buf[i] < 128); } buf[buf.len] = 0; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 46b509ffe1..dc7345cd05 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -106,7 +106,6 @@ const Dependency = js_ast.Dependency; const JSAst = js_ast.BundledAst; const Loader = options.Loader; pub const Index = @import("../ast/base.zig").Index; -const Batcher = bun.Batcher; const Symbol = js_ast.Symbol; const EventLoop = bun.JSC.AnyEventLoop; const MultiArrayList = bun.MultiArrayList; @@ -2264,7 +2263,7 @@ pub const BundleV2 = struct { const fd = if (bun.Watcher.requires_file_descriptors) switch (bun.sys.open( &(std.posix.toPosixPath(load.path) catch break :add_watchers), - bun.C.O_EVTONLY, + bun.c.O_EVTONLY, 0, )) { .result => |fd| fd, @@ -4599,7 +4598,7 @@ pub const ParseTask = struct { result: ?*OnBeforeParseResult = null, - const headers = bun.C.translated; + const headers = bun.c; comptime { bun.assert(@sizeOf(OnBeforeParseArguments) == @sizeOf(headers.OnBeforeParseArguments)); @@ -5908,15 +5907,9 @@ const LinkerGraph = struct { list.appendSliceAssumeCapacity(original_parts.slice()); list.appendAssumeCapacity(self.part_id); - entry.value_ptr.* = BabyList(u32).init(list.items); + entry.value_ptr.* = .init(list.items); } else { - entry.value_ptr.* = bun.from( - BabyList(u32), - self.graph.allocator, - &[_]u32{ - self.part_id, - }, - ) catch unreachable; + entry.value_ptr.* = BabyList(u32).fromSlice(self.graph.allocator, &.{self.part_id}) catch bun.outOfMemory(); } } else { entry.value_ptr.push(self.graph.allocator, self.part_id) catch unreachable; @@ -15533,17 +15526,13 @@ pub const LinkerContext = struct { } break :brk dependencies; } else &.{}; + var symbol_uses: Part.SymbolUseMap = .empty; + symbol_uses.put(c.allocator, wrapper_ref, .{ .count_estimate = 1 }) catch bun.outOfMemory(); const part_index = c.graph.addPartToFile( source_index, .{ .stmts = &.{}, - .symbol_uses = bun.from( - Part.SymbolUseMap, - c.allocator, - .{ - .{ wrapper_ref, Symbol.Use{ .count_estimate = 1 } }, - }, - ) catch unreachable, + .symbol_uses = symbol_uses, .declared_symbols = js_ast.DeclaredSymbol.List.fromSlice( c.allocator, &[_]js_ast.DeclaredSymbol{ @@ -15595,16 +15584,12 @@ pub const LinkerContext = struct { }; } + var symbol_uses: Part.SymbolUseMap = .empty; + symbol_uses.put(c.allocator, wrapper_ref, .{ .count_estimate = 1 }) catch bun.outOfMemory(); const part_index = c.graph.addPartToFile( source_index, .{ - .symbol_uses = bun.fromMapLike( - Part.SymbolUseMap, - c.allocator, - &.{ - .{ wrapper_ref, .{ .count_estimate = 1 } }, - }, - ) catch unreachable, + .symbol_uses = symbol_uses, .declared_symbols = js_ast.DeclaredSymbol.List.fromSlice(c.allocator, &[_]js_ast.DeclaredSymbol{ .{ .ref = wrapper_ref, .is_top_level = true }, }) catch unreachable, diff --git a/src/c-headers-for-zig.h b/src/c-headers-for-zig.h index 6292d6ffcb..512812aeff 100644 --- a/src/c-headers-for-zig.h +++ b/src/c-headers-for-zig.h @@ -1,8 +1,8 @@ // This file is run through translate-c and exposed to Zig code -// under the namespace bun.C.translated. Prefer adding includes +// under the namespace bun.c (lowercase c). Prefer adding includes // to this file instead of manually porting struct definitions // into Zig code. By using automatic translation, differences -// in platforms can be avoided. +// between platforms and subtle mistakes can be avoided. // // When Zig is translating this file, it will define these macros: // - WINDOWS @@ -10,15 +10,17 @@ // - LINUX // - POSIX +// For `POSIX_SPAWN_SETSID` and some other non-POSIX extensions in glibc +#if LINUX +#define _GNU_SOURCE +#endif + // OnBeforeParseResult, etc... #include "../packages/bun-native-bundler-plugin-api/bundler_plugin.h" #if POSIX -// passwd, getpwuid_r #include "pwd.h" -// geteuid #include -// AI_ADDRCONFIG #include #endif @@ -26,7 +28,17 @@ #include #include #include +#include +#include +#include +#include #elif LINUX #include #include +#include +#include +#include +#include +#include +#include #endif diff --git a/src/c.zig b/src/c.zig index 6a3c3036e8..fac44409c4 100644 --- a/src/c.zig +++ b/src/c.zig @@ -1,3 +1,13 @@ +//! Adding to this namespace is considered deprecated. +//! +//! If the declaration truly came from C, it should be perfectly possible to +//! translate the definition and put it in `c-headers-for-zig.h`, and available +//! via the lowercase `c` namespace. Wrappers around functions should go in a +//! more specific namespace, such as `bun.spawn`, `bun.strings` or `bun.sys` +//! +//! By avoiding manual transcription of C headers into Zig, we avoid bugs due to +//! different definitions between platforms, as well as very common mistakes +//! that can be made when porting definitions. It also keeps code much cleaner. const std = @import("std"); const bun = @import("root").bun; const Environment = @import("./env.zig"); diff --git a/src/crash_handler.zig b/src/crash_handler.zig index 0a20373464..519052dce4 100644 --- a/src/crash_handler.zig +++ b/src/crash_handler.zig @@ -72,7 +72,7 @@ var before_crash_handlers: std.ArrayListUnmanaged(struct { *anyopaque, *const On // TODO: I don't think it's safe to lock/unlock a mutex inside a signal handler. var before_crash_handlers_mutex: bun.Mutex = .{}; -const CPUFeatures = @import("./bun.js/bindings/CPUFeatures.zig").CPUFeatures; +const CPUFeatures = @import("./bun.js/bindings/CPUFeatures.zig"); /// This structure and formatter must be kept in sync with `bun.report`'s decoder implementation. pub const CrashReason = union(enum) { @@ -905,10 +905,8 @@ pub fn printMetadata(writer: anytype) !void { try writer.print("Windows v{s}\n", .{std.zig.system.windows.detectRuntimeVersion()}); } - if (comptime bun.Environment.isX64) { - if (!cpu_features.avx and !cpu_features.avx2 and !cpu_features.avx512) { - is_ancient_cpu = true; - } + if (bun.Environment.isX64) { + is_ancient_cpu = !cpu_features.hasAnyAVX(); } if (!cpu_features.isEmpty()) { diff --git a/src/darwin_c.zig b/src/darwin_c.zig index c042bca553..9a67df2bdf 100644 --- a/src/darwin_c.zig +++ b/src/darwin_c.zig @@ -9,7 +9,6 @@ const StatError = std.fs.File.StatError; const off_t = std.c.off_t; const errno = posix.errno; const zeroes = mem.zeroes; -const This = @This(); pub extern "c" fn copyfile(from: [*:0]const u8, to: [*:0]const u8, state: ?std.c.copyfile_state_t, flags: u32) c_int; pub const COPYFILE_STATE_SRC_FD = @as(c_int, 1); pub const COPYFILE_STATE_SRC_FILENAME = @as(c_int, 2); @@ -89,73 +88,6 @@ pub const stat = blk: { break :blk @extern(T, .{ .name = if (bun.Environment.isAarch64) "stat" else "stat64" }); }; -// pub fn stat_absolute(path: [:0]const u8) StatError!Stat { -// if (builtin.os.tag == .windows) { -// var io_status_block: windows.IO_STATUS_BLOCK = undefined; -// var info: windows.FILE_ALL_INFORMATION = undefined; -// const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation); -// switch (rc) { -// .SUCCESS => {}, -// .BUFFER_OVERFLOW => {}, -// .INVALID_PARAMETER => unreachable, -// .ACCESS_DENIED => return error.AccessDenied, -// else => return windows.unexpectedStatus(rc), -// } -// return Stat{ -// .inode = info.InternalInformation.IndexNumber, -// .size = @bitCast(u64, info.StandardInformation.EndOfFile), -// .mode = 0, -// .kind = if (info.StandardInformation.Directory == 0) .File else .Directory, -// .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime), -// .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime), -// .ctime = windows.fromSysTime(info.BasicInformation.CreationTime), -// }; -// } - -// var st = zeroes(libc_stat); -// switch (errno(stat(path.ptr, &st))) { -// 0 => {}, -// // .EINVAL => unreachable, -// .EBADF => unreachable, // Always a race condition. -// .ENOMEM => return error.SystemResources, -// .EACCES => return error.AccessDenied, -// else => |err| return os.unexpectedErrno(err), -// } - -// const atime = st.atime(); -// const mtime = st.mtime(); -// const ctime = st.ctime(); -// return Stat{ -// .inode = st.ino, -// .size = @bitCast(u64, st.size), -// .mode = st.mode, -// .kind = switch (builtin.os.tag) { -// .wasi => switch (st.filetype) { -// os.FILETYPE_BLOCK_DEVICE => Kind.BlockDevice, -// os.FILETYPE_CHARACTER_DEVICE => Kind.CharacterDevice, -// os.FILETYPE_DIRECTORY => Kind.Directory, -// os.FILETYPE_SYMBOLIC_LINK => Kind.SymLink, -// os.FILETYPE_REGULAR_FILE => Kind.File, -// os.FILETYPE_SOCKET_STREAM, os.FILETYPE_SOCKET_DGRAM => Kind.UnixDomainSocket, -// else => Kind.Unknown, -// }, -// else => switch (st.mode & os.S.IFMT) { -// os.S.IFBLK => Kind.BlockDevice, -// os.S.IFCHR => Kind.CharacterDevice, -// os.S.IFDIR => Kind.Directory, -// os.S.IFIFO => Kind.NamedPipe, -// os.S.IFLNK => Kind.SymLink, -// os.S.IFREG => Kind.File, -// os.S.IFSOCK => Kind.UnixDomainSocket, -// else => Kind.Unknown, -// }, -// }, -// .atime = @as(i128, atime.sec) * std.time.ns_per_s + atime.nsec, -// .mtime = @as(i128, mtime.sec) * std.time.ns_per_s + mtime.nsec, -// .ctime = @as(i128, ctime.sec) * std.time.ns_per_s + ctime.nsec, -// }; -// } - // benchmarking this did nothing on macOS // i verified it wasn't returning -1 pub fn preallocate_file(_: posix.fd_t, _: off_t, _: off_t) !void { @@ -491,7 +423,7 @@ pub fn getSystemUptime() u64 { } pub fn getSystemLoadavg() [3]f64 { - var loadavg: bun.C.translated.struct_loadavg = undefined; + var loadavg: bun.c.struct_loadavg = undefined; var size: usize = @sizeOf(@TypeOf(loadavg)); std.posix.sysctlbynameZ( @@ -695,14 +627,9 @@ pub const ifaddrs = extern struct { pub extern fn getifaddrs(*?*ifaddrs) c_int; pub extern fn freeifaddrs(?*ifaddrs) void; -const net_if_h = @cImport({ - // TODO: remove this c import! instead of adding to it, add to - // c-headers-for-zig.h and use bun.C.translated. - @cInclude("net/if.h"); -}); -pub const IFF_RUNNING = net_if_h.IFF_RUNNING; -pub const IFF_UP = net_if_h.IFF_UP; -pub const IFF_LOOPBACK = net_if_h.IFF_LOOPBACK; +pub const IFF_RUNNING = bun.c.IFF_RUNNING; +pub const IFF_UP = bun.c.IFF_UP; +pub const IFF_LOOPBACK = bun.c.IFF_LOOPBACK; pub const sockaddr_dl = extern struct { sdl_len: u8, // Total length of sockaddr */ sdl_family: u8, // AF_LINK */ @@ -719,17 +646,9 @@ pub const sockaddr_dl = extern struct { //#endif }; -pub usingnamespace @cImport({ - // TODO: remove this c import! instead of adding to it, add to - // c-headers-for-zig.h and use bun.C.translated. - @cInclude("sys/spawn.h"); - @cInclude("sys/fcntl.h"); - @cInclude("sys/socket.h"); -}); - pub const F = struct { - pub const DUPFD_CLOEXEC = This.F_DUPFD_CLOEXEC; - pub const DUPFD = This.F_DUPFD; + pub const DUPFD_CLOEXEC = bun.c.F_DUPFD_CLOEXEC; + pub const DUPFD = bun.c.F_DUPFD; }; // it turns out preallocating on APFS on an M1 is slower. diff --git a/src/deps/uws.zig b/src/deps/uws.zig index ee2baf1102..93fa2e3290 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -35,7 +35,8 @@ pub const CloseCode = enum(i32) { failure = 1, }; -const BoringSSL = bun.BoringSSL; +const BoringSSL = bun.BoringSSL.c; + fn NativeSocketHandleType(comptime ssl: bool) type { if (ssl) { return BoringSSL.SSL; diff --git a/src/dns.zig b/src/dns.zig index 1921e1428a..398be2d0ad 100644 --- a/src/dns.zig +++ b/src/dns.zig @@ -3,9 +3,9 @@ const std = @import("std"); const JSC = bun.JSC; const JSValue = JSC.JSValue; -pub const AI_V4MAPPED: c_int = if (bun.Environment.isWindows) 2048 else bun.C.translated.AI_V4MAPPED; -pub const AI_ADDRCONFIG: c_int = if (bun.Environment.isWindows) 1024 else bun.C.translated.AI_ADDRCONFIG; -pub const AI_ALL: c_int = if (bun.Environment.isWindows) 256 else bun.C.translated.AI_ALL; +pub const AI_V4MAPPED: c_int = if (bun.Environment.isWindows) 2048 else bun.c.AI_V4MAPPED; +pub const AI_ADDRCONFIG: c_int = if (bun.Environment.isWindows) 1024 else bun.c.AI_ADDRCONFIG; +pub const AI_ALL: c_int = if (bun.Environment.isWindows) 256 else bun.c.AI_ALL; pub const GetAddrInfo = struct { name: []const u8 = "", diff --git a/src/exact_size_matcher.zig b/src/exact_size_matcher.zig index f3a7d75c2c..607b5ce589 100644 --- a/src/exact_size_matcher.zig +++ b/src/exact_size_matcher.zig @@ -77,5 +77,4 @@ pub fn ExactSizeMatcher(comptime max_bytes: usize) type { }; } -const eight = ExactSizeMatcher(8); const expect = std.testing.expect; diff --git a/src/hmac.zig b/src/hmac.zig index f33a1357df..790cbfb9fc 100644 --- a/src/hmac.zig +++ b/src/hmac.zig @@ -1,7 +1,7 @@ const bun = @import("root").bun; const std = @import("std"); -const boring = bun.BoringSSL; +const boring = bun.BoringSSL.c; pub fn generate(key: []const u8, data: []const u8, algorithm: bun.JSC.API.Bun.Crypto.EVP.Algorithm, out: *[boring.EVP_MAX_MD_SIZE]u8) ?[]const u8 { var outlen: c_uint = boring.EVP_MAX_MD_SIZE; diff --git a/src/http.zig b/src/http.zig index bffc2b3796..e33b4dbaa2 100644 --- a/src/http.zig +++ b/src/http.zig @@ -29,7 +29,7 @@ const posix = std.posix; const SOCK = posix.SOCK; const Arena = @import("./allocators/mimalloc_arena.zig").Arena; const ZlibPool = @import("./http/zlib.zig"); -const BoringSSL = bun.BoringSSL; +const BoringSSL = bun.BoringSSL.c; const Progress = bun.Progress; const X509 = @import("./bun.js/api/bun/x509.zig"); const SSLConfig = @import("./bun.js/api/server.zig").ServerConfig.SSLConfig; @@ -1568,7 +1568,7 @@ pub fn checkServerIdentity( } } - if (BoringSSL.checkX509ServerIdentity(x509, hostname)) { + if (bun.BoringSSL.checkX509ServerIdentity(x509, hostname)) { return true; } } @@ -1621,7 +1621,7 @@ pub fn onOpen( } if (comptime is_ssl) { - var ssl_ptr: *BoringSSL.SSL = @as(*BoringSSL.SSL, @ptrCast(socket.getNativeHandle())); + var ssl_ptr: *BoringSSL.SSL = @ptrCast(socket.getNativeHandle()); if (!ssl_ptr.isInitFinished()) { var _hostname = client.hostname orelse client.url.hostname; if (client.http_proxy) |proxy| { diff --git a/src/http/websocket_http_client.zig b/src/http/websocket_http_client.zig index 66aa643d6b..9ae8b136c6 100644 --- a/src/http/websocket_http_client.zig +++ b/src/http/websocket_http_client.zig @@ -425,8 +425,8 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type { this.fail(ErrorCode.tls_handshake_failed); return; } - const ssl_ptr = @as(*BoringSSL.SSL, @ptrCast(socket.getNativeHandle())); - if (BoringSSL.SSL_get_servername(ssl_ptr, 0)) |servername| { + const ssl_ptr = @as(*BoringSSL.c.SSL, @ptrCast(socket.getNativeHandle())); + if (BoringSSL.c.SSL_get_servername(ssl_ptr, 0)) |servername| { const hostname = servername[0..bun.len(servername)]; if (!BoringSSL.checkServerIdentity(ssl_ptr, hostname)) { this.fail(ErrorCode.tls_handshake_failed); @@ -1113,8 +1113,8 @@ pub fn NewWebSocketClient(comptime ssl: bool) type { if (authorized) { if (reject_unauthorized) { - const ssl_ptr = @as(*BoringSSL.SSL, @ptrCast(socket.getNativeHandle())); - if (BoringSSL.SSL_get_servername(ssl_ptr, 0)) |servername| { + const ssl_ptr = @as(*BoringSSL.c.SSL, @ptrCast(socket.getNativeHandle())); + if (BoringSSL.c.SSL_get_servername(ssl_ptr, 0)) |servername| { const hostname = servername[0..bun.len(servername)]; if (!BoringSSL.checkServerIdentity(ssl_ptr, hostname)) { this.outgoing_websocket = null; diff --git a/src/js_ast.zig b/src/js_ast.zig index 904da1df73..969e5b5fc5 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -2845,7 +2845,7 @@ pub const Stmt = struct { loc: logger.Loc, data: Data, - pub const Batcher = bun.Batcher(Stmt); + pub const Batcher = NewBatcher(Stmt); pub fn assign(a: Expr, b: Expr) Stmt { return Stmt.alloc( @@ -8940,3 +8940,41 @@ const ToJSError = error{ }; const writeAnyToHasher = bun.writeAnyToHasher; + +/// Say you need to allocate a bunch of tiny arrays +/// You could just do separate allocations for each, but that is slow +/// With std.ArrayList, pointers invalidate on resize and that means it will crash. +/// So a better idea is to batch up your allocations into one larger allocation +/// and then just make all the arrays point to different parts of the larger allocation +pub fn NewBatcher(comptime Type: type) type { + return struct { + head: []Type, + + pub fn init(allocator: std.mem.Allocator, count: usize) !@This() { + const all = try allocator.alloc(Type, count); + return @This(){ .head = all }; + } + + pub fn done(this: *@This()) void { + bun.assert(this.head.len == 0); // count to init() was too large, overallocation + } + + pub fn eat(this: *@This(), value: Type) *Type { + return @as(*Type, @ptrCast(&this.head.eat1(value).ptr)); + } + + pub fn eat1(this: *@This(), value: Type) []Type { + var prev = this.head[0..1]; + prev[0] = value; + this.head = this.head[1..]; + return prev; + } + + pub fn next(this: *@This(), values: anytype) []Type { + this.head[0..values.len].* = values; + const prev = this.head[0..values.len]; + this.head = this.head[values.len..]; + return prev; + } + }; +} diff --git a/src/jsc.zig b/src/jsc.zig index ae8a3394e1..8af9ba76f0 100644 --- a/src/jsc.zig +++ b/src/jsc.zig @@ -80,19 +80,19 @@ pub const jsBoolean = @This().JSValue.jsBoolean; pub const jsEmptyString = @This().JSValue.jsEmptyString; pub const jsNumber = @This().JSValue.jsNumber; -const __jsc_log = Output.scoped(.JSC, true); +const log = Output.scoped(.JSC, true); pub inline fn markBinding(src: std.builtin.SourceLocation) void { - __jsc_log("{s} ({s}:{d})", .{ src.fn_name, src.file, src.line }); + log("{s} ({s}:{d})", .{ src.fn_name, src.file, src.line }); } pub inline fn markMemberBinding(comptime class: anytype, src: std.builtin.SourceLocation) void { const classname = switch (@typeInfo(@TypeOf(class))) { .pointer => class, // assumed to be a static string else => @typeName(class), }; - __jsc_log("{s}.{s} ({s}:{d})", .{ classname, src.fn_name, src.file, src.line }); + log("{s}.{s} ({s}:{d})", .{ classname, src.fn_name, src.file, src.line }); } + pub const Subprocess = API.Bun.Subprocess; -pub const ResourceUsage = API.Bun.ResourceUsage; /// This file is generated by: /// 1. `bun src/bun.js/scripts/generate-classes.ts` @@ -105,12 +105,7 @@ pub const ResourceUsage = API.Bun.ResourceUsage; /// - pub usingnamespace JSC.Codegen.JSMyClassName; /// 5. make clean-bindings && make bindings -j10 /// -pub const Codegen = struct { - pub const GeneratedClasses = @import("ZigGeneratedClasses"); - pub usingnamespace GeneratedClasses; - pub usingnamespace @import("./bun.js/bindings/codegen.zig"); -}; - +pub const Codegen = @import("ZigGeneratedClasses"); pub const GeneratedClassesList = @import("./bun.js/bindings/generated_classes_list.zig").Classes; pub const RuntimeTranspilerCache = @import("./bun.js/RuntimeTranspilerCache.zig").RuntimeTranspilerCache; diff --git a/src/linux_c.zig b/src/linux_c.zig index d8cb012fde..52588080ed 100644 --- a/src/linux_c.zig +++ b/src/linux_c.zig @@ -493,28 +493,19 @@ pub fn posix_spawn_file_actions_addchdir_np(actions: *posix_spawn_file_actions_t pub extern fn vmsplice(fd: c_int, iovec: [*]const std.posix.iovec, iovec_count: usize, flags: u32) isize; -const net_c = @cImport({ - // TODO: remove this c import! instead of adding to it, add to - // c-headers-for-zig.h and use bun.C.translated. - @cInclude("ifaddrs.h"); // getifaddrs, freeifaddrs - @cInclude("net/if.h"); // IFF_RUNNING, IFF_UP - @cInclude("fcntl.h"); // F_DUPFD_CLOEXEC - @cInclude("sys/socket.h"); -}); - -pub const FD_CLOEXEC = net_c.FD_CLOEXEC; -pub const freeifaddrs = net_c.freeifaddrs; -pub const getifaddrs = net_c.getifaddrs; -pub const ifaddrs = net_c.ifaddrs; -pub const IFF_LOOPBACK = net_c.IFF_LOOPBACK; -pub const IFF_RUNNING = net_c.IFF_RUNNING; -pub const IFF_UP = net_c.IFF_UP; -pub const MSG_DONTWAIT = net_c.MSG_DONTWAIT; -pub const MSG_NOSIGNAL = net_c.MSG_NOSIGNAL; +pub const FD_CLOEXEC = bun.c.FD_CLOEXEC; +pub const freeifaddrs = bun.c.freeifaddrs; +pub const getifaddrs = bun.c.getifaddrs; +pub const ifaddrs = bun.c.ifaddrs; +pub const IFF_LOOPBACK = bun.c.IFF_LOOPBACK; +pub const IFF_RUNNING = bun.c.IFF_RUNNING; +pub const IFF_UP = bun.c.IFF_UP; +pub const MSG_DONTWAIT = bun.c.MSG_DONTWAIT; +pub const MSG_NOSIGNAL = bun.c.MSG_NOSIGNAL; pub const F = struct { - pub const DUPFD_CLOEXEC = net_c.F_DUPFD_CLOEXEC; - pub const DUPFD = net_c.F_DUPFD; + pub const DUPFD_CLOEXEC = bun.c.F_DUPFD_CLOEXEC; + pub const DUPFD = bun.c.F_DUPFD; }; pub const Mode = u32; @@ -538,7 +529,7 @@ pub fn getErrno(rc: anytype) E { // glibc system call wrapper returns i32/int // the errno is stored in a thread local variable // - // TODO: the inclusion of 'u32' and 'isize' seems suspicous + // TODO: the inclusion of 'u32' and 'isize' seems suspicious i32, c_int, u32, isize, i64 => if (rc == -1) @enumFromInt(std.c._errno().*) else @@ -550,17 +541,13 @@ pub fn getErrno(rc: anytype) E { pub const getuid = std.os.linux.getuid; pub const getgid = std.os.linux.getgid; -pub const linux_fs = if (bun.Environment.isLinux) @cImport({ - // TODO: remove this c import! instead of adding to it, add to - // c-headers-for-zig.h and use bun.C.translated. - @cInclude("linux/fs.h"); -}) else struct {}; +pub const linux_fs = bun.c; /// https://man7.org/linux/man-pages/man2/ioctl_ficlone.2.html /// /// Support for FICLONE is dependent on the filesystem driver. pub fn ioctl_ficlone(dest_fd: bun.FileDescriptor, srcfd: bun.FileDescriptor) usize { - return std.os.linux.ioctl(dest_fd.cast(), linux_fs.FICLONE, @intCast(srcfd.int())); + return std.os.linux.ioctl(dest_fd.cast(), bun.c.FICLONE, @intCast(srcfd.int())); } pub const RWFFlagSupport = enum(u8) { diff --git a/src/s3/credentials.zig b/src/s3/credentials.zig index 73e7e8f857..9014f9cf30 100644 --- a/src/s3/credentials.zig +++ b/src/s3/credentials.zig @@ -672,8 +672,8 @@ pub const S3Credentials = struct { const authorization = brk: { // we hash the hash so we need 2 buffers - var hmac_sig_service: [bun.BoringSSL.EVP_MAX_MD_SIZE]u8 = undefined; - var hmac_sig_service2: [bun.BoringSSL.EVP_MAX_MD_SIZE]u8 = undefined; + var hmac_sig_service: [bun.BoringSSL.c.EVP_MAX_MD_SIZE]u8 = undefined; + var hmac_sig_service2: [bun.BoringSSL.c.EVP_MAX_MD_SIZE]u8 = undefined; const sigDateRegionServiceReq = brk_sign: { const key = try std.fmt.bufPrint(&tmp_buffer, "{s}{s}{s}", .{ region, service_name, this.secretAccessKey }); diff --git a/src/sha.zig b/src/sha.zig index a8ee6356c5..b82dcaa2d3 100644 --- a/src/sha.zig +++ b/src/sha.zig @@ -1,4 +1,4 @@ -const BoringSSL = bun.BoringSSL; +const BoringSSL = bun.BoringSSL.c; const std = @import("std"); pub const bun = @import("root").bun; @@ -10,7 +10,7 @@ fn NewHasher(comptime digest_size: comptime_int, comptime ContextType: type, com pub const digest: comptime_int = digest_size; pub fn init() @This() { - BoringSSL.load(); + bun.BoringSSL.load(); var this: @This() = .{ .hasher = undefined, }; @@ -44,7 +44,7 @@ fn NewEVP(comptime digest_size: comptime_int, comptime MDName: []const u8) type pub const digest: comptime_int = digest_size; pub fn init() @This() { - BoringSSL.load(); + bun.BoringSSL.load(); const md = @field(BoringSSL, MDName)(); var this = @This(){}; diff --git a/src/sql/postgres.zig b/src/sql/postgres.zig index 4a240fdf85..a8f52d9c4e 100644 --- a/src/sql/postgres.zig +++ b/src/sql/postgres.zig @@ -12,7 +12,7 @@ pub const short = u16; pub const PostgresShort = u16; const Crypto = JSC.API.Bun.Crypto; const JSValue = JSC.JSValue; -const BoringSSL = @import("../boringssl.zig"); +const BoringSSL = bun.BoringSSL; pub const AnyPostgresError = error{ ConnectionClosed, ExpectedRequest, @@ -1340,7 +1340,7 @@ pub const PostgresSQLConnection = struct { }; fn hmac(password: []const u8, data: []const u8) ?[32]u8 { - var buf = std.mem.zeroes([bun.BoringSSL.EVP_MAX_MD_SIZE]u8); + var buf = std.mem.zeroes([bun.BoringSSL.c.EVP_MAX_MD_SIZE]u8); // TODO: I don't think this is failable. const result = bun.hmac.generate(password, data, .sha256, &buf) orelse return null; @@ -1713,8 +1713,8 @@ pub const PostgresSQLConnection = struct { return; } - const ssl_ptr = @as(*BoringSSL.SSL, @ptrCast(this.socket.getNativeHandle())); - if (BoringSSL.SSL_get_servername(ssl_ptr, 0)) |servername| { + const ssl_ptr: *BoringSSL.c.SSL = @ptrCast(this.socket.getNativeHandle()); + if (BoringSSL.c.SSL_get_servername(ssl_ptr, 0)) |servername| { const hostname = servername[0..bun.len(servername)]; if (!BoringSSL.checkServerIdentity(ssl_ptr, hostname)) { this.failWithJSValue(ssl_error.toJS(this.globalObject)); diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 2613d5c5df..6eb62ba086 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -5583,7 +5583,7 @@ pub fn moveSlice(slice: string, from: string, to: string) string { return result; } -pub usingnamespace @import("exact_size_matcher.zig"); +pub const ExactSizeMatcher = @import("exact_size_matcher.zig").ExactSizeMatcher; pub const unicode_replacement = 0xFFFD; pub const unicode_replacement_str = brk: { diff --git a/src/sync.zig b/src/sync.zig index 3740e849dc..851cb1ac39 100644 --- a/src/sync.zig +++ b/src/sync.zig @@ -371,24 +371,24 @@ pub fn Channel( const Self = @This(); const Buffer = std.fifo.LinearFifo(T, buffer_type); - pub usingnamespace switch (buffer_type) { - .Static => struct { - pub inline fn init() Self { - return Self.withBuffer(Buffer.init()); - } - }, - .Slice => struct { - pub inline fn init(buf: []T) Self { - return Self.withBuffer(Buffer.init(buf)); - } - }, - .Dynamic => struct { - pub inline fn init(allocator: std.mem.Allocator) Self { - return Self.withBuffer(Buffer.init(allocator)); - } - }, + pub const init = switch (buffer_type) { + .Static => initStatic, + .Slice => initSlice, + .Dynamic => initDynamic, }; + pub inline fn initStatic() Self { + return .withBuffer(Buffer.init()); + } + + pub inline fn initSlice(buf: []T) Self { + return .withBuffer(Buffer.init(buf)); + } + + pub inline fn initDynamic(allocator: std.mem.Allocator) Self { + return .withBuffer(Buffer.init(allocator)); + } + fn withBuffer(buffer: Buffer) Self { return Self{ .mutex = Mutex.init(), diff --git a/src/sys.zig b/src/sys.zig index d0ae1fdd4d..47fc5fc250 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -155,7 +155,7 @@ pub const O = switch (Environment.os) { pub const TMPFILE = 0o20040000; pub const NDELAY = NONBLOCK; - pub const SYMLINK = bun.C.translated.O_SYMLINK; + pub const SYMLINK = bun.c.O_SYMLINK; pub const toPacked = toPackedO; }, @@ -2198,7 +2198,7 @@ pub fn read(fd: bun.FileDescriptor, buf: []u8) Maybe(usize) { }; } -const socket_flags_nonblock = bun.C.MSG_DONTWAIT | bun.C.MSG_NOSIGNAL; +const socket_flags_nonblock = bun.c.MSG_DONTWAIT | bun.c.MSG_NOSIGNAL; pub fn recvNonBlock(fd: bun.FileDescriptor, buf: []u8) Maybe(usize) { return recv(fd, buf, socket_flags_nonblock); diff --git a/src/util.zig b/src/util.zig index d4eadfcf99..04b2fa9a86 100644 --- a/src/util.zig +++ b/src/util.zig @@ -166,18 +166,6 @@ pub inline fn from( return fromSlice(Array, allocator, []const Of(Array), @as([]const Of(Array), default)); } -pub fn concat( - comptime T: type, - dest: []T, - src: []const []const T, -) void { - var remain = dest; - for (src) |group| { - bun.copy(T, remain[0..group.len], group); - remain = remain[group.len..]; - } -} - pub fn fromSlice( comptime Array: type, allocator: std.mem.Allocator, @@ -241,44 +229,6 @@ pub fn fromSlice( } } -/// Say you need to allocate a bunch of tiny arrays -/// You could just do separate allocations for each, but that is slow -/// With std.ArrayList, pointers invalidate on resize and that means it will crash. -/// So a better idea is to batch up your allocations into one larger allocation -/// and then just make all the arrays point to different parts of the larger allocation -pub fn Batcher(comptime Type: type) type { - return struct { - head: []Type, - - pub fn init(allocator: std.mem.Allocator, count: usize) !@This() { - const all = try allocator.alloc(Type, count); - return @This(){ .head = all }; - } - - pub fn done(this: *@This()) void { - bun.assert(this.head.len == 0); // count to init() was too large, overallocation - } - - pub fn eat(this: *@This(), value: Type) *Type { - return @as(*Type, @ptrCast(&this.head.eat1(value).ptr)); - } - - pub fn eat1(this: *@This(), value: Type) []Type { - var prev = this.head[0..1]; - prev[0] = value; - this.head = this.head[1..]; - return prev; - } - - pub fn next(this: *@This(), values: anytype) []Type { - this.head[0..values.len].* = values; - const prev = this.head[0..values.len]; - this.head = this.head[values.len..]; - return prev; - } - }; -} - fn needsAllocator(comptime Fn: anytype) bool { return std.meta.fields(std.meta.ArgsTuple(@TypeOf(Fn))).len > 2; }