mirror of
https://github.com/oven-sh/bun
synced 2026-02-18 23:01:58 +00:00
Replace `catch bun.outOfMemory()`, which can accidentally catch non-OOM-related errors, with either `bun.handleOom` or a manual `catch |err| switch (err)`. (For internal tracking: fixes STAB-1070) --------- Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
1852 lines
63 KiB
Zig
1852 lines
63 KiB
Zig
//! The Subprocess object is returned by `Bun.spawn`. This file also holds the
|
|
//! code for `Bun.spawnSync`
|
|
|
|
const Subprocess = @This();
|
|
|
|
pub const js = jsc.Codegen.JSSubprocess;
|
|
pub const toJS = js.toJS;
|
|
pub const fromJS = js.fromJS;
|
|
pub const fromJSDirect = js.fromJSDirect;
|
|
|
|
const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{});
|
|
pub const ref = RefCount.ref;
|
|
pub const deref = RefCount.deref;
|
|
|
|
ref_count: RefCount,
|
|
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,
|
|
|
|
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.SendQueue,
|
|
flags: Flags = .{},
|
|
|
|
weak_file_sink_stdin_ptr: ?*jsc.WebCore.FileSink = null,
|
|
abort_signal: ?*webcore.AbortSignal = null,
|
|
|
|
event_loop_timer_refd: bool = false,
|
|
event_loop_timer: bun.api.Timer.EventLoopTimer = .{
|
|
.tag = .SubprocessTimeout,
|
|
.next = .{
|
|
.sec = 0,
|
|
.nsec = 0,
|
|
},
|
|
},
|
|
killSignal: SignalCode,
|
|
|
|
stdout_maxbuf: ?*MaxBuf = null,
|
|
stderr_maxbuf: ?*MaxBuf = null,
|
|
exited_due_to_maxbuf: ?MaxBuf.Kind = null,
|
|
|
|
pub const Flags = packed struct(u8) {
|
|
is_sync: bool = false,
|
|
killed: bool = false,
|
|
has_stdin_destructor_called: bool = false,
|
|
finalized: bool = false,
|
|
deref_on_stdin_destroyed: bool = false,
|
|
is_stdin_a_readable_stream: bool = false,
|
|
_: u2 = 0,
|
|
};
|
|
|
|
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 inline fn assertStdioResult(result: StdioResult) void {
|
|
if (comptime Environment.allow_assert) {
|
|
if (Environment.isPosix) {
|
|
if (result) |fd| {
|
|
bun.assert(fd != bun.invalid_fd);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub const ResourceUsage = @import("./subprocess/ResourceUsage.zig");
|
|
|
|
pub fn appendEnvpFromJS(globalThis: *jsc.JSGlobalObject, object: *jsc.JSObject, envp: *std.ArrayList(?[*:0]const u8), PATH: *[]const u8) bun.JSError!void {
|
|
var object_iter = try jsc.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true }).init(globalThis, object);
|
|
defer object_iter.deinit();
|
|
|
|
try envp.ensureTotalCapacityPrecise(object_iter.len +
|
|
// +1 incase there's IPC
|
|
// +1 for null terminator
|
|
2);
|
|
while (try object_iter.next()) |key| {
|
|
var value = object_iter.value;
|
|
if (value.isUndefined()) continue;
|
|
|
|
const line = try std.fmt.allocPrintZ(envp.allocator, "{}={}", .{ key, try value.getZigString(globalThis) });
|
|
|
|
if (key.eqlComptime("PATH")) {
|
|
PATH.* = bun.asByteSlice(line["PATH=".len..]);
|
|
}
|
|
|
|
try envp.append(line);
|
|
}
|
|
}
|
|
|
|
const log = Output.scoped(.Subprocess, .visible);
|
|
pub const StdioKind = enum {
|
|
stdin,
|
|
stdout,
|
|
stderr,
|
|
|
|
pub fn toFd(this: @This()) bun.FileDescriptor {
|
|
return switch (this) {
|
|
.stdin => .stdin(),
|
|
.stdout => .stdout(),
|
|
.stderr => .stderr(),
|
|
};
|
|
}
|
|
|
|
pub fn toNum(this: @This()) c_int {
|
|
return switch (this) {
|
|
.stdin => 0,
|
|
.stdout => 1,
|
|
.stderr => 2,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub fn onAbortSignal(subprocess_ctx: ?*anyopaque, _: jsc.JSValue) callconv(.C) void {
|
|
var this: *Subprocess = @ptrCast(@alignCast(subprocess_ctx.?));
|
|
this.clearAbortSignal();
|
|
_ = this.tryKill(this.killSignal);
|
|
}
|
|
|
|
pub fn resourceUsage(
|
|
this: *Subprocess,
|
|
globalObject: *JSGlobalObject,
|
|
_: *jsc.CallFrame,
|
|
) bun.JSError!JSValue {
|
|
return this.createResourceUsageObject(globalObject);
|
|
}
|
|
|
|
pub fn createResourceUsageObject(this: *Subprocess, globalObject: *JSGlobalObject) bun.JSError!JSValue {
|
|
return ResourceUsage.create(
|
|
brk: {
|
|
if (this.pid_rusage != null) {
|
|
break :brk &this.pid_rusage.?;
|
|
}
|
|
|
|
if (Environment.isWindows) {
|
|
if (this.process.poller == .uv) {
|
|
this.pid_rusage = PosixSpawn.process.uv_getrusage(&this.process.poller.uv);
|
|
break :brk &this.pid_rusage.?;
|
|
}
|
|
}
|
|
|
|
return .js_undefined;
|
|
},
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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 = CowString.initOwned(pipe.state.done, bun.default_allocator) };
|
|
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", .{});
|
|
}
|
|
|
|
pub const PipeReader = @import("./subprocess/SubprocessPipeReader.zig");
|
|
pub const Readable = @import("./subprocess/Readable.zig").Readable;
|
|
|
|
pub fn getStderr(this: *Subprocess, globalThis: *JSGlobalObject) bun.JSError!JSValue {
|
|
this.observable_getters.insert(.stderr);
|
|
return this.stderr.toJS(globalThis, this.hasExited());
|
|
}
|
|
|
|
pub fn getStdin(this: *Subprocess, globalThis: *JSGlobalObject) bun.JSError!JSValue {
|
|
this.observable_getters.insert(.stdin);
|
|
return this.stdin.toJS(globalThis, this);
|
|
}
|
|
|
|
pub fn getStdout(this: *Subprocess, globalThis: *JSGlobalObject) bun.JSError!JSValue {
|
|
this.observable_getters.insert(.stdout);
|
|
// NOTE: ownership of internal buffers is transferred to the JSValue, which
|
|
// gets cached on JSSubprocess (created via bindgen). This makes it
|
|
// re-accessable to JS code but not via `this.stdout`, which is now `.closed`.
|
|
return this.stdout.toJS(globalThis, this.hasExited());
|
|
}
|
|
|
|
pub fn asyncDispose(this: *Subprocess, global: *JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
|
|
if (this.process.hasExited()) {
|
|
// rely on GC to clean everything up in this case
|
|
return .js_undefined;
|
|
}
|
|
|
|
const this_jsvalue = callframe.this();
|
|
|
|
defer this_jsvalue.ensureStillAlive();
|
|
|
|
// 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(this.killSignal)) {
|
|
.result => {},
|
|
.err => |err| {
|
|
// Signal 9 should always be fine, but just in case that somehow fails.
|
|
return global.throwValue(err.toJS(global));
|
|
},
|
|
}
|
|
|
|
return this.getExited(this_jsvalue, global);
|
|
}
|
|
|
|
fn setEventLoopTimerRefd(this: *Subprocess, refd: bool) void {
|
|
if (this.event_loop_timer_refd == refd) return;
|
|
this.event_loop_timer_refd = refd;
|
|
if (refd) {
|
|
this.globalThis.bunVM().timer.incrementTimerRef(1);
|
|
} else {
|
|
this.globalThis.bunVM().timer.incrementTimerRef(-1);
|
|
}
|
|
}
|
|
|
|
pub fn timeoutCallback(this: *Subprocess) bun.api.Timer.EventLoopTimer.Arm {
|
|
this.setEventLoopTimerRefd(false);
|
|
if (this.event_loop_timer.state == .CANCELLED) return .disarm;
|
|
if (this.hasExited()) {
|
|
this.event_loop_timer.state = .CANCELLED;
|
|
return .disarm;
|
|
}
|
|
this.event_loop_timer.state = .FIRED;
|
|
_ = this.tryKill(this.killSignal);
|
|
return .disarm;
|
|
}
|
|
|
|
pub fn onMaxBuffer(this: *Subprocess, kind: MaxBuf.Kind) void {
|
|
this.exited_due_to_maxbuf = kind;
|
|
_ = this.tryKill(this.killSignal);
|
|
}
|
|
|
|
fn parseSignal(arg: jsc.JSValue, globalThis: *jsc.JSGlobalObject) !SignalCode {
|
|
if (arg.getNumber()) |sig64| {
|
|
// Node does this:
|
|
if (std.math.isNan(sig64)) {
|
|
return SignalCode.default;
|
|
}
|
|
|
|
// This matches node behavior, minus some details with the error messages: https://gist.github.com/Jarred-Sumner/23ba38682bf9d84dff2f67eb35c42ab6
|
|
if (std.math.isInf(sig64) or @trunc(sig64) != sig64) {
|
|
return globalThis.throwInvalidArguments("Unknown signal", .{});
|
|
}
|
|
|
|
if (sig64 < 0) {
|
|
return globalThis.throwInvalidArguments("Invalid signal: must be >= 0", .{});
|
|
}
|
|
|
|
if (sig64 > 31) {
|
|
return globalThis.throwInvalidArguments("Invalid signal: must be < 32", .{});
|
|
}
|
|
|
|
const code: SignalCode = @enumFromInt(@as(u8, @intFromFloat(sig64)));
|
|
return code;
|
|
} else if (arg.isString()) {
|
|
if (arg.asString().length() == 0) {
|
|
return SignalCode.default;
|
|
}
|
|
const signal_code = try arg.toEnum(globalThis, "signal", SignalCode);
|
|
return signal_code;
|
|
} else if (!arg.isEmptyOrUndefinedOrNull()) {
|
|
return globalThis.throwInvalidArguments("Invalid signal: must be a string or an integer", .{});
|
|
}
|
|
|
|
return SignalCode.default;
|
|
}
|
|
|
|
pub fn kill(
|
|
this: *Subprocess,
|
|
globalThis: *JSGlobalObject,
|
|
callframe: *jsc.CallFrame,
|
|
) bun.JSError!JSValue {
|
|
this.this_jsvalue = callframe.this();
|
|
|
|
const arguments = callframe.arguments_old(1);
|
|
// If signal is 0, then no actual signal is sent, but error checking
|
|
// is still performed.
|
|
const sig: SignalCode = try parseSignal(arguments.ptr[0], globalThis);
|
|
|
|
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.toJS(globalThis));
|
|
},
|
|
}
|
|
|
|
return .js_undefined;
|
|
}
|
|
|
|
pub fn hasKilled(this: *const Subprocess) bool {
|
|
return this.process.hasKilled();
|
|
}
|
|
|
|
pub fn tryKill(this: *Subprocess, sig: SignalCode) bun.sys.Maybe(void) {
|
|
if (this.hasExited()) {
|
|
return .success;
|
|
}
|
|
return this.process.kill(@intFromEnum(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 .js_undefined;
|
|
}
|
|
|
|
pub fn doUnref(this: *Subprocess, _: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!JSValue {
|
|
this.jsUnref();
|
|
return .js_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", .{});
|
|
|
|
return IPC.doSend(if (this.ipc_data) |*data| data else null, global, callFrame, if (this.hasExited()) .subprocess_exited else .subprocess);
|
|
}
|
|
pub fn disconnectIPC(this: *Subprocess, nextTick: bool) void {
|
|
const ipc_data = this.ipc() orelse return;
|
|
ipc_data.closeSocketNextTick(nextTick);
|
|
}
|
|
pub fn disconnect(this: *Subprocess, globalThis: *JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
|
|
_ = globalThis;
|
|
_ = callframe;
|
|
this.disconnectIPC(true);
|
|
return .js_undefined;
|
|
}
|
|
|
|
pub fn getConnected(this: *Subprocess, globalThis: *JSGlobalObject) JSValue {
|
|
_ = globalThis;
|
|
const ipc_data = this.ipc();
|
|
return JSValue.jsBoolean(ipc_data != null and ipc_data.?.isConnected());
|
|
}
|
|
|
|
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) bun.JSError!JSValue {
|
|
const array = try JSValue.createEmptyArray(global, 0);
|
|
try array.push(global, .null);
|
|
try array.push(global, .null); // TODO: align this with options
|
|
try 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) {
|
|
try 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());
|
|
try array.push(global, JSValue.jsNumber(fdno));
|
|
}
|
|
} else {
|
|
try array.push(global, JSValue.jsNumber(item.cast()));
|
|
}
|
|
}
|
|
return array;
|
|
}
|
|
|
|
pub const Source = union(enum) {
|
|
blob: jsc.WebCore.Blob.Any,
|
|
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 NewStaticPipeWriter = @import("./subprocess/StaticPipeWriter.zig").NewStaticPipeWriter;
|
|
pub const StaticPipeWriter = NewStaticPipeWriter(Subprocess);
|
|
|
|
pub fn memoryCost(this: *const Subprocess) usize {
|
|
return @sizeOf(@This()) +
|
|
this.process.memoryCost() +
|
|
this.stdin.memoryCost() +
|
|
this.stdout.memoryCost() +
|
|
this.stderr.memoryCost();
|
|
}
|
|
|
|
fn consumeExitedPromise(this_jsvalue: JSValue, globalThis: *jsc.JSGlobalObject) ?JSValue {
|
|
if (jsc.Codegen.JSSubprocess.exitedPromiseGetCached(this_jsvalue)) |promise| {
|
|
jsc.Codegen.JSSubprocess.exitedPromiseSetCached(this_jsvalue, globalThis, .zero);
|
|
return promise;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn consumeOnExitCallback(this_jsvalue: JSValue, globalThis: *jsc.JSGlobalObject) ?JSValue {
|
|
if (jsc.Codegen.JSSubprocess.onExitCallbackGetCached(this_jsvalue)) |callback| {
|
|
jsc.Codegen.JSSubprocess.onExitCallbackSetCached(this_jsvalue, globalThis, .zero);
|
|
return callback;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn consumeOnDisconnectCallback(this_jsvalue: JSValue, globalThis: *jsc.JSGlobalObject) ?JSValue {
|
|
if (jsc.Codegen.JSSubprocess.onDisconnectCallbackGetCached(this_jsvalue)) |callback| {
|
|
jsc.Codegen.JSSubprocess.onDisconnectCallbackSetCached(this_jsvalue, globalThis, .zero);
|
|
return callback;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
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);
|
|
|
|
if (this.event_loop_timer.state == .ACTIVE) {
|
|
jsc_vm.timer.remove(&this.event_loop_timer);
|
|
}
|
|
this.setEventLoopTimerRefd(false);
|
|
|
|
jsc_vm.onSubprocessExit(process);
|
|
|
|
var stdin: ?*jsc.WebCore.FileSink = if (this.stdin == .pipe and this.flags.is_stdin_a_readable_stream) this.stdin.pipe else 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)));
|
|
}
|
|
|
|
if (!this.flags.is_stdin_a_readable_stream) {
|
|
existing_stdin_value = existing_value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// We won't be sending any more data.
|
|
if (this.stdin == .buffer) {
|
|
this.stdin.buffer.close();
|
|
}
|
|
|
|
if (existing_stdin_value != .zero) {
|
|
jsc.WebCore.FileSink.JSSink.setDestroyCallback(existing_stdin_value, 0);
|
|
}
|
|
|
|
if (this.flags.is_sync) {
|
|
// This doesn't match Node.js' behavior, but for synchronous
|
|
// subprocesses the streams should not keep the timers going.
|
|
if (this.stdout == .pipe) {
|
|
this.stdout.close();
|
|
}
|
|
|
|
if (this.stderr == .pipe) {
|
|
this.stderr.close();
|
|
}
|
|
} else {
|
|
// This matches Node.js behavior. Node calls resume() on the streams.
|
|
if (this.stdout == .pipe and !this.stdout.pipe.reader.isDone()) {
|
|
this.stdout.pipe.reader.read();
|
|
}
|
|
|
|
if (this.stderr == .pipe and !this.stderr.pipe.reader.isDone()) {
|
|
this.stderr.pipe.reader.read();
|
|
}
|
|
}
|
|
|
|
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(&status);
|
|
}
|
|
|
|
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_jsvalue != .zero) {
|
|
if (consumeExitedPromise(this_jsvalue, globalThis)) |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.toJS(globalThis)),
|
|
.signaled => promise.asAnyPromise().?.resolve(globalThis, JSValue.jsNumber(128 +% @intFromEnum(status.signaled))),
|
|
else => {
|
|
// crash in debug mode
|
|
if (comptime Environment.allow_assert)
|
|
unreachable;
|
|
},
|
|
}
|
|
}
|
|
|
|
if (consumeOnExitCallback(this_jsvalue, globalThis)) |callback| {
|
|
const waitpid_value: JSValue =
|
|
if (status == .err)
|
|
status.err.toJS(globalThis)
|
|
else
|
|
.js_undefined;
|
|
|
|
const this_value: JSValue = if (this_jsvalue.isEmptyOrUndefinedOrNull()) .js_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 {
|
|
item.close();
|
|
}
|
|
}
|
|
this.stdio_pipes.clearAndFree(bun.default_allocator);
|
|
}
|
|
}
|
|
|
|
fn deinit(this: *Subprocess) void {
|
|
log("deinit", .{});
|
|
bun.destroy(this);
|
|
}
|
|
|
|
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();
|
|
|
|
if (this.event_loop_timer.state == .ACTIVE) {
|
|
this.globalThis.bunVM().timer.remove(&this.event_loop_timer);
|
|
}
|
|
this.setEventLoopTimerRefd(false);
|
|
|
|
MaxBuf.removeFromSubprocess(&this.stdout_maxbuf);
|
|
MaxBuf.removeFromSubprocess(&this.stderr_maxbuf);
|
|
|
|
if (this.ipc_data != null) {
|
|
this.disconnectIPC(false);
|
|
}
|
|
|
|
this.flags.finalized = true;
|
|
this.deref();
|
|
}
|
|
|
|
pub fn getExited(
|
|
this: *Subprocess,
|
|
this_value: JSValue,
|
|
globalThis: *JSGlobalObject,
|
|
) JSValue {
|
|
if (jsc.Codegen.JSSubprocess.exitedPromiseGetCached(this_value)) |promise| {
|
|
return promise;
|
|
}
|
|
|
|
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.dangerouslyCreateRejectedPromiseValueWithoutNotifyingVM(globalThis, err.toJS(globalThis));
|
|
},
|
|
else => {
|
|
const promise = jsc.JSPromise.create(globalThis).toJS();
|
|
jsc.Codegen.JSSubprocess.exitedPromiseSetCached(this_value, globalThis, promise);
|
|
return promise;
|
|
},
|
|
}
|
|
}
|
|
|
|
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, pretend_argv0: ?[*:0]const u8, first_cmd: JSValue, allocator: std.mem.Allocator) bun.JSError!struct {
|
|
argv0: [:0]const u8,
|
|
arg0: [:0]u8,
|
|
} {
|
|
var arg0 = try first_cmd.toSliceOrNullWithAllocator(globalThis, allocator);
|
|
defer arg0.deinit();
|
|
// Heap allocate it to ensure we don't run out of stack space.
|
|
const path_buf: *bun.PathBuffer = try bun.default_allocator.create(bun.PathBuffer);
|
|
defer bun.default_allocator.destroy(path_buf);
|
|
|
|
var actual_argv0: [:0]const u8 = "";
|
|
|
|
const argv0_to_use: []const u8 = arg0.slice();
|
|
|
|
// This mimicks libuv's behavior, which mimicks execvpe
|
|
// Only resolve from $PATH when the command is not an absolute path
|
|
const PATH_to_use: []const u8 = if (strings.containsChar(argv0_to_use, '/'))
|
|
""
|
|
// If no $PATH is provided, we fallback to the one from environ
|
|
// This is already the behavior of the PATH passed in here.
|
|
else if (PATH.len > 0)
|
|
PATH
|
|
else if (comptime Environment.isPosix)
|
|
// If the user explicitly passed an empty $PATH, we fallback to the OS-specific default (which libuv also does)
|
|
bun.sliceTo(BUN_DEFAULT_PATH_FOR_SPAWN, 0)
|
|
else
|
|
"";
|
|
|
|
if (PATH_to_use.len == 0) {
|
|
actual_argv0 = try allocator.dupeZ(u8, argv0_to_use);
|
|
} else {
|
|
const resolved = which(path_buf, PATH_to_use, cwd, argv0_to_use) orelse {
|
|
return throwCommandNotFound(globalThis, argv0_to_use);
|
|
};
|
|
actual_argv0 = try allocator.dupeZ(u8, resolved);
|
|
}
|
|
|
|
return .{
|
|
.argv0 = actual_argv0,
|
|
.arg0 = if (pretend_argv0) |p| try allocator.dupeZ(u8, bun.sliceTo(p, 0)) else try allocator.dupeZ(u8, arg0.slice()),
|
|
};
|
|
}
|
|
|
|
fn getArgv(globalThis: *jsc.JSGlobalObject, args: JSValue, PATH: []const u8, cwd: []const u8, argv0: *?[*:0]const u8, allocator: std.mem.Allocator, argv: *std.ArrayList(?[*:0]const u8)) bun.JSError!void {
|
|
var cmds_array = try args.arrayIterator(globalThis);
|
|
// + 1 for argv0
|
|
// + 1 for null terminator
|
|
argv.* = try @TypeOf(argv.*).initCapacity(allocator, cmds_array.len + 2);
|
|
|
|
if (args.isEmptyOrUndefinedOrNull()) {
|
|
return globalThis.throwInvalidArguments("cmd must be an array of strings", .{});
|
|
}
|
|
|
|
if (cmds_array.len == 0) {
|
|
return globalThis.throwInvalidArguments("cmd must not be empty", .{});
|
|
}
|
|
|
|
const argv0_result = try getArgv0(globalThis, PATH, cwd, argv0.*, (try cmds_array.next()).?, allocator);
|
|
|
|
argv0.* = argv0_result.argv0.ptr;
|
|
argv.appendAssumeCapacity(argv0_result.arg0.ptr);
|
|
|
|
while (try cmds_array.next()) |value| {
|
|
const arg = try value.toBunString(globalThis);
|
|
defer arg.deref();
|
|
|
|
argv.appendAssumeCapacity(try arg.toOwnedSliceZ(allocator));
|
|
}
|
|
|
|
if (argv.items.len == 0) {
|
|
return globalThis.throwInvalidArguments("cmd must be an array of strings", .{});
|
|
}
|
|
}
|
|
|
|
pub fn spawnMaybeSync(
|
|
globalThis: *jsc.JSGlobalObject,
|
|
args_: JSValue,
|
|
secondaryArgsValue: ?JSValue,
|
|
comptime is_sync: bool,
|
|
) bun.JSError!JSValue {
|
|
if (comptime is_sync) {
|
|
// We skip this on Windows due to test failures.
|
|
if (comptime !Environment.isWindows) {
|
|
// Since the event loop is recursively called, we need to check if it's safe to recurse.
|
|
if (!bun.StackCheck.init().isSafeToRecurse()) {
|
|
return globalThis.throwStackOverflow();
|
|
}
|
|
}
|
|
}
|
|
|
|
var arena = bun.ArenaAllocator.init(bun.default_allocator);
|
|
defer arena.deinit();
|
|
const allocator = arena.allocator();
|
|
|
|
var override_env = false;
|
|
var env_array = std.ArrayListUnmanaged(?[*:0]const u8){};
|
|
var jsc_vm = globalThis.bunVM();
|
|
|
|
var cwd = jsc_vm.transpiler.fs.top_level_dir;
|
|
|
|
var stdio = [3]Stdio{
|
|
.{ .ignore = {} },
|
|
.{ .pipe = {} },
|
|
.{ .inherit = {} },
|
|
};
|
|
|
|
if (comptime is_sync) {
|
|
stdio[1] = .{ .pipe = {} };
|
|
stdio[2] = .{ .pipe = {} };
|
|
}
|
|
var lazy = false;
|
|
var on_exit_callback = JSValue.zero;
|
|
var on_disconnect_callback = JSValue.zero;
|
|
var PATH = jsc_vm.transpiler.env.get("PATH") orelse "";
|
|
var argv = std.ArrayList(?[*:0]const u8).init(allocator);
|
|
var cmd_value = JSValue.zero;
|
|
var detached = false;
|
|
var args = args_;
|
|
var maybe_ipc_mode: if (is_sync) void else ?IPC.Mode = if (is_sync) {} else null;
|
|
var ipc_callback: JSValue = .zero;
|
|
var extra_fds = std.ArrayList(bun.spawn.SpawnOptions.Stdio).init(bun.default_allocator);
|
|
var argv0: ?[*:0]const u8 = null;
|
|
var ipc_channel: i32 = -1;
|
|
var timeout: ?i32 = null;
|
|
var killSignal: SignalCode = SignalCode.default;
|
|
var maxBuffer: ?i64 = null;
|
|
|
|
var windows_hide: bool = false;
|
|
var windows_verbatim_arguments: bool = false;
|
|
var abort_signal: ?*jsc.WebCore.AbortSignal = null;
|
|
defer {
|
|
// Ensure we clean it up on error.
|
|
if (abort_signal) |signal| {
|
|
signal.unref();
|
|
}
|
|
}
|
|
|
|
{
|
|
if (args.isEmptyOrUndefinedOrNull()) {
|
|
return globalThis.throwInvalidArguments("cmd must be an array", .{});
|
|
}
|
|
|
|
const args_type = args.jsType();
|
|
if (args_type.isArray()) {
|
|
cmd_value = args;
|
|
args = secondaryArgsValue orelse JSValue.zero;
|
|
} else if (!args.isObject()) {
|
|
return globalThis.throwInvalidArguments("cmd must be an array", .{});
|
|
} else if (try args.getTruthy(globalThis, "cmd")) |cmd_value_| {
|
|
cmd_value = cmd_value_;
|
|
} else {
|
|
return globalThis.throwInvalidArguments("cmd must be an array", .{});
|
|
}
|
|
|
|
if (args.isObject()) {
|
|
if (try args.getTruthy(globalThis, "argv0")) |argv0_| {
|
|
const argv0_str = try argv0_.getZigString(globalThis);
|
|
if (argv0_str.len > 0) {
|
|
argv0 = try argv0_str.toOwnedSliceZ(allocator);
|
|
}
|
|
}
|
|
|
|
// need to update `cwd` before searching for executable with `Which.which`
|
|
if (try args.getTruthy(globalThis, "cwd")) |cwd_| {
|
|
const cwd_str = try cwd_.getZigString(globalThis);
|
|
if (cwd_str.len > 0) {
|
|
cwd = try cwd_str.toOwnedSliceZ(allocator);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (args != .zero and args.isObject()) {
|
|
// This must run before the stdio parsing happens
|
|
if (!is_sync) {
|
|
if (try args.getTruthy(globalThis, "ipc")) |val| {
|
|
if (val.isCell() and val.isCallable()) {
|
|
maybe_ipc_mode = ipc_mode: {
|
|
if (try args.getTruthy(globalThis, "serialization")) |mode_val| {
|
|
if (mode_val.isString()) {
|
|
break :ipc_mode try IPC.Mode.fromJS(globalThis, mode_val) orelse {
|
|
return globalThis.throwInvalidArguments("serialization must be \"json\" or \"advanced\"", .{});
|
|
};
|
|
} else {
|
|
if (!globalThis.hasException()) {
|
|
return globalThis.throwInvalidArgumentType("spawn", "serialization", "string");
|
|
}
|
|
return .zero;
|
|
}
|
|
}
|
|
break :ipc_mode .advanced;
|
|
};
|
|
|
|
ipc_callback = val.withAsyncContextIfNeeded(globalThis);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (try args.getTruthy(globalThis, "signal")) |signal_val| {
|
|
if (signal_val.as(jsc.WebCore.AbortSignal)) |signal| {
|
|
abort_signal = signal.ref();
|
|
} else {
|
|
return globalThis.throwInvalidArgumentTypeValue("signal", "AbortSignal", signal_val);
|
|
}
|
|
}
|
|
|
|
if (try args.getTruthy(globalThis, "onDisconnect")) |onDisconnect_| {
|
|
if (!onDisconnect_.isCell() or !onDisconnect_.isCallable()) {
|
|
return globalThis.throwInvalidArguments("onDisconnect must be a function or undefined", .{});
|
|
}
|
|
|
|
on_disconnect_callback = if (comptime is_sync)
|
|
onDisconnect_
|
|
else
|
|
onDisconnect_.withAsyncContextIfNeeded(globalThis);
|
|
}
|
|
|
|
if (try args.getTruthy(globalThis, "onExit")) |onExit_| {
|
|
if (!onExit_.isCell() or !onExit_.isCallable()) {
|
|
return globalThis.throwInvalidArguments("onExit must be a function or undefined", .{});
|
|
}
|
|
|
|
on_exit_callback = if (comptime is_sync)
|
|
onExit_
|
|
else
|
|
onExit_.withAsyncContextIfNeeded(globalThis);
|
|
}
|
|
|
|
if (try args.getTruthy(globalThis, "env")) |env_arg| {
|
|
env_arg.ensureStillAlive();
|
|
const object = env_arg.getObject() orelse {
|
|
return globalThis.throwInvalidArguments("env must be an object", .{});
|
|
};
|
|
|
|
override_env = true;
|
|
// If the env object does not include a $PATH, it must disable path lookup for argv[0]
|
|
var NEW_PATH: []const u8 = "";
|
|
var envp_managed = env_array.toManaged(allocator);
|
|
try appendEnvpFromJS(globalThis, object, &envp_managed, &NEW_PATH);
|
|
env_array = envp_managed.moveToUnmanaged();
|
|
PATH = NEW_PATH;
|
|
}
|
|
|
|
try getArgv(globalThis, cmd_value, PATH, cwd, &argv0, allocator, &argv);
|
|
|
|
if (try args.get(globalThis, "stdio")) |stdio_val| {
|
|
if (!stdio_val.isEmptyOrUndefinedOrNull()) {
|
|
if (stdio_val.jsType().isArray()) {
|
|
var stdio_iter = try stdio_val.arrayIterator(globalThis);
|
|
var i: u31 = 0;
|
|
while (try stdio_iter.next()) |value| : (i += 1) {
|
|
try stdio[i].extract(globalThis, i, value, is_sync);
|
|
if (i == 2)
|
|
break;
|
|
}
|
|
i += 1;
|
|
|
|
while (try stdio_iter.next()) |value| : (i += 1) {
|
|
var new_item: Stdio = undefined;
|
|
try new_item.extract(globalThis, i, value, is_sync);
|
|
|
|
const opt = switch (new_item.asSpawnOption(i)) {
|
|
.result => |opt| opt,
|
|
.err => |e| {
|
|
return e.throwJS(globalThis);
|
|
},
|
|
};
|
|
if (opt == .ipc) {
|
|
ipc_channel = @intCast(extra_fds.items.len);
|
|
}
|
|
try extra_fds.append(opt);
|
|
}
|
|
} else {
|
|
return globalThis.throwInvalidArguments("stdio must be an array", .{});
|
|
}
|
|
}
|
|
} else {
|
|
if (try args.get(globalThis, "stdin")) |value| {
|
|
try stdio[0].extract(globalThis, 0, value, is_sync);
|
|
}
|
|
|
|
if (try args.get(globalThis, "stderr")) |value| {
|
|
try stdio[2].extract(globalThis, 2, value, is_sync);
|
|
}
|
|
|
|
if (try args.get(globalThis, "stdout")) |value| {
|
|
try stdio[1].extract(globalThis, 1, value, is_sync);
|
|
}
|
|
}
|
|
|
|
if (comptime !is_sync) {
|
|
if (try args.get(globalThis, "lazy")) |lazy_val| {
|
|
if (lazy_val.isBoolean()) {
|
|
lazy = lazy_val.toBoolean();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (try args.get(globalThis, "detached")) |detached_val| {
|
|
if (detached_val.isBoolean()) {
|
|
detached = detached_val.toBoolean();
|
|
}
|
|
}
|
|
|
|
if (Environment.isWindows) {
|
|
if (try args.get(globalThis, "windowsHide")) |val| {
|
|
if (val.isBoolean()) {
|
|
windows_hide = val.asBoolean();
|
|
}
|
|
}
|
|
|
|
if (try args.get(globalThis, "windowsVerbatimArguments")) |val| {
|
|
if (val.isBoolean()) {
|
|
windows_verbatim_arguments = val.asBoolean();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (try args.get(globalThis, "timeout")) |timeout_value| brk: {
|
|
if (timeout_value != .null) {
|
|
if (timeout_value.isNumber() and std.math.isPositiveInf(timeout_value.asNumber())) {
|
|
break :brk;
|
|
}
|
|
|
|
const timeout_int = try globalThis.validateIntegerRange(timeout_value, u64, 0, .{ .min = 0, .field_name = "timeout" });
|
|
if (timeout_int > 0)
|
|
timeout = @intCast(@as(u31, @truncate(timeout_int)));
|
|
}
|
|
}
|
|
|
|
if (try args.get(globalThis, "killSignal")) |val| {
|
|
killSignal = try parseSignal(val, globalThis);
|
|
}
|
|
|
|
if (try args.get(globalThis, "maxBuffer")) |val| {
|
|
if (val.isNumber() and val.isFinite()) { // 'Infinity' does not set maxBuffer
|
|
const value = try val.coerce(i64, globalThis);
|
|
if (value > 0) {
|
|
maxBuffer = value;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
try getArgv(globalThis, cmd_value, PATH, cwd, &argv0, allocator, &argv);
|
|
}
|
|
}
|
|
|
|
log("spawn maxBuffer: {?d}", .{maxBuffer});
|
|
|
|
if (!override_env and env_array.items.len == 0) {
|
|
env_array.items = jsc_vm.transpiler.env.map.createNullDelimitedEnvMap(allocator) catch |err| return globalThis.throwError(err, "in Bun.spawn") catch return .zero;
|
|
env_array.capacity = env_array.items.len;
|
|
}
|
|
|
|
inline for (0..stdio.len) |fd_index| {
|
|
if (stdio[fd_index].canUseMemfd(is_sync, fd_index > 0 and maxBuffer != null)) {
|
|
if (stdio[fd_index].useMemfd(fd_index)) {
|
|
jsc_vm.counters.mark(.spawn_memfd);
|
|
}
|
|
}
|
|
}
|
|
var should_close_memfd = Environment.isLinux;
|
|
|
|
defer {
|
|
if (should_close_memfd) {
|
|
inline for (0..stdio.len) |fd_index| {
|
|
if (stdio[fd_index] == .memfd) {
|
|
stdio[fd_index].memfd.close();
|
|
stdio[fd_index] = .ignore;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//"NODE_CHANNEL_FD=" is 16 bytes long, 15 bytes for the number, and 1 byte for the null terminator should be enough/safe
|
|
var ipc_env_buf: [32]u8 = undefined;
|
|
if (!is_sync) if (maybe_ipc_mode) |ipc_mode| {
|
|
// IPC is currently implemented in a very limited way.
|
|
//
|
|
// Node lets you pass as many fds as you want, they all become be sockets; then, IPC is just a special
|
|
// runtime-owned version of "pipe" (in which pipe is a misleading name since they're bidirectional sockets).
|
|
//
|
|
// Bun currently only supports three fds: stdin, stdout, and stderr, which are all unidirectional
|
|
//
|
|
// And then one fd is assigned specifically and only for IPC. If the user dont specify it, we add one (default: 3).
|
|
//
|
|
// When Bun.spawn() is given an `.ipc` callback, it enables IPC as follows:
|
|
env_array.ensureUnusedCapacity(allocator, 3) catch |err| return globalThis.throwError(err, "in Bun.spawn") catch return .zero;
|
|
const ipc_fd: i32 = brk: {
|
|
if (ipc_channel == -1) {
|
|
// If the user didn't specify an IPC channel, we need to add one
|
|
ipc_channel = @intCast(extra_fds.items.len);
|
|
var ipc_extra_fd_default = Stdio{ .ipc = {} };
|
|
const fd: i32 = ipc_channel + 3;
|
|
switch (ipc_extra_fd_default.asSpawnOption(fd)) {
|
|
.result => |opt| {
|
|
try extra_fds.append(opt);
|
|
},
|
|
.err => |e| {
|
|
return e.throwJS(globalThis);
|
|
},
|
|
}
|
|
break :brk fd;
|
|
} else {
|
|
break :brk @intCast(ipc_channel + 3);
|
|
}
|
|
};
|
|
|
|
const pipe_env = std.fmt.bufPrintZ(
|
|
&ipc_env_buf,
|
|
"NODE_CHANNEL_FD={d}",
|
|
.{ipc_fd},
|
|
) catch {
|
|
return globalThis.throwOutOfMemory();
|
|
};
|
|
env_array.appendAssumeCapacity(pipe_env);
|
|
|
|
env_array.appendAssumeCapacity(switch (ipc_mode) {
|
|
inline else => |t| "NODE_CHANNEL_SERIALIZATION_MODE=" ++ @tagName(t),
|
|
});
|
|
};
|
|
|
|
try env_array.append(allocator, null);
|
|
try argv.append(null);
|
|
|
|
if (comptime is_sync) {
|
|
for (&stdio, 0..) |*io, i| {
|
|
io.toSync(@truncate(i));
|
|
}
|
|
}
|
|
|
|
// If the whole thread is supposed to do absolutely nothing while waiting,
|
|
// we can block the thread which reduces CPU usage.
|
|
//
|
|
// That means:
|
|
// - No maximum buffer
|
|
// - No timeout
|
|
// - No abort signal
|
|
// - No stdin, stdout, stderr pipes
|
|
// - No extra fds
|
|
// - No auto killer (for tests)
|
|
// - No execution time limit (for tests)
|
|
// - No IPC
|
|
// - No inspector (since they might want to press pause or step)
|
|
const can_block_entire_thread_to_reduce_cpu_usage_in_fast_path = (comptime Environment.isPosix and is_sync) and
|
|
abort_signal == null and
|
|
timeout == null and
|
|
maxBuffer == null and
|
|
!stdio[0].isPiped() and
|
|
!stdio[1].isPiped() and
|
|
!stdio[2].isPiped() and
|
|
extra_fds.items.len == 0 and
|
|
!jsc_vm.auto_killer.enabled and
|
|
!jsc_vm.jsc_vm.hasExecutionTimeLimit() and
|
|
!jsc_vm.isInspectorEnabled() and
|
|
!bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_DISABLE_SPAWNSYNC_FAST_PATH);
|
|
|
|
const spawn_options = bun.spawn.SpawnOptions{
|
|
.cwd = cwd,
|
|
.detached = detached,
|
|
.stdin = switch (stdio[0].asSpawnOption(0)) {
|
|
.result => |opt| opt,
|
|
.err => |e| return e.throwJS(globalThis),
|
|
},
|
|
.stdout = switch (stdio[1].asSpawnOption(1)) {
|
|
.result => |opt| opt,
|
|
.err => |e| return e.throwJS(globalThis),
|
|
},
|
|
.stderr = switch (stdio[2].asSpawnOption(2)) {
|
|
.result => |opt| opt,
|
|
.err => |e| return e.throwJS(globalThis),
|
|
},
|
|
.extra_fds = extra_fds.items,
|
|
.argv0 = argv0,
|
|
.can_block_entire_thread_to_reduce_cpu_usage_in_fast_path = can_block_entire_thread_to_reduce_cpu_usage_in_fast_path,
|
|
|
|
.windows = if (Environment.isWindows) .{
|
|
.hide_window = windows_hide,
|
|
.verbatim_arguments = windows_verbatim_arguments,
|
|
.loop = jsc.EventLoopHandle.init(jsc_vm),
|
|
},
|
|
};
|
|
|
|
var spawned = switch (bun.spawn.spawnProcess(
|
|
&spawn_options,
|
|
@ptrCast(argv.items.ptr),
|
|
@ptrCast(env_array.items.ptr),
|
|
) catch |err| switch (err) {
|
|
error.EMFILE, error.ENFILE => {
|
|
spawn_options.deinit();
|
|
const display_path: [:0]const u8 = if (argv.items.len > 0 and argv.items[0] != null)
|
|
std.mem.sliceTo(argv.items[0].?, 0)
|
|
else
|
|
"";
|
|
var systemerror = bun.sys.Error.fromCode(if (err == error.EMFILE) .MFILE else .NFILE, .posix_spawn).withPath(display_path).toSystemError();
|
|
systemerror.errno = if (err == error.EMFILE) -bun.sys.UV_E.MFILE else -bun.sys.UV_E.NFILE;
|
|
return globalThis.throwValue(systemerror.toErrorInstance(globalThis));
|
|
},
|
|
else => {
|
|
spawn_options.deinit();
|
|
return globalThis.throwError(err, ": failed to spawn process") catch return .zero;
|
|
},
|
|
}) {
|
|
.err => |err| {
|
|
spawn_options.deinit();
|
|
switch (err.getErrno()) {
|
|
.ACCES, .NOENT, .PERM, .ISDIR, .NOTDIR => |errno| {
|
|
const display_path: [:0]const u8 = if (argv.items.len > 0 and argv.items[0] != null)
|
|
std.mem.sliceTo(argv.items[0].?, 0)
|
|
else
|
|
"";
|
|
if (display_path.len > 0) {
|
|
var systemerror = err.withPath(display_path).toSystemError();
|
|
if (errno == .NOENT) systemerror.errno = -bun.sys.UV_E.NOENT;
|
|
return globalThis.throwValue(systemerror.toErrorInstance(globalThis));
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
|
|
return globalThis.throwValue(err.toJS(globalThis));
|
|
},
|
|
.result => |result| result,
|
|
};
|
|
|
|
const loop = jsc_vm.eventLoop();
|
|
|
|
const process = spawned.toProcess(loop, is_sync);
|
|
|
|
var subprocess = bun.new(Subprocess, .{
|
|
.ref_count = .init(),
|
|
.globalThis = globalThis,
|
|
.process = process,
|
|
.pid_rusage = null,
|
|
.stdin = .{ .ignore = {} },
|
|
.stdout = .{ .ignore = {} },
|
|
.stderr = .{ .ignore = {} },
|
|
.stdio_pipes = .{},
|
|
.ipc_data = null,
|
|
.flags = .{
|
|
.is_sync = is_sync,
|
|
},
|
|
.killSignal = undefined,
|
|
});
|
|
|
|
const posix_ipc_fd = if (Environment.isPosix and !is_sync and maybe_ipc_mode != null)
|
|
spawned.extra_pipes.items[@intCast(ipc_channel)]
|
|
else
|
|
bun.invalid_fd;
|
|
|
|
MaxBuf.createForSubprocess(subprocess, &subprocess.stderr_maxbuf, maxBuffer);
|
|
MaxBuf.createForSubprocess(subprocess, &subprocess.stdout_maxbuf, maxBuffer);
|
|
|
|
var promise_for_stream: jsc.JSValue = .zero;
|
|
|
|
// When run synchronously, subprocess isn't garbage collected
|
|
subprocess.* = Subprocess{
|
|
.globalThis = globalThis,
|
|
.process = process,
|
|
.pid_rusage = null,
|
|
.stdin = Writable.init(
|
|
&stdio[0],
|
|
loop,
|
|
subprocess,
|
|
spawned.stdin,
|
|
&promise_for_stream,
|
|
) catch {
|
|
subprocess.deref();
|
|
return globalThis.throwOutOfMemory();
|
|
},
|
|
.stdout = Readable.init(
|
|
stdio[1],
|
|
loop,
|
|
subprocess,
|
|
spawned.stdout,
|
|
jsc_vm.allocator,
|
|
subprocess.stdout_maxbuf,
|
|
is_sync,
|
|
),
|
|
.stderr = Readable.init(
|
|
stdio[2],
|
|
loop,
|
|
subprocess,
|
|
spawned.stderr,
|
|
jsc_vm.allocator,
|
|
subprocess.stderr_maxbuf,
|
|
is_sync,
|
|
),
|
|
// 1. JavaScript.
|
|
// 2. Process.
|
|
.ref_count = .initExactRefs(2),
|
|
.stdio_pipes = spawned.extra_pipes.moveToUnmanaged(),
|
|
.ipc_data = if (!is_sync and comptime Environment.isWindows)
|
|
if (maybe_ipc_mode) |ipc_mode| ( //
|
|
.init(ipc_mode, .{ .subprocess = subprocess }, .uninitialized) //
|
|
) else null
|
|
else
|
|
null,
|
|
|
|
.flags = .{
|
|
.is_sync = is_sync,
|
|
},
|
|
.killSignal = killSignal,
|
|
.stderr_maxbuf = subprocess.stderr_maxbuf,
|
|
.stdout_maxbuf = subprocess.stdout_maxbuf,
|
|
};
|
|
|
|
subprocess.process.setExitHandler(subprocess);
|
|
|
|
promise_for_stream.ensureStillAlive();
|
|
subprocess.flags.is_stdin_a_readable_stream = promise_for_stream != .zero;
|
|
|
|
if (promise_for_stream != .zero and !globalThis.hasException()) {
|
|
if (promise_for_stream.toError()) |err| {
|
|
_ = globalThis.throwValue(err) catch {};
|
|
}
|
|
}
|
|
|
|
if (globalThis.hasException()) {
|
|
const err = globalThis.takeException(error.JSError);
|
|
// Ensure we kill the process so we don't leave things in an unexpected state.
|
|
_ = subprocess.tryKill(subprocess.killSignal);
|
|
|
|
if (globalThis.hasException()) {
|
|
return error.JSError;
|
|
}
|
|
|
|
return globalThis.throwValue(err);
|
|
}
|
|
|
|
var posix_ipc_info: if (Environment.isPosix) IPC.Socket else void = undefined;
|
|
if (Environment.isPosix and !is_sync) {
|
|
if (maybe_ipc_mode) |mode| {
|
|
if (uws.us_socket_t.fromFd(
|
|
jsc_vm.rareData().spawnIPCContext(jsc_vm),
|
|
@sizeOf(*IPC.SendQueue),
|
|
posix_ipc_fd.cast(),
|
|
1,
|
|
)) |socket| {
|
|
subprocess.ipc_data = .init(mode, .{ .subprocess = subprocess }, .uninitialized);
|
|
posix_ipc_info = IPC.Socket.from(socket);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (subprocess.ipc_data) |*ipc_data| {
|
|
if (Environment.isPosix) {
|
|
if (posix_ipc_info.ext(*IPC.SendQueue)) |ctx| {
|
|
ctx.* = &subprocess.ipc_data.?;
|
|
subprocess.ipc_data.?.socket = .{ .open = posix_ipc_info };
|
|
}
|
|
} else {
|
|
if (ipc_data.windowsConfigureServer(
|
|
subprocess.stdio_pipes.items[@intCast(ipc_channel)].buffer,
|
|
).asErr()) |err| {
|
|
subprocess.deref();
|
|
return globalThis.throwValue(err.toJS(globalThis));
|
|
}
|
|
subprocess.stdio_pipes.items[@intCast(ipc_channel)] = .unavailable;
|
|
}
|
|
ipc_data.writeVersionPacket(globalThis);
|
|
}
|
|
|
|
if (subprocess.stdin == .pipe and promise_for_stream == .zero) {
|
|
subprocess.stdin.pipe.signal = jsc.WebCore.streams.Signal.init(&subprocess.stdin);
|
|
}
|
|
|
|
const out = if (comptime !is_sync)
|
|
subprocess.toJS(globalThis)
|
|
else
|
|
JSValue.zero;
|
|
subprocess.this_jsvalue = out;
|
|
|
|
var send_exit_notification = false;
|
|
|
|
// This must go before other things happen so that the exit handler is registered before onProcessExit can potentially be called.
|
|
if (timeout) |timeout_val| {
|
|
subprocess.event_loop_timer.next = bun.timespec.msFromNow(timeout_val);
|
|
globalThis.bunVM().timer.insert(&subprocess.event_loop_timer);
|
|
subprocess.setEventLoopTimerRefd(true);
|
|
}
|
|
|
|
if (comptime !is_sync) {
|
|
bun.debugAssert(out != .zero);
|
|
|
|
if (on_exit_callback.isCell()) {
|
|
jsc.Codegen.JSSubprocess.onExitCallbackSetCached(out, globalThis, on_exit_callback);
|
|
}
|
|
if (on_disconnect_callback.isCell()) {
|
|
jsc.Codegen.JSSubprocess.onDisconnectCallbackSetCached(out, globalThis, on_disconnect_callback);
|
|
}
|
|
if (ipc_callback.isCell()) {
|
|
jsc.Codegen.JSSubprocess.ipcCallbackSetCached(out, globalThis, ipc_callback);
|
|
}
|
|
|
|
if (stdio[0] == .readable_stream) {
|
|
jsc.Codegen.JSSubprocess.stdinSetCached(out, globalThis, stdio[0].readable_stream.value);
|
|
}
|
|
|
|
switch (subprocess.process.watch()) {
|
|
.result => {},
|
|
.err => {
|
|
send_exit_notification = true;
|
|
lazy = false;
|
|
},
|
|
}
|
|
}
|
|
|
|
defer {
|
|
if (send_exit_notification) {
|
|
if (subprocess.process.hasExited()) {
|
|
// process has already exited, we called wait4(), but we did not call onProcessExit()
|
|
subprocess.process.onExit(subprocess.process.status, &std.mem.zeroes(Rusage));
|
|
} else {
|
|
// process has already exited, but we haven't called wait4() yet
|
|
// https://cs.github.com/libuv/libuv/blob/b00d1bd225b602570baee82a6152eaa823a84fa6/src/unix/process.c#L1007
|
|
subprocess.process.wait(is_sync);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (subprocess.stdin == .buffer) {
|
|
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.stderr == .pipe) {
|
|
subprocess.stderr.pipe.start(subprocess, loop).assert();
|
|
|
|
if ((is_sync or !lazy) and subprocess.stderr == .pipe) {
|
|
subprocess.stderr.pipe.readAll();
|
|
}
|
|
}
|
|
|
|
should_close_memfd = false;
|
|
|
|
if (comptime !is_sync) {
|
|
// Once everything is set up, we can add the abort listener
|
|
// Adding the abort listener may call the onAbortSignal callback immediately if it was already aborted
|
|
// Therefore, we must do this at the very end.
|
|
if (abort_signal) |signal| {
|
|
signal.pendingActivityRef();
|
|
subprocess.abort_signal = signal.addListener(subprocess, onAbortSignal);
|
|
abort_signal = null;
|
|
}
|
|
if (!subprocess.process.hasExited()) {
|
|
jsc_vm.onSubprocessSpawn(subprocess.process);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
comptime bun.assert(is_sync);
|
|
|
|
if (can_block_entire_thread_to_reduce_cpu_usage_in_fast_path) {
|
|
jsc_vm.counters.mark(.spawnSync_blocking);
|
|
const debug_timer = Output.DebugTimer.start();
|
|
subprocess.process.wait(true);
|
|
log("spawnSync fast path took {}", .{debug_timer});
|
|
|
|
// watchOrReap will handle the already exited case for us.
|
|
}
|
|
|
|
switch (subprocess.process.watchOrReap()) {
|
|
.result => {
|
|
// Once everything is set up, we can add the abort listener
|
|
// Adding the abort listener may call the onAbortSignal callback immediately if it was already aborted
|
|
// Therefore, we must do this at the very end.
|
|
if (abort_signal) |signal| {
|
|
signal.pendingActivityRef();
|
|
subprocess.abort_signal = signal.addListener(subprocess, onAbortSignal);
|
|
abort_signal = null;
|
|
}
|
|
},
|
|
.err => {
|
|
subprocess.process.wait(true);
|
|
},
|
|
}
|
|
|
|
if (!subprocess.process.hasExited()) {
|
|
jsc_vm.onSubprocessSpawn(subprocess.process);
|
|
}
|
|
|
|
// We cannot release heap access while JS is running
|
|
{
|
|
const old_vm = jsc_vm.uwsLoop().internal_loop_data.jsc_vm;
|
|
jsc_vm.uwsLoop().internal_loop_data.jsc_vm = null;
|
|
defer {
|
|
jsc_vm.uwsLoop().internal_loop_data.jsc_vm = old_vm;
|
|
}
|
|
while (subprocess.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();
|
|
}
|
|
}
|
|
|
|
subprocess.updateHasPendingActivity();
|
|
|
|
const signalCode = subprocess.getSignalCode(globalThis);
|
|
const exitCode = subprocess.getExitCode(globalThis);
|
|
const stdout = try subprocess.stdout.toBufferedValue(globalThis);
|
|
const stderr = try subprocess.stderr.toBufferedValue(globalThis);
|
|
const resource_usage: JSValue = if (!globalThis.hasException()) try subprocess.createResourceUsageObject(globalThis) else .zero;
|
|
const exitedDueToTimeout = subprocess.event_loop_timer.state == .FIRED;
|
|
const exitedDueToMaxBuffer = subprocess.exited_due_to_maxbuf;
|
|
const resultPid = jsc.JSValue.jsNumberFromInt32(subprocess.pid());
|
|
subprocess.finalize();
|
|
|
|
if (globalThis.hasException()) {
|
|
// e.g. a termination exception.
|
|
return .zero;
|
|
}
|
|
|
|
const sync_value = jsc.JSValue.createEmptyObject(globalThis, 5 + @as(usize, @intFromBool(!signalCode.isEmptyOrUndefinedOrNull())));
|
|
sync_value.put(globalThis, jsc.ZigString.static("exitCode"), exitCode);
|
|
if (!signalCode.isEmptyOrUndefinedOrNull()) {
|
|
sync_value.put(globalThis, jsc.ZigString.static("signalCode"), signalCode);
|
|
}
|
|
sync_value.put(globalThis, jsc.ZigString.static("stdout"), stdout);
|
|
sync_value.put(globalThis, jsc.ZigString.static("stderr"), stderr);
|
|
sync_value.put(globalThis, jsc.ZigString.static("success"), JSValue.jsBoolean(exitCode.isInt32() and exitCode.asInt32() == 0));
|
|
sync_value.put(globalThis, jsc.ZigString.static("resourceUsage"), resource_usage);
|
|
if (timeout != null) sync_value.put(globalThis, jsc.ZigString.static("exitedDueToTimeout"), if (exitedDueToTimeout) .true else .false);
|
|
if (maxBuffer != null) sync_value.put(globalThis, jsc.ZigString.static("exitedDueToMaxBuffer"), if (exitedDueToMaxBuffer != null) .true else .false);
|
|
sync_value.put(globalThis, jsc.ZigString.static("pid"), resultPid);
|
|
|
|
return sync_value;
|
|
}
|
|
|
|
fn throwCommandNotFound(globalThis: *jsc.JSGlobalObject, command: []const u8) bun.JSError {
|
|
const err = jsc.SystemError{
|
|
.message = bun.handleOom(bun.String.createFormat("Executable not found in $PATH: \"{s}\"", .{command})),
|
|
.code = bun.String.static("ENOENT"),
|
|
.errno = -bun.sys.UV_E.NOENT,
|
|
.path = bun.String.cloneUTF8(command),
|
|
};
|
|
return globalThis.throwValue(err.toErrorInstance(globalThis));
|
|
}
|
|
|
|
pub fn handleIPCMessage(
|
|
this: *Subprocess,
|
|
message: IPC.DecodedIPCMessage,
|
|
handle: jsc.JSValue,
|
|
) 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", .{});
|
|
const this_jsvalue = this.this_jsvalue;
|
|
defer this_jsvalue.ensureStillAlive();
|
|
if (this_jsvalue != .zero) {
|
|
if (jsc.Codegen.JSSubprocess.ipcCallbackGetCached(this_jsvalue)) |cb| {
|
|
const globalThis = this.globalThis;
|
|
globalThis.bunVM().eventLoop().runCallback(
|
|
cb,
|
|
globalThis,
|
|
this_jsvalue,
|
|
&[_]JSValue{ data, this_jsvalue, handle },
|
|
);
|
|
}
|
|
}
|
|
},
|
|
.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", .{});
|
|
const this_jsvalue = this.this_jsvalue;
|
|
defer this_jsvalue.ensureStillAlive();
|
|
const globalThis = this.globalThis;
|
|
this.updateHasPendingActivity();
|
|
|
|
if (this_jsvalue != .zero) {
|
|
// Avoid keeping the callback alive longer than necessary
|
|
jsc.Codegen.JSSubprocess.ipcCallbackSetCached(this_jsvalue, globalThis, .zero);
|
|
|
|
// Call the onDisconnectCallback if it exists and prevent it from being kept alive longer than necessary
|
|
if (consumeOnDisconnectCallback(this_jsvalue, globalThis)) |callback| {
|
|
globalThis.bunVM().eventLoop().runCallback(callback, globalThis, this_jsvalue, &.{.true});
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn ipc(this: *Subprocess) ?*IPC.SendQueue {
|
|
return &(this.ipc_data orelse return null);
|
|
}
|
|
pub fn getGlobalThis(this: *Subprocess) ?*jsc.JSGlobalObject {
|
|
return this.globalThis;
|
|
}
|
|
|
|
const IPClog = Output.scoped(.IPC, .visible);
|
|
|
|
pub const StdioResult = if (Environment.isWindows) bun.spawn.WindowsSpawnResult.StdioResult else ?bun.FileDescriptor;
|
|
pub const Writable = @import("./subprocess/Writable.zig").Writable;
|
|
|
|
pub const MaxBuf = bun.io.MaxBuf;
|
|
|
|
const string = []const u8;
|
|
|
|
const IPC = @import("../../ipc.zig");
|
|
const node_cluster_binding = @import("../../node/node_cluster_binding.zig");
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const bun = @import("bun");
|
|
const Async = bun.Async;
|
|
const Environment = bun.Environment;
|
|
const Output = bun.Output;
|
|
const default_allocator = bun.default_allocator;
|
|
const strings = bun.strings;
|
|
const uws = bun.uws;
|
|
const webcore = bun.webcore;
|
|
const which = bun.which;
|
|
const CowString = bun.ptr.CowString;
|
|
|
|
const jsc = bun.jsc;
|
|
const JSGlobalObject = jsc.JSGlobalObject;
|
|
const JSValue = jsc.JSValue;
|
|
|
|
const PosixSpawn = bun.spawn;
|
|
const Process = bun.spawn.Process;
|
|
const Rusage = bun.spawn.Rusage;
|
|
const Stdio = bun.spawn.Stdio;
|
|
|
|
const windows = bun.windows;
|
|
const uv = windows.libuv;
|