aboutsummaryrefslogtreecommitdiff
path: root/src/cli/run_command.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/cli/run_command.zig')
-rw-r--r--src/cli/run_command.zig349
1 files changed, 343 insertions, 6 deletions
diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig
index 19046e292..1c5f3e1ec 100644
--- a/src/cli/run_command.zig
+++ b/src/cli/run_command.zig
@@ -9,6 +9,8 @@ const stringZ = bun.stringZ;
const default_allocator = bun.default_allocator;
const C = bun.C;
const std = @import("std");
+const uws = @import("../deps/uws.zig");
+const JSC = bun.JSC;
const lex = bun.js_lexer;
const logger = @import("root").bun.logger;
@@ -41,6 +43,9 @@ const NpmArgs = struct {
const yarn_commands: []u64 = @import("./list-of-yarn-commands.zig").all_yarn_commands;
const ShellCompletions = @import("./shell_completions.zig");
+const PosixSpawn = @import("../bun.js/api/bun/spawn.zig").PosixSpawn;
+
+const PackageManager = @import("../install/install.zig").PackageManager;
pub const RunCommand = struct {
const shells_to_search = &[_]string{
@@ -49,7 +54,7 @@ pub const RunCommand = struct {
"zsh",
};
- pub fn findShell(PATH: string, cwd: string) ?string {
+ pub fn findShell(PATH: string, cwd: string) ?stringZ {
if (comptime Environment.isWindows) {
return "C:\\Windows\\System32\\cmd.exe";
}
@@ -225,7 +230,338 @@ pub const RunCommand = struct {
const log = Output.scoped(.RUN, false);
- pub fn runPackageScript(
+ pub const PostinstallSubprocess = struct {
+ script_name: []const u8,
+ package_name: []const u8,
+
+ finished_fds: u8 = 0,
+
+ output_buffer: bun.ByteList,
+ pid_poll: *JSC.FilePoll,
+ waitpid_result: ?PosixSpawn.WaitPidResult,
+ stdout_poll: *JSC.FilePoll,
+ stderr_poll: *JSC.FilePoll,
+ package_manager: *PackageManager,
+
+ /// A "nothing" struct that lets us reuse the same pointer
+ /// but with a different tag for the file poll
+ pub const PidPollData = struct { process: PostinstallSubprocess };
+
+ pub fn init(
+ manager: *PackageManager,
+ script_name: []const u8,
+ package_name: []const u8,
+ stdout_fd: bun.FileDescriptor,
+ stderr_fd: bun.FileDescriptor,
+ pid_fd: bun.FileDescriptor,
+ ) !?*PostinstallSubprocess {
+ // TODO: this doesnt handle some cleanup edge cases on error
+ var this = try manager.allocator.create(PostinstallSubprocess);
+ errdefer this.deinit(manager.allocator);
+
+ this.* = .{
+ .package_name = package_name,
+ .script_name = script_name,
+ .package_manager = manager,
+ .waitpid_result = null,
+ .output_buffer = .{},
+ .pid_poll = JSC.FilePoll.initWithPackageManager(
+ manager,
+ pid_fd,
+ .{},
+ @as(*PidPollData, @ptrCast(this)),
+ ),
+ .stdout_poll = JSC.FilePoll.initWithPackageManager(manager, stdout_fd, .{}, this),
+ .stderr_poll = JSC.FilePoll.initWithPackageManager(manager, stderr_fd, .{}, this),
+ };
+
+ try this.stdout_poll.register(manager.uws_event_loop, .readable, false).throw();
+ try this.stderr_poll.register(manager.uws_event_loop, .readable, false).throw();
+
+ switch (this.pid_poll.register(
+ manager.uws_event_loop,
+ .process,
+ true,
+ )) {
+ .result => {},
+ .err => |err| {
+ // Sometimes the pid poll can fail to register if the process exits
+ // between posix_spawn() and pid_poll.register(), but it is unlikely.
+ // Any other error is unexpected here.
+ if (err.getErrno() != .SRCH) {
+ @panic("This shouldn't happen. Could not register pid poll");
+ }
+
+ this.package_manager.pending_tasks -= 1;
+ this.onProcessUpdate(0);
+ return null;
+ },
+ }
+
+ return this;
+ }
+
+ pub fn onOutputUpdate(this: *PostinstallSubprocess, size: i64, fd: bun.FileDescriptor) void {
+ var needed_capacity = this.output_buffer.len + @as(u32, @intCast(size));
+ _ = needed_capacity;
+ this.output_buffer.ensureUnusedCapacity(this.package_manager.allocator, @intCast(size)) catch @panic("Failed to allocate memory for output buffer");
+
+ if (size == 0) {
+ this.finished_fds += 1;
+ if (this.waitpid_result) |result| {
+ if (this.finished_fds == 2) {
+ this.onResult(result);
+ }
+ }
+ return;
+ }
+
+ var remaining = size;
+ while (remaining > 0) {
+ const n: u32 = @truncate(std.os.read(
+ fd,
+ this.output_buffer.ptr[this.output_buffer.len..this.output_buffer.cap],
+ ) catch return);
+ this.output_buffer.len += n;
+ remaining -|= n;
+ }
+ }
+
+ pub fn printOutput(this: *PostinstallSubprocess) void {
+ Output.errorWriter().writeAll(this.output_buffer.slice()) catch {};
+ }
+
+ pub fn onProcessUpdate(this: *PostinstallSubprocess, _: i64) void {
+ switch (PosixSpawn.waitpid(this.pid_poll.fileDescriptor(), std.os.W.NOHANG)) {
+ .err => |err| {
+ Output.prettyErrorln("<r><red>error<r>: Failed to run <b>{s}<r> script from \"<b>{s}<r>\" due to error <b>{d} {s}<r>", .{ this.script_name, this.package_name, err.errno, @tagName(err.getErrno()) });
+ Output.flush();
+ this.package_manager.pending_tasks -= 1;
+ },
+ .result => |result| this.onResult(result),
+ }
+ }
+
+ pub fn onResult(this: *PostinstallSubprocess, result: PosixSpawn.WaitPidResult) void {
+ if (result.pid == 0) {
+ Output.prettyErrorln("<r><red>error<r>: Failed to run <b>{s}<r> script from \"<b>{s}<r>\" due to error <b>{d} {s}<r>", .{ this.script_name, this.package_name, 0, "Unknown" });
+ Output.flush();
+
+ this.package_manager.pending_tasks -= 1;
+ return;
+ }
+ if (std.os.W.IFEXITED(result.status)) {
+ defer this.deinit(this.package_manager.allocator);
+
+ const code = std.os.W.EXITSTATUS(result.status);
+ if (code > 0) {
+ if (this.finished_fds < 2) {
+ this.waitpid_result = result;
+ return;
+ }
+ this.printOutput();
+ Output.prettyErrorln("<r><red>error<r><d>:<r> <b>{s}<r> script from \"<b>{s}<r>\" exited with {any}<r>", .{ this.script_name, this.package_name, bun.SignalCode.from(code) });
+ Output.flush();
+ Global.exit(code);
+ }
+
+ this.package_manager.pending_tasks -= 1;
+ return;
+ }
+ if (std.os.W.IFSIGNALED(result.status)) {
+ const signal = std.os.W.TERMSIG(result.status);
+
+ if (this.finished_fds < 2) {
+ this.waitpid_result = result;
+ return;
+ }
+ this.printOutput();
+
+ Output.prettyErrorln("<r><red>error<r><d>:<r> <b>{s}<r> script from \"<b>{s}<r>\" exited with {any}<r>", .{ this.script_name, this.package_name, bun.SignalCode.from(signal) });
+ Output.flush();
+ Global.exit(1);
+ }
+ if (std.os.W.IFSTOPPED(result.status)) {
+ const signal = std.os.W.STOPSIG(result.status);
+
+ if (this.finished_fds < 2) {
+ this.waitpid_result = result;
+ return;
+ }
+ this.printOutput();
+
+ Output.prettyErrorln("<r><red>error<r><d>:<r> <b>{s}<r> script from \"<b>{s}<r>\" was stopped by signal {any}<r>", .{ this.script_name, this.package_name, bun.SignalCode.from(signal) });
+ Output.flush();
+ Global.exit(1);
+ }
+ }
+
+ pub fn deinit(this: *PostinstallSubprocess, alloc: std.mem.Allocator) void {
+ _ = this.stdout_poll.unregister(this.package_manager.uws_event_loop, false);
+ _ = this.stderr_poll.unregister(this.package_manager.uws_event_loop, false);
+ _ = this.pid_poll.unregister(this.package_manager.uws_event_loop, false);
+
+ _ = bun.sys.close(this.stdout_poll.fileDescriptor());
+ _ = bun.sys.close(this.stderr_poll.fileDescriptor());
+ _ = bun.sys.close(this.pid_poll.fileDescriptor());
+
+ alloc.destroy(this);
+ }
+ };
+
+ inline fn spawnScript(
+ ctx: *PackageManager,
+ name: string,
+ package_name: string,
+ cwd: string,
+ env: *DotEnv.Loader,
+ argv: [*:null]?[*:0]const u8,
+ ) !?*PostinstallSubprocess {
+ var flags: i32 = bun.C.POSIX_SPAWN_SETSIGDEF | bun.C.POSIX_SPAWN_SETSIGMASK;
+ if (comptime Environment.isMac) {
+ flags |= bun.C.POSIX_SPAWN_CLOEXEC_DEFAULT;
+ }
+
+ var attr = try PosixSpawn.Attr.init();
+ defer attr.deinit();
+ try attr.set(@intCast(flags));
+ try attr.resetSignals();
+
+ var actions = try PosixSpawn.Actions.init();
+ defer actions.deinit();
+ try actions.openZ(bun.STDIN_FD, "/dev/null", std.os.O.RDONLY, 0o664);
+
+ // Have both stdout and stderr write to the same buffer
+ const fdsOut = try std.os.pipe2(0);
+ try actions.dup2(fdsOut[1], bun.STDOUT_FD);
+
+ const fdsErr = try std.os.pipe2(0);
+ try actions.dup2(fdsErr[1], bun.STDERR_FD);
+
+ try actions.chdir(cwd);
+
+ var arena = bun.ArenaAllocator.init(ctx.allocator);
+ defer arena.deinit();
+
+ const pid = brk: {
+ defer {
+ _ = bun.sys.close(fdsOut[1]);
+ _ = bun.sys.close(fdsErr[1]);
+ }
+ switch (PosixSpawn.spawnZ(
+ argv[0].?,
+ actions,
+ attr,
+ argv,
+ try env.map.createNullDelimitedEnvMap(arena.allocator()),
+ )) {
+ .err => |err| {
+ Output.prettyErrorln("<r><red>error<r>: Failed to spawn script <b>{s}<r> due to error <b>{d} {s}<r>", .{ name, err.errno, @tagName(err.getErrno()) });
+ Output.flush();
+ return null;
+ },
+ .result => |pid| break :brk pid,
+ }
+ };
+
+ const pidfd: std.os.fd_t = brk: {
+ if (!Environment.isLinux) {
+ break :brk pid;
+ }
+
+ const kernel = @import("../analytics.zig").GenerateHeader.GeneratePlatform.kernelVersion();
+
+ // pidfd_nonblock only supported in 5.10+
+ const pidfd_flags: u32 = if (kernel.orderWithoutTag(.{ .major = 5, .minor = 10, .patch = 0 }).compare(.gte))
+ std.os.O.NONBLOCK
+ else
+ 0;
+
+ const fd = std.os.linux.pidfd_open(
+ pid,
+ pidfd_flags,
+ );
+
+ switch (std.os.linux.getErrno(fd)) {
+ .SUCCESS => break :brk @as(std.os.fd_t, @intCast(fd)),
+ else => |err| {
+ var status: u32 = 0;
+ // ensure we don't leak the child process on error
+ _ = std.os.linux.waitpid(pid, &status, 0);
+
+ Output.prettyErrorln("<r><red>error<r>: Failed to spawn script <b>{s}<r> due to error <b>{d} {s}<r>", .{ name, err, @tagName(err) });
+ Output.flush();
+
+ return null;
+ },
+ }
+ };
+
+ return try PostinstallSubprocess.init(ctx, name, package_name, fdsOut[0], fdsErr[0], pidfd);
+ }
+
+ /// Used to execute postinstall scripts
+ pub fn spawnPackageScript(
+ ctx: *PackageManager,
+ original_script: string,
+ name: string,
+ package_name: string,
+ cwd: string,
+ passthrough: []const string,
+ silent: bool,
+ ) !void {
+ const env = ctx.env;
+ const shell_bin = findShell(env.map.get("PATH") orelse "", cwd) orelse return error.MissingShell;
+
+ var script = original_script;
+ var copy_script = try std.ArrayList(u8).initCapacity(ctx.allocator, script.len + 1);
+
+ // We're going to do this slowly.
+ // Find exact matches of yarn, pnpm, npm
+
+ try replacePackageManagerRun(&copy_script, script);
+ try copy_script.append(0);
+
+ var combined_script: [:0]u8 = copy_script.items[0 .. copy_script.items.len - 1 :0];
+
+ log("Script from pkg \"{s}\" : \"{s}\"", .{ package_name, combined_script });
+
+ if (passthrough.len > 0) {
+ var combined_script_len = script.len;
+ for (passthrough) |p| {
+ combined_script_len += p.len + 1;
+ }
+ var combined_script_buf = try ctx.allocator.allocSentinel(u8, combined_script_len, 0);
+ bun.copy(u8, combined_script_buf, script);
+ var remaining_script_buf = combined_script_buf[script.len..];
+ for (passthrough) |part| {
+ var p = part;
+ remaining_script_buf[0] = ' ';
+ bun.copy(u8, remaining_script_buf[1..], p);
+ remaining_script_buf = remaining_script_buf[p.len + 1 ..];
+ }
+ combined_script = combined_script_buf;
+ }
+
+ if (!silent) {
+ Output.prettyErrorln("<r><d><magenta>$<r> <d><b>{s}<r>", .{combined_script});
+ Output.flush();
+ }
+
+ var argv = try ctx.allocator.allocSentinel(?[*:0]const u8, 3, null);
+ defer ctx.allocator.free(argv);
+ argv[0] = shell_bin;
+ argv[1] = "-c";
+ argv[2] = combined_script;
+
+ _ = spawnScript(ctx, name, package_name, cwd, env, argv) catch |err| {
+ Output.prettyErrorln("<r><red>error<r>: Failed to run script <b>{s}<r> due to error <b>{s}<r>", .{ name, @errorName(err) });
+ Output.flush();
+ return;
+ };
+ }
+
+ pub fn runPackageScriptForeground(
allocator: std.mem.Allocator,
original_script: string,
name: string,
@@ -323,6 +659,7 @@ pub const RunCommand = struct {
return true;
}
+
pub fn runBinary(
ctx: Command.Context,
executable: []const u8,
@@ -1046,11 +1383,11 @@ pub const RunCommand = struct {
else => {
if (scripts.get(script_name_to_search)) |script_content| {
// allocate enough to hold "post${scriptname}"
-
var temp_script_buffer = try std.fmt.allocPrint(ctx.allocator, "ppre{s}", .{script_name_to_search});
+ defer ctx.allocator.free(temp_script_buffer);
if (scripts.get(temp_script_buffer[1..])) |prescript| {
- if (!try runPackageScript(
+ if (!try runPackageScriptForeground(
ctx.allocator,
prescript,
temp_script_buffer[1..],
@@ -1063,7 +1400,7 @@ pub const RunCommand = struct {
}
}
- if (!try runPackageScript(
+ if (!try runPackageScriptForeground(
ctx.allocator,
script_content,
script_name_to_search,
@@ -1076,7 +1413,7 @@ pub const RunCommand = struct {
temp_script_buffer[0.."post".len].* = "post".*;
if (scripts.get(temp_script_buffer)) |postscript| {
- if (!try runPackageScript(
+ if (!try runPackageScriptForeground(
ctx.allocator,
postscript,
temp_script_buffer,