diff options
Diffstat (limited to 'src/cli/run_command.zig')
-rw-r--r-- | src/cli/run_command.zig | 349 |
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(©_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, |