//! The interpreter for the shell language //! //! There are several constraints on the Bun shell language that make this //! interpreter implementation unique: //! //! 1. We try to keep everything in the Bun process as much as possible for //! performance reasons and also to leverage Bun's existing IO/FS code //! 2. We try to use non-blocking IO operations as much as possible so the //! shell does not block the main JS thread //! 3. Zig does not have coroutines (yet) //! //! The idea is that this is a tree-walking interpreter. Except it's not. //! //! Why not? Because 99% of operations in the shell are IO, and we need to do //! non-blocking IO because Bun is a JS runtime. //! //! So what do we do? Instead of iteratively walking the AST like in a traditional //! tree-walking interpreter, we're also going to build up a tree of state-machines //! (an AST node becomes a state-machine node), so we can suspend and resume //! execution without blocking the main thread. //! //! We'll also need to do things in continuation-passing style, see `Yield.zig` for //! more on that. //! //! Once all these pieces come together, this ends up being a: //! "state-machine based [tree-walking], [trampoline]-driven [continuation-passing style] interpreter" //! //! [tree-walking]: https://en.wikipedia.org/wiki/Interpreter_(computing)#Abstract_syntax_tree_interpreters //! [trampoline]: https://en.wikipedia.org/wiki/Trampoline_(computing) //! [continuation-passing style]: https://en.wikipedia.org/wiki/Continuation-passing_style //! //! # Memory management //! //! Almost all allocations go through the `AllocationScope` allocator. This //! trackd memory allocations and frees in debug builds (or builds with asan //! enabled) and helps us catch memory leaks. //! //! The underlying parent allocator that every `AllocationScope` uses in the //! shell is `bun.default_allocator`. This means in builds of Bun which do not //! have `AllocationScope` enabled, every allocation just goes straight through //! to `bun.default_allocator`. //! //! Usually every state machine node ends up creating a new allocation scope, //! so an `AllocationScope` is stored in the base header struct (see `Base.zig`) //! that all state-machine nodes include in their layout. //! //! You will often see `Base.initWithNewAllocScope` to create a new state machine node //! and allocation scope. //! //! Sometimes it is necessary to "leak" an allocation from its scope. For //! example, argument expansion happens in an allocation scope inside //! `Expansion.zig`. //! //! But the string that is expanded may end up becoming the key/value of an //! environment variable, which we internally use the reference counted `EnvStr` //! for. When we turn it into an `EnvStr`, the reference counting scheme is //! responsible for managing the memory so we can call //! `allocScope.leakSlice(str)` to tell it not to track the allocation anymore //! and let `EnvStr` handle it. const std = @import("std"); const builtin = @import("builtin"); const string = []const u8; const bun = @import("bun"); const posix = std.posix; pub const Arena = std.heap.ArenaAllocator; const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const JSC = bun.JSC; const JSValue = bun.JSC.JSValue; const JSGlobalObject = bun.JSC.JSGlobalObject; const which = bun.which; pub const Braces = @import("./braces.zig"); pub const Syscall = bun.sys; const Glob = @import("../glob.zig"); const ResolvePath = bun.path; const TaggedPointerUnion = bun.TaggedPointerUnion; pub const WorkPoolTask = JSC.WorkPoolTask; pub const WorkPool = JSC.WorkPool; const windows = bun.windows; const uv = windows.libuv; const Maybe = JSC.Maybe; const WTFStringImplStruct = @import("../string.zig").WTFStringImplStruct; const Yield = shell.Yield; pub const Pipe = [2]bun.FileDescriptor; const shell = bun.shell; const ast = shell.AST; pub const SmolList = shell.SmolList; pub const GlobWalker = Glob.BunGlobWalkerZ; pub const stdin_no = 0; pub const stdout_no = 1; pub const stderr_no = 2; pub fn OOM(e: anyerror) noreturn { if (comptime bun.Environment.allow_assert) { if (e != error.OutOfMemory) bun.outOfMemory(); } bun.outOfMemory(); } pub const log = bun.Output.scoped(.SHELL, false); const assert = bun.assert; /// This is a zero-sized type returned by `.needsIO()`, designed to ensure /// functions which rely on IO are not called when they do don't need it. /// /// For example the .enqueue(), .enqueueFmtBltn(), etc functions. /// /// It is used like this: /// /// ```zig /// if (this.bltn.stdout.needsIO()) |safeguard| { /// this.bltn.stdout.enqueue(this, chunk, safeguard); /// return .cont; /// } // _ = this.bltn.writeNoIO(.stdout, chunk); /// ``` /// /// The compiler optimizes away this type so it has zero runtime cost. /// /// You should never instantiate this type directly, unless you know /// from previous context that the output needs IO. /// /// Functions which accept a `_: OutputNeedsIOSafeGuard` parameter can /// safely assume the stdout/stderr they are working with require IO. pub const OutputNeedsIOSafeGuard = enum(u0) { output_needs_io }; /// Similar to `OutputNeedsIOSafeGuard` but to ensure a function is /// called at the "top" of the call-stack relative to the interpreter's /// execution. pub const CallstackGuard = enum(u0) { __i_know_what_i_am_doing }; pub const ExitCode = u16; pub const StateKind = enum(u8) { script, stmt, assign, cmd, binary, pipeline, expansion, if_clause, condexpr, @"async", subshell, }; /// Copy-on-write file descriptor. This is to avoid having multiple non-blocking /// writers to the same file descriptor, which breaks epoll/kqueue /// /// Two main fields: /// 1. refcount - tracks number of references to the fd, closes file descriptor when reaches 0 /// 2. being_written - if the fd is currently being used by a BufferedWriter for non-blocking writes /// /// If you want to write to the file descriptor, you call `.write()`, if `being_written` is true it will duplicate the file descriptor. pub const CowFd = struct { __fd: bun.FileDescriptor, refcount: u32 = 1, being_used: bool = false, const debug = bun.Output.scoped(.CowFd, true); pub fn init(fd: bun.FileDescriptor) *CowFd { const this = bun.default_allocator.create(CowFd) catch bun.outOfMemory(); this.* = .{ .__fd = fd, }; debug("init(0x{x}, fd={})", .{ @intFromPtr(this), fd }); return this; } pub fn dup(this: *CowFd) Maybe(*CowFd) { const new = bun.new(CowFd, .{ .fd = bun.sys.dup(this.fd), .writercount = 1, }); debug("dup(0x{x}, fd={}) = (0x{x}, fd={})", .{ @intFromPtr(this), this.fd, new, new.fd }); return new; } pub fn use(this: *CowFd) Maybe(*CowFd) { if (!this.being_used) { this.being_used = true; this.ref(); return .{ .result = this }; } return this.dup(); } pub fn doneUsing(this: *CowFd) void { this.being_used = false; } pub fn ref(this: *CowFd) void { this.refcount += 1; } pub fn refSelf(this: *CowFd) *CowFd { this.ref(); return this; } pub fn deref(this: *CowFd) void { this.refcount -= 1; if (this.refcount == 0) { this.deinit(); } } pub fn deinit(this: *CowFd) void { assert(this.refcount == 0); this.__fd.close(); bun.default_allocator.destroy(this); } }; pub const CoroutineResult = enum { /// it's okay for the caller to continue its execution cont, yield, }; pub const RefCountedStr = @import("./RefCountedStr.zig"); pub const EnvStr = @import("./EnvStr.zig").EnvStr; pub const EnvMap = @import("./EnvMap.zig"); pub const ParsedShellScript = @import("./ParsedShellScript.zig"); pub const ShellArgs = struct { /// This is the arena used to allocate the input shell script's AST nodes, /// tokens, and a string pool used to store all strings. __arena: *bun.ArenaAllocator, /// Root ast node script_ast: ast.Script = .{ .stmts = &[_]ast.Stmt{} }, pub const new = bun.TrivialNew(@This()); pub fn arena_allocator(this: *ShellArgs) std.mem.Allocator { return this.__arena.allocator(); } pub fn deinit(this: *ShellArgs) void { this.__arena.deinit(); bun.destroy(this.__arena); bun.destroy(this); } pub fn init() *ShellArgs { const arena = bun.new(bun.ArenaAllocator, bun.ArenaAllocator.init(bun.default_allocator)); return ShellArgs.new(.{ .__arena = arena, .script_ast = undefined, }); } }; pub const AssignCtx = Interpreter.Assigns.AssignCtx; /// This interpreter works by basically turning the AST into a state machine so /// that execution can be suspended and resumed to support async. pub const Interpreter = struct { pub const js = JSC.Codegen.JSShellInterpreter; pub const toJS = js.toJS; pub const fromJS = js.fromJS; pub const fromJSDirect = js.fromJSDirect; command_ctx: bun.CLI.Command.Context, event_loop: JSC.EventLoopHandle, /// This is the allocator used to allocate interpreter state allocator: Allocator, args: *ShellArgs, /// JS objects used as input for the shell script /// This should be allocated using the arena jsobjs: []JSValue, root_shell: ShellExecEnv, root_io: IO, has_pending_activity: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), started: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), // Necessary for builtin commands. keep_alive: bun.Async.KeepAlive = .{}, vm_args_utf8: std.ArrayList(JSC.ZigString.Slice), async_commands_executing: u32 = 0, globalThis: *JSC.JSGlobalObject, flags: packed struct(u8) { done: bool = false, quiet: bool = false, __unused: u6 = 0, } = .{}, exit_code: ?ExitCode = 0, this_jsvalue: JSValue = .zero, __alloc_scope: if (bun.Environment.enableAllocScopes) bun.AllocationScope else void, // Here are all the state nodes: pub const State = @import("./states/Base.zig"); pub const Script = @import("./states/Script.zig"); pub const Stmt = @import("./states/Stmt.zig"); pub const Pipeline = @import("./states/Pipeline.zig"); pub const Binary = @import("./states/Binary.zig"); pub const Subshell = @import("./states/Subshell.zig"); pub const Expansion = @import("./states/Expansion.zig"); pub const Assigns = @import("./states/Assigns.zig"); pub const Async = @import("./states/Async.zig"); pub const CondExpr = @import("./states/CondExpr.zig"); pub const If = @import("./states/If.zig"); pub const Cmd = @import("./states/Cmd.zig"); pub const InterpreterChildPtr = StatePtrUnion(.{ Script, }); /// During execution, the shell has an "environment" or "context". This /// contains important details like environment variables, cwd, etc. Every /// state node is given a `*ShellExecEnv` which is stored in its header (see /// `states/Base.zig`). /// /// Certain state nodes like subshells, pipelines, and cmd substitutions /// will duplicate their `*ShellExecEnv` so that they can make modifications /// without affecting their parent `ShellExecEnv`. This is done in the /// `.dupeForSubshell` function. /// /// For example: /// /// ```bash /// echo $(FOO=bar; echo $FOO); echo $FOO /// ``` /// /// The $FOO variable is set inside the command substitution but not outside. /// /// Note that stdin/stdout/stderr is also considered to be part of the /// environment/context, but we keep that in a separate struct called `IO`. We do /// this because stdin/stdout/stderr changes a lot and we don't want to copy /// this `ShellExecEnv` struct too much. /// /// More info here: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_12 pub const ShellExecEnv = struct { kind: Kind = .normal, /// This is the buffered stdout/stderr that captures the entire /// output of the script and is given to JS. /// /// Accross the entire script execution, this is usually the same. /// /// It changes when a cmd substitution is run. /// /// These MUST use the `bun.default_allocator` Allocator _buffered_stdout: Bufio = .{ .owned = .{} }, _buffered_stderr: Bufio = .{ .owned = .{} }, /// TODO Performance optimization: make these env maps copy-on-write /// Shell env for expansion by the shell shell_env: EnvMap, /// Local environment variables to be given to a subprocess cmd_local_env: EnvMap, /// Exported environment variables available to all subprocesses. This includes system ones. export_env: EnvMap, /// The current working directory of the shell. /// Use an array list so we don't have to keep reallocating /// Always has zero-sentinel __prev_cwd: std.ArrayList(u8), __cwd: std.ArrayList(u8), cwd_fd: bun.FileDescriptor, async_pids: SmolList(pid_t, 4) = SmolList(pid_t, 4).zeroes, __alloc_scope: if (bun.Environment.enableAllocScopes) *bun.AllocationScope else void, const pid_t = if (bun.Environment.isPosix) std.posix.pid_t else uv.uv_pid_t; const Bufio = union(enum) { owned: bun.ByteList, borrowed: *bun.ByteList }; const Kind = enum { normal, cmd_subst, subshell, pipeline, }; pub fn allocator(this: *ShellExecEnv) std.mem.Allocator { if (comptime bun.Environment.enableAllocScopes) return this.__alloc_scope.allocator(); return bun.default_allocator; } pub fn buffered_stdout(this: *ShellExecEnv) *bun.ByteList { return switch (this._buffered_stdout) { .owned => &this._buffered_stdout.owned, .borrowed => this._buffered_stdout.borrowed, }; } pub fn buffered_stderr(this: *ShellExecEnv) *bun.ByteList { return switch (this._buffered_stderr) { .owned => &this._buffered_stderr.owned, .borrowed => this._buffered_stderr.borrowed, }; } pub inline fn cwdZ(this: *ShellExecEnv) [:0]const u8 { if (this.__cwd.items.len == 0) return ""; return this.__cwd.items[0..this.__cwd.items.len -| 1 :0]; } pub inline fn prevCwdZ(this: *ShellExecEnv) [:0]const u8 { if (this.__prev_cwd.items.len == 0) return ""; return this.__prev_cwd.items[0..this.__prev_cwd.items.len -| 1 :0]; } pub inline fn prevCwd(this: *ShellExecEnv) []const u8 { const prevcwdz = this.prevCwdZ(); return prevcwdz[0..prevcwdz.len]; } pub inline fn cwd(this: *ShellExecEnv) []const u8 { const cwdz = this.cwdZ(); return cwdz[0..cwdz.len]; } pub fn deinit(this: *ShellExecEnv) void { this.deinitImpl(true, true); } /// Doesn't deref `this.io` /// /// If called by interpreter we have to: /// 1. not free this *ShellExecEnv, because its on a field on the interpreter /// 2. don't free buffered_stdout and buffered_stderr, because that is used for output fn deinitImpl(this: *ShellExecEnv, comptime destroy_this: bool, comptime free_buffered_io: bool) void { log("[ShellExecEnv] deinit {x}", .{@intFromPtr(this)}); if (comptime free_buffered_io) { if (this._buffered_stdout == .owned) { this._buffered_stdout.owned.deinitWithAllocator(bun.default_allocator); } if (this._buffered_stderr == .owned) { this._buffered_stderr.owned.deinitWithAllocator(bun.default_allocator); } } this.shell_env.deinit(); this.cmd_local_env.deinit(); this.export_env.deinit(); this.__cwd.deinit(); this.__prev_cwd.deinit(); closefd(this.cwd_fd); if (comptime destroy_this) this.allocator().destroy(this); } pub fn dupeForSubshell( this: *ShellExecEnv, alloc_scope: if (bun.Environment.enableAllocScopes) *bun.AllocationScope else void, alloc: Allocator, io: IO, kind: Kind, ) Maybe(*ShellExecEnv) { const duped = alloc.create(ShellExecEnv) catch bun.outOfMemory(); const dupedfd = switch (Syscall.dup(this.cwd_fd)) { .err => |err| return .{ .err = err }, .result => |fd| fd, }; const stdout: Bufio = switch (io.stdout) { .fd => brk: { if (io.stdout.fd.captured != null) break :brk .{ .borrowed = io.stdout.fd.captured.? }; break :brk .{ .owned = .{} }; }, .ignore => .{ .owned = .{} }, .pipe => switch (kind) { .normal, .cmd_subst => .{ .owned = .{} }, .subshell, .pipeline => .{ .borrowed = this.buffered_stdout() }, }, }; const stderr: Bufio = switch (io.stderr) { .fd => brk: { if (io.stderr.fd.captured != null) break :brk .{ .borrowed = io.stderr.fd.captured.? }; break :brk .{ .owned = .{} }; }, .ignore => .{ .owned = .{} }, .pipe => switch (kind) { .normal, .cmd_subst => .{ .owned = .{} }, .subshell, .pipeline => .{ .borrowed = this.buffered_stderr() }, }, }; duped.* = .{ .kind = kind, ._buffered_stdout = stdout, ._buffered_stderr = stderr, .shell_env = this.shell_env.clone(), .cmd_local_env = EnvMap.init(alloc), .export_env = this.export_env.clone(), .__prev_cwd = this.__prev_cwd.clone() catch bun.outOfMemory(), .__cwd = this.__cwd.clone() catch bun.outOfMemory(), // TODO probably need to use os.dup here .cwd_fd = dupedfd, .__alloc_scope = alloc_scope, }; return .{ .result = duped }; } /// NOTE: This will `.ref()` value, so you should `defer value.deref()` it before handing it to this function. pub fn assignVar(this: *ShellExecEnv, interp: *ThisInterpreter, label: EnvStr, value: EnvStr, assign_ctx: AssignCtx) void { _ = interp; // autofix switch (assign_ctx) { .cmd => this.cmd_local_env.insert(label, value), .shell => this.shell_env.insert(label, value), .exported => this.export_env.insert(label, value), } } pub fn changePrevCwd(self: *ShellExecEnv, interp: *ThisInterpreter) Maybe(void) { return self.changeCwd(interp, self.prevCwdZ()); } pub fn changeCwd(this: *ShellExecEnv, interp: *ThisInterpreter, new_cwd_: anytype) Maybe(void) { return this.changeCwdImpl(interp, new_cwd_, false); } pub fn changeCwdImpl(this: *ShellExecEnv, _: *ThisInterpreter, new_cwd_: anytype, comptime in_init: bool) Maybe(void) { if (comptime @TypeOf(new_cwd_) != [:0]const u8 and @TypeOf(new_cwd_) != []const u8) { @compileError("Bad type for new_cwd " ++ @typeName(@TypeOf(new_cwd_))); } const is_sentinel = @TypeOf(new_cwd_) == [:0]const u8; const new_cwd: [:0]const u8 = brk: { if (ResolvePath.Platform.auto.isAbsolute(new_cwd_)) { if (is_sentinel) { @memcpy(ResolvePath.join_buf[0..new_cwd_.len], new_cwd_[0..new_cwd_.len]); ResolvePath.join_buf[new_cwd_.len] = 0; break :brk ResolvePath.join_buf[0..new_cwd_.len :0]; } std.mem.copyForwards(u8, &ResolvePath.join_buf, new_cwd_); ResolvePath.join_buf[new_cwd_.len] = 0; break :brk ResolvePath.join_buf[0..new_cwd_.len :0]; } const existing_cwd = this.cwd(); const cwd_str = ResolvePath.joinZ(&[_][]const u8{ existing_cwd, new_cwd_, }, .auto); // remove trailing separator if (bun.Environment.isWindows) { const sep = '\\'; if (cwd_str.len > 1 and cwd_str[cwd_str.len - 1] == sep) { ResolvePath.join_buf[cwd_str.len - 1] = 0; break :brk ResolvePath.join_buf[0 .. cwd_str.len - 1 :0]; } } if (cwd_str.len > 1 and cwd_str[cwd_str.len - 1] == '/') { ResolvePath.join_buf[cwd_str.len - 1] = 0; break :brk ResolvePath.join_buf[0 .. cwd_str.len - 1 :0]; } break :brk cwd_str; }; const new_cwd_fd = switch (ShellSyscall.openat( this.cwd_fd, new_cwd, bun.O.DIRECTORY | bun.O.RDONLY, 0, )) { .result => |fd| fd, .err => |err| { return Maybe(void).initErr(err); }, }; _ = this.cwd_fd.closeAllowingBadFileDescriptor(null); this.__prev_cwd.clearRetainingCapacity(); this.__prev_cwd.appendSlice(this.__cwd.items[0..]) catch bun.outOfMemory(); this.__cwd.clearRetainingCapacity(); this.__cwd.appendSlice(new_cwd[0 .. new_cwd.len + 1]) catch bun.outOfMemory(); if (comptime bun.Environment.allow_assert) { assert(this.__cwd.items[this.__cwd.items.len -| 1] == 0); assert(this.__prev_cwd.items[this.__prev_cwd.items.len -| 1] == 0); } this.cwd_fd = new_cwd_fd; if (comptime !in_init) { this.export_env.insert(EnvStr.initSlice("OLDPWD"), EnvStr.initSlice(this.prevCwd())); } this.export_env.insert(EnvStr.initSlice("PWD"), EnvStr.initSlice(this.cwd())); return Maybe(void).success; } pub fn getHomedir(self: *ShellExecEnv) EnvStr { const env_var: ?EnvStr = brk: { const static_str = if (comptime bun.Environment.isWindows) EnvStr.initSlice("USERPROFILE") else EnvStr.initSlice("HOME"); break :brk self.shell_env.get(static_str) orelse self.export_env.get(static_str); }; return env_var orelse EnvStr.initSlice(""); } pub fn writeFailingErrorFmt( this: *ShellExecEnv, ctx: anytype, enqueueCb: fn (c: @TypeOf(ctx)) void, comptime fmt: []const u8, args: anytype, ) Yield { const io: *IO.OutKind = &@field(ctx.io, "stderr"); switch (io.*) { .fd => |x| { enqueueCb(ctx); return x.writer.enqueueFmt(ctx, x.captured, fmt, args); }, .pipe => { const bufio: *bun.ByteList = this.buffered_stderr(); bufio.appendFmt(bun.default_allocator, fmt, args) catch bun.outOfMemory(); return ctx.parent.childDone(ctx, 1); }, // FIXME: This is not correct? This would just make the entire shell hang I think? .ignore => { const childptr = IOWriterChildPtr.init(ctx); // TODO: is this necessary const count = std.fmt.count(fmt, args); return .{ .on_io_writer_chunk = .{ .child = childptr.asAnyOpaque(), .err = null, .written = count, } }; }, } } }; const ThisInterpreter = @This(); const ShellErrorKind = error{ OutOfMemory, Syscall, }; const ShellErrorCtx = union(enum) { syscall: Syscall.Error, other: ShellErrorKind, fn toJS(this: ShellErrorCtx, globalThis: *JSGlobalObject) JSValue { return switch (this) { .syscall => |err| err.toJS(globalThis), .other => |err| bun.JSC.ZigString.fromBytes(@errorName(err)).toJS(globalThis), }; } }; pub fn createShellInterpreter(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { const allocator = bun.default_allocator; const arguments_ = callframe.arguments_old(3); var arguments = JSC.CallFrame.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); const resolve = arguments.nextEat() orelse return globalThis.throw("shell: expected 3 arguments, got 0", .{}); const reject = arguments.nextEat() orelse return globalThis.throw("shell: expected 3 arguments, got 0", .{}); const parsed_shell_script_js = arguments.nextEat() orelse return globalThis.throw("shell: expected 3 arguments, got 0", .{}); const parsed_shell_script = parsed_shell_script_js.as(ParsedShellScript) orelse return globalThis.throw("shell: expected a ParsedShellScript", .{}); var shargs: *ShellArgs = undefined; var jsobjs: std.ArrayList(JSValue) = std.ArrayList(JSValue).init(allocator); var quiet: bool = false; var cwd: ?bun.String = null; var export_env: ?EnvMap = null; if (parsed_shell_script.args == null) return globalThis.throw("shell: shell args is null, this is a bug in Bun. Please file a GitHub issue.", .{}); parsed_shell_script.take( globalThis, &shargs, &jsobjs, &quiet, &cwd, &export_env, ); const cwd_string: ?bun.JSC.ZigString.Slice = if (cwd) |c| brk: { break :brk c.toUTF8(bun.default_allocator); } else null; defer if (cwd_string) |c| c.deinit(); const interpreter: *Interpreter = switch (ThisInterpreter.init( undefined, // command_ctx, unused when event_loop is .js .{ .js = globalThis.bunVM().event_loop }, allocator, shargs, jsobjs.items[0..], export_env, if (cwd_string) |c| c.slice() else null, )) { .result => |i| i, .err => |*e| { jsobjs.deinit(); if (export_env) |*ee| ee.deinit(); if (cwd) |*cc| cc.deref(); shargs.deinit(); return try throwShellErr(e, .{ .js = globalThis.bunVM().event_loop }); }, }; if (globalThis.hasException()) { jsobjs.deinit(); if (export_env) |*ee| ee.deinit(); if (cwd) |*cc| cc.deref(); shargs.deinit(); interpreter.finalize(); return error.JSError; } interpreter.flags.quiet = quiet; interpreter.globalThis = globalThis; const js_value = JSC.Codegen.JSShellInterpreter.toJS(interpreter, globalThis); interpreter.this_jsvalue = js_value; JSC.Codegen.JSShellInterpreter.resolveSetCached(js_value, globalThis, resolve); JSC.Codegen.JSShellInterpreter.rejectSetCached(js_value, globalThis, reject); interpreter.keep_alive.ref(globalThis.bunVM()); bun.Analytics.Features.shell += 1; return js_value; } pub fn parse( arena_allocator: std.mem.Allocator, script: []const u8, jsobjs: []JSValue, jsstrings_to_escape: []bun.String, out_parser: *?bun.shell.Parser, out_lex_result: *?shell.LexResult, ) !ast.Script { const lex_result = brk: { if (bun.strings.isAllASCII(script)) { var lexer = bun.shell.LexerAscii.new(arena_allocator, script, jsstrings_to_escape); try lexer.lex(); break :brk lexer.get_result(); } var lexer = bun.shell.LexerUnicode.new(arena_allocator, script, jsstrings_to_escape); try lexer.lex(); break :brk lexer.get_result(); }; if (lex_result.errors.len > 0) { out_lex_result.* = lex_result; return shell.ParseError.Lex; } if (comptime bun.Environment.allow_assert) { const debug = bun.Output.scoped(.ShellTokens, true); var test_tokens = std.ArrayList(shell.Test.TestToken).initCapacity(arena_allocator, lex_result.tokens.len) catch @panic("OOPS"); defer test_tokens.deinit(); for (lex_result.tokens) |tok| { const test_tok = shell.Test.TestToken.from_real(tok, lex_result.strpool); test_tokens.append(test_tok) catch @panic("OOPS"); } const str = std.json.stringifyAlloc(bun.default_allocator, test_tokens.items[0..], .{}) catch @panic("OOPS"); defer bun.default_allocator.free(str); debug("Tokens: {s}", .{str}); } out_parser.* = try bun.shell.Parser.new(arena_allocator, lex_result, jsobjs); const script_ast = try out_parser.*.?.parse(); return script_ast; } /// If all initialization allocations succeed, the arena will be copied /// into the interpreter struct, so it is not a stale reference and safe to call `arena.deinit()` on error. pub fn init( ctx: bun.CLI.Command.Context, event_loop: JSC.EventLoopHandle, allocator: Allocator, shargs: *ShellArgs, jsobjs: []JSValue, export_env_: ?EnvMap, cwd_: ?[]const u8, ) shell.Result(*ThisInterpreter) { const export_env = brk: { if (event_loop == .js) break :brk if (export_env_) |e| e else EnvMap.init(allocator); var env_loader: *bun.DotEnv.Loader = env_loader: { if (event_loop == .js) { break :env_loader event_loop.js.virtual_machine.transpiler.env; } break :env_loader event_loop.env(); }; // This will save ~2x memory var export_env = EnvMap.initWithCapacity(allocator, env_loader.map.map.unmanaged.entries.len); var iter = env_loader.iterator(); while (iter.next()) |entry| { const value = EnvStr.initSlice(entry.value_ptr.value); const key = EnvStr.initSlice(entry.key_ptr.*); export_env.insert(key, value); } break :brk export_env; }; // Avoid the large stack allocation on Windows. const pathbuf = bun.path_buffer_pool.get(); defer bun.path_buffer_pool.put(pathbuf); const cwd: [:0]const u8 = switch (Syscall.getcwdZ(pathbuf)) { .result => |cwd| cwd, .err => |err| { return .{ .err = .{ .sys = err.toShellSystemError() } }; }, }; const cwd_fd = switch (Syscall.open(cwd, bun.O.DIRECTORY | bun.O.RDONLY, 0)) { .result => |fd| fd, .err => |err| { return .{ .err = .{ .sys = err.toShellSystemError() } }; }, }; var cwd_arr = std.ArrayList(u8).initCapacity(bun.default_allocator, cwd.len + 1) catch bun.outOfMemory(); cwd_arr.appendSlice(cwd[0 .. cwd.len + 1]) catch bun.outOfMemory(); if (comptime bun.Environment.allow_assert) { assert(cwd_arr.items[cwd_arr.items.len -| 1] == 0); } log("Duping stdin", .{}); const stdin_fd = switch (if (bun.Output.Source.Stdio.isStdinNull()) bun.sys.openNullDevice() else ShellSyscall.dup(shell.STDIN_FD)) { .result => |fd| fd, .err => |err| return .{ .err = .{ .sys = err.toShellSystemError() } }, }; const stdin_reader = IOReader.init(stdin_fd, event_loop); const interpreter = allocator.create(ThisInterpreter) catch bun.outOfMemory(); interpreter.* = .{ .command_ctx = ctx, .event_loop = event_loop, .args = shargs, .allocator = allocator, .jsobjs = jsobjs, .root_shell = ShellExecEnv{ .shell_env = EnvMap.init(allocator), .cmd_local_env = EnvMap.init(allocator), .export_env = export_env, .__cwd = cwd_arr, .__prev_cwd = cwd_arr.clone() catch bun.outOfMemory(), .cwd_fd = cwd_fd, .__alloc_scope = undefined, }, .root_io = .{ .stdin = .{ .fd = stdin_reader, }, // By default stdout/stderr should be an IOWriter writing to a dup'ed stdout/stderr // But if the user later calls `.setQuiet(true)` then all those syscalls/initialization was pointless work // So we cheaply initialize them now as `.pipe` // When `Interpreter.run()` is called, we check if `this.flags.quiet == false`, if so then we then properly initialize the IOWriter .stdout = .pipe, .stderr = .pipe, }, .vm_args_utf8 = std.ArrayList(JSC.ZigString.Slice).init(bun.default_allocator), .__alloc_scope = if (bun.Environment.enableAllocScopes) bun.AllocationScope.init(allocator) else {}, .globalThis = undefined, }; if (cwd_) |c| { if (interpreter.root_shell.changeCwdImpl(interpreter, c, true).asErr()) |e| return .{ .err = .{ .sys = e.toShellSystemError() } }; } interpreter.root_shell.__alloc_scope = if (bun.Environment.enableAllocScopes) &interpreter.__alloc_scope else {}; return .{ .result = interpreter }; } pub fn initAndRunFromFile(ctx: bun.CLI.Command.Context, mini: *JSC.MiniEventLoop, path: []const u8) !bun.shell.ExitCode { var shargs = ShellArgs.init(); const src = src: { var file = try std.fs.cwd().openFile(path, .{}); defer file.close(); break :src try file.reader().readAllAlloc(shargs.arena_allocator(), std.math.maxInt(u32)); }; defer shargs.deinit(); const jsobjs: []JSValue = &[_]JSValue{}; var out_parser: ?bun.shell.Parser = null; var out_lex_result: ?bun.shell.LexResult = null; const script = ThisInterpreter.parse( shargs.arena_allocator(), src, jsobjs, &[_]bun.String{}, &out_parser, &out_lex_result, ) catch |err| { if (err == bun.shell.ParseError.Lex) { assert(out_lex_result != null); const str = out_lex_result.?.combineErrors(shargs.arena_allocator()); bun.Output.prettyErrorln("error: Failed to run {s} due to error {s}", .{ std.fs.path.basename(path), str }); bun.Global.exit(1); } if (out_parser) |*p| { const errstr = p.combineErrors(); bun.Output.prettyErrorln("error: Failed to run {s} due to error {s}", .{ std.fs.path.basename(path), errstr }); bun.Global.exit(1); } return err; }; shargs.script_ast = script; var interp = switch (ThisInterpreter.init( ctx, .{ .mini = mini }, bun.default_allocator, shargs, jsobjs, null, null, )) { .err => |*e| { e.throwMini(); }, .result => |i| i, }; const exit_code: ExitCode = 1; const IsDone = struct { interp: *const Interpreter, fn isDone(this: *anyopaque) bool { const asdlfk = bun.cast(*const @This(), this); return asdlfk.interp.flags.done; } }; var is_done: IsDone = .{ .interp = interp, }; interp.exit_code = exit_code; switch (try interp.run()) { .err => |e| { interp.deinitEverything(); bun.Output.err(e, "Failed to run script {s}", .{std.fs.path.basename(path)}); bun.Global.exit(1); return 1; }, else => {}, } mini.tick(&is_done, @as(fn (*anyopaque) bool, IsDone.isDone)); const code = interp.exit_code.?; interp.deinitEverything(); return code; } pub fn initAndRunFromSource(ctx: bun.CLI.Command.Context, mini: *JSC.MiniEventLoop, path_for_errors: []const u8, src: []const u8, cwd: ?[]const u8) !ExitCode { bun.Analytics.Features.standalone_shell += 1; var shargs = ShellArgs.init(); defer shargs.deinit(); const jsobjs: []JSValue = &[_]JSValue{}; var out_parser: ?bun.shell.Parser = null; var out_lex_result: ?bun.shell.LexResult = null; const script = ThisInterpreter.parse(shargs.arena_allocator(), src, jsobjs, &[_]bun.String{}, &out_parser, &out_lex_result) catch |err| { if (err == bun.shell.ParseError.Lex) { assert(out_lex_result != null); const str = out_lex_result.?.combineErrors(shargs.arena_allocator()); bun.Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ path_for_errors, str }); bun.Global.exit(1); } if (out_parser) |*p| { const errstr = p.combineErrors(); bun.Output.prettyErrorln("error: Failed to run script {s} due to error {s}", .{ path_for_errors, errstr }); bun.Global.exit(1); } return err; }; shargs.script_ast = script; var interp: *ThisInterpreter = switch (ThisInterpreter.init( ctx, .{ .mini = mini }, bun.default_allocator, shargs, jsobjs, null, cwd, )) { .err => |*e| { e.throwMini(); }, .result => |i| i, }; const IsDone = struct { interp: *const Interpreter, fn isDone(this: *anyopaque) bool { const asdlfk = bun.cast(*const @This(), this); return asdlfk.interp.flags.done; } }; var is_done: IsDone = .{ .interp = interp, }; const exit_code: ExitCode = 1; interp.exit_code = exit_code; switch (try interp.run()) { .err => |e| { interp.deinitEverything(); bun.Output.err(e, "Failed to run script {s}", .{path_for_errors}); bun.Global.exit(1); return 1; }, else => {}, } mini.tick(&is_done, @as(fn (*anyopaque) bool, IsDone.isDone)); const code = interp.exit_code.?; interp.deinitEverything(); return code; } fn setupIOBeforeRun(this: *ThisInterpreter) Maybe(void) { if (!this.flags.quiet) { const event_loop = this.event_loop; log("Duping stdout", .{}); const stdout_fd = switch (if (bun.Output.Source.Stdio.isStdoutNull()) bun.sys.openNullDevice() else ShellSyscall.dup(.stdout())) { .result => |fd| fd, .err => |err| return .{ .err = err }, }; log("Duping stderr", .{}); const stderr_fd = switch (if (bun.Output.Source.Stdio.isStderrNull()) bun.sys.openNullDevice() else ShellSyscall.dup(.stderr())) { .result => |fd| fd, .err => |err| return .{ .err = err }, }; const stdout_writer = IOWriter.init( stdout_fd, .{ .pollable = isPollable(stdout_fd, event_loop.stdout().data.file.mode), }, event_loop, ); const stderr_writer = IOWriter.init(stderr_fd, .{ .pollable = isPollable(stderr_fd, event_loop.stderr().data.file.mode), }, event_loop); this.root_io = .{ .stdin = this.root_io.stdin, .stdout = .{ .fd = .{ .writer = stdout_writer, }, }, .stderr = .{ .fd = .{ .writer = stderr_writer, }, }, }; if (event_loop == .js) { this.root_io.stdout.fd.captured = &this.root_shell._buffered_stdout.owned; this.root_io.stderr.fd.captured = &this.root_shell._buffered_stderr.owned; } } return Maybe(void).success; } pub fn run(this: *ThisInterpreter) !Maybe(void) { log("Interpreter(0x{x}) run", .{@intFromPtr(this)}); if (this.setupIOBeforeRun().asErr()) |e| { return .{ .err = e }; } var root = Script.init(this, &this.root_shell, &this.args.script_ast, Script.ParentPtr.init(this), this.root_io.copy()); this.started.store(true, .seq_cst); root.start().run(); return Maybe(void).success; } pub fn runFromJS(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { log("Interpreter(0x{x}) runFromJS", .{@intFromPtr(this)}); _ = callframe; // autofix if (this.setupIOBeforeRun().asErr()) |e| { defer this.deinitEverything(); const shellerr = bun.shell.ShellErr.newSys(e); return try throwShellErr(&shellerr, .{ .js = globalThis.bunVM().event_loop }); } incrPendingActivityFlag(&this.has_pending_activity); var root = Script.init(this, &this.root_shell, &this.args.script_ast, Script.ParentPtr.init(this), this.root_io.copy()); this.started.store(true, .seq_cst); root.start().run(); if (globalThis.hasException()) return error.JSError; return .js_undefined; } fn ioToJSValue(globalThis: *JSGlobalObject, buf: *bun.ByteList) JSValue { const bytelist = buf.*; buf.* = .{}; const buffer: JSC.Node.Buffer = .{ .allocator = bun.default_allocator, .buffer = JSC.ArrayBuffer.fromBytes(@constCast(bytelist.slice()), .Uint8Array), }; return buffer.toNodeBuffer(globalThis); } pub fn asyncCmdDone(this: *ThisInterpreter, @"async": *Async) void { log("asyncCommandDone {}", .{@"async"}); @"async".actuallyDeinit(); this.async_commands_executing -= 1; if (this.async_commands_executing == 0 and this.exit_code != null) { this.finish(this.exit_code.?).run(); } } pub fn childDone(this: *ThisInterpreter, child: InterpreterChildPtr, exit_code: ExitCode) Yield { if (child.ptr.is(Script)) { const script = child.as(Script); script.deinitFromInterpreter(); this.exit_code = exit_code; if (this.async_commands_executing == 0) return this.finish(exit_code); return .suspended; } @panic("Bad child"); } pub fn finish(this: *ThisInterpreter, exit_code: ExitCode) Yield { log("Interpreter(0x{x}) finish {d}", .{ @intFromPtr(this), exit_code }); defer decrPendingActivityFlag(&this.has_pending_activity); if (this.event_loop == .js) { defer this.deinitAfterJSRun(); this.exit_code = exit_code; const this_jsvalue = this.this_jsvalue; if (this_jsvalue != .zero) { if (JSC.Codegen.JSShellInterpreter.resolveGetCached(this_jsvalue)) |resolve| { const loop = this.event_loop.js; const globalThis = this.globalThis; this.this_jsvalue = .zero; this.keep_alive.disable(); loop.enter(); _ = resolve.call(globalThis, .js_undefined, &.{ JSValue.jsNumberFromU16(exit_code), this.getBufferedStdout(globalThis), this.getBufferedStderr(globalThis), }) catch |err| globalThis.reportActiveExceptionAsUnhandled(err); JSC.Codegen.JSShellInterpreter.resolveSetCached(this_jsvalue, globalThis, .js_undefined); JSC.Codegen.JSShellInterpreter.rejectSetCached(this_jsvalue, globalThis, .js_undefined); loop.exit(); } } } else { this.flags.done = true; this.exit_code = exit_code; } return .done; } fn deinitAfterJSRun(this: *ThisInterpreter) void { log("Interpreter(0x{x}) deinitAfterJSRun", .{@intFromPtr(this)}); for (this.jsobjs) |jsobj| { jsobj.unprotect(); } this.root_io.deref(); this.keep_alive.disable(); this.root_shell.deinitImpl(false, false); this.this_jsvalue = .zero; } fn deinitFromFinalizer(this: *ThisInterpreter) void { if (this.root_shell._buffered_stderr == .owned) { this.root_shell._buffered_stderr.owned.deinitWithAllocator(bun.default_allocator); } if (this.root_shell._buffered_stdout == .owned) { this.root_shell._buffered_stdout.owned.deinitWithAllocator(bun.default_allocator); } this.this_jsvalue = .zero; this.allocator.destroy(this); } fn deinitEverything(this: *ThisInterpreter) void { log("deinit interpreter", .{}); for (this.jsobjs) |jsobj| { jsobj.unprotect(); } this.root_io.deref(); this.root_shell.deinitImpl(false, true); for (this.vm_args_utf8.items[0..]) |str| { str.deinit(); } this.vm_args_utf8.deinit(); this.this_jsvalue = .zero; this.allocator.destroy(this); } pub fn setQuiet(this: *ThisInterpreter, _: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { log("Interpreter(0x{x}) setQuiet()", .{@intFromPtr(this)}); this.flags.quiet = true; return .js_undefined; } pub fn setCwd(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const value = callframe.argument(0); const str = try bun.String.fromJS(value, globalThis); const slice = str.toUTF8(bun.default_allocator); defer slice.deinit(); switch (this.root_shell.changeCwd(this, slice.slice())) { .err => |e| { return globalThis.throwValue(e.toJS(globalThis)); }, .result => {}, } return .js_undefined; } pub fn setEnv(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { const value1 = callframe.argument(0); if (!value1.isObject()) { return globalThis.throwInvalidArguments("env must be an object", .{}); } var object_iter = JSC.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true, }).init(globalThis, value1); defer object_iter.deinit(); this.root_shell.export_env.clearRetainingCapacity(); this.root_shell.export_env.ensureTotalCapacity(object_iter.len); // If the env object does not include a $PATH, it must disable path lookup for argv[0] // PATH = ""; while (object_iter.next()) |key| { const keyslice = key.toOwnedSlice(bun.default_allocator) catch bun.outOfMemory(); var value = object_iter.value; if (value.isUndefined()) continue; const value_str = value.getZigString(globalThis); const slice = value_str.toOwnedSlice(bun.default_allocator) catch bun.outOfMemory(); const keyref = EnvStr.initRefCounted(keyslice); defer keyref.deref(); const valueref = EnvStr.initRefCounted(slice); defer valueref.deref(); this.root_shell.export_env.insert(keyref, valueref); } return .js_undefined; } pub fn isRunning(this: *ThisInterpreter, _: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue { return JSC.JSValue.jsBoolean(this.hasPendingActivity()); } pub fn getStarted(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { _ = globalThis; // autofix _ = callframe; // autofix return JSC.JSValue.jsBoolean(this.started.load(.seq_cst)); } pub fn getBufferedStdout(this: *ThisInterpreter, globalThis: *JSGlobalObject) JSC.JSValue { return ioToJSValue(globalThis, this.root_shell.buffered_stdout()); } pub fn getBufferedStderr(this: *ThisInterpreter, globalThis: *JSGlobalObject) JSC.JSValue { return ioToJSValue(globalThis, this.root_shell.buffered_stderr()); } pub fn finalize(this: *ThisInterpreter) void { log("Interpreter(0x{x}) finalize", .{@intFromPtr(this)}); this.deinitFromFinalizer(); } pub fn hasPendingActivity(this: *ThisInterpreter) bool { return this.has_pending_activity.load(.seq_cst) > 0; } fn incrPendingActivityFlag(has_pending_activity: *std.atomic.Value(u32)) void { _ = has_pending_activity.fetchAdd(1, .seq_cst); log("Interpreter incr pending activity {d}", .{has_pending_activity.load(.seq_cst)}); } fn decrPendingActivityFlag(has_pending_activity: *std.atomic.Value(u32)) void { _ = has_pending_activity.fetchSub(1, .seq_cst); log("Interpreter decr pending activity {d}", .{has_pending_activity.load(.seq_cst)}); } pub fn rootIO(this: *const Interpreter) *const IO { return &this.root_io; } pub fn getVmArgsUtf8(this: *Interpreter, argv: []const *WTFStringImplStruct, idx: u8) []const u8 { if (this.vm_args_utf8.items.len != argv.len) { this.vm_args_utf8.ensureTotalCapacity(argv.len) catch bun.outOfMemory(); for (argv) |arg| { this.vm_args_utf8.append(arg.toUTF8(bun.default_allocator)) catch bun.outOfMemory(); } } return this.vm_args_utf8.items[idx].slice(); } const ExpansionOpts = struct { for_spawn: bool = true, single: bool = false, }; pub const Builtin = @import("./Builtin.zig"); /// TODO: Investigate whether or not this can be removed now that we have /// removed recursion pub const AsyncDeinitReader = IOReader.AsyncDeinitReader; pub const IO = @import("./IO.zig"); pub const IOReader = @import("./IOReader.zig"); pub const IOReaderChildPtr = IOReader.ChildPtr; pub const IOWriter = @import("./IOWriter.zig"); pub const AsyncDeinitWriter = IOWriter.AsyncDeinitWriter; }; /// Construct a tagged union of the state nodes provided in `TypesValue`. /// The returned type has functions to call state node functions on the underlying type. /// /// A state node must implement the following functions: /// - `.start()` /// - `.deinit()` /// - `.childDone()` /// /// In addition, a state node struct must declare a `pub const ChildPtr = StatePtrUnion(...)` variable. /// This `ChildPtr` variable declares all the possible state nodes that can be a *child* of the state node. pub fn StatePtrUnion(comptime TypesValue: anytype) type { return struct { ptr: Ptr, const Ptr = TaggedPointerUnion(TypesValue); pub fn getChildPtrType(comptime Type: type) type { if (Type == Interpreter) return Interpreter.InterpreterChildPtr; if (!@hasDecl(Type, "ChildPtr")) { @compileError(@typeName(Type) ++ " does not have ChildPtr aksjdflkasjdflkasdjf"); } return Type.ChildPtr; } pub fn scopedAllocator(this: @This()) if (bun.Environment.enableAllocScopes) *bun.AllocationScope else void { if (comptime !bun.Environment.enableAllocScopes) return; const tags = comptime std.meta.fields(Ptr.Tag); inline for (tags) |tag| { if (this.tagInt() == tag.value) { const Ty = comptime Ptr.typeFromTag(tag.value); Ptr.assert_type(Ty); var casted = this.as(Ty); if (comptime Ty == Interpreter) { return &casted.__alloc_scope; } return casted.base.__alloc_scope.scopedAllocator(); } } unknownTag(this.tagInt()); } pub fn allocator(this: @This()) std.mem.Allocator { const tags = comptime std.meta.fields(Ptr.Tag); inline for (tags) |tag| { if (this.tagInt() == tag.value) { const Ty = comptime Ptr.typeFromTag(tag.value); Ptr.assert_type(Ty); var casted = this.as(Ty); if (comptime Ty == Interpreter) { if (bun.Environment.enableAllocScopes) return casted.__alloc_scope.allocator(); return bun.default_allocator; } return casted.base.allocator(); } } unknownTag(this.tagInt()); } pub fn create(this: @This(), comptime Ty: type) *Ty { if (comptime bun.Environment.enableAllocScopes) { return this.allocator().create(Ty) catch bun.outOfMemory(); } return bun.default_allocator.create(Ty) catch bun.outOfMemory(); } pub fn destroy(this: @This(), ptr: anytype) void { if (comptime bun.Environment.enableAllocScopes) { this.allocator().destroy(ptr); } else { bun.default_allocator.destroy(ptr); } } /// Starts the state node. pub fn start(this: @This()) Yield { const tags = comptime std.meta.fields(Ptr.Tag); inline for (tags) |tag| { if (this.tagInt() == tag.value) { const Ty = comptime Ptr.typeFromTag(tag.value); Ptr.assert_type(Ty); var casted = this.as(Ty); return casted.start(); } } unknownTag(this.tagInt()); } /// Deinitializes the state node pub fn deinit(this: @This()) void { const tags = comptime std.meta.fields(Ptr.Tag); inline for (tags) |tag| { if (this.tagInt() == tag.value) { const Ty = comptime Ptr.typeFromTag(tag.value); Ptr.assert_type(Ty); var casted = this.as(Ty); casted.deinit(); return; } } unknownTag(this.tagInt()); } /// Signals to the state node that one of its children completed with the /// given exit code pub fn childDone(this: @This(), child: anytype, exit_code: ExitCode) Yield { const tags = comptime std.meta.fields(Ptr.Tag); inline for (tags) |tag| { if (this.tagInt() == tag.value) { const Ty = comptime Ptr.typeFromTag(tag.value); Ptr.assert_type(Ty); const child_ptr = brk: { const ChildPtr = getChildPtrType(Ty); break :brk ChildPtr.init(child); }; var casted = this.as(Ty); return casted.childDone(child_ptr, exit_code); } } unknownTag(this.tagInt()); } pub fn unknownTag(tag: Ptr.TagInt) noreturn { return bun.Output.panic("Unknown tag for shell state node: {d}\n", .{tag}); } pub fn tagInt(this: @This()) Ptr.TagInt { return @intFromEnum(this.ptr.tag()); } pub fn tagName(this: @This()) []const u8 { return Ptr.typeNameFromTag(this.tagInt()).?; } pub fn init(_ptr: anytype) @This() { const tyinfo = @typeInfo(@TypeOf(_ptr)); if (tyinfo != .pointer) @compileError("Only pass pointers to StatePtrUnion.init(), you gave us a: " ++ @typeName(@TypeOf(_ptr))); const Type = std.meta.Child(@TypeOf(_ptr)); Ptr.assert_type(Type); return .{ .ptr = Ptr.init(_ptr) }; } pub inline fn as(this: @This(), comptime Type: type) *Type { return this.ptr.as(Type); } }; } pub fn MaybeChild(comptime T: type) type { return switch (@typeInfo(T)) { .Array => |info| info.child, .Vector => |info| info.child, .pointer => |info| info.child, .Optional => |info| info.child, else => T, }; } pub fn closefd(fd: bun.FileDescriptor) void { if (fd.closeAllowingBadFileDescriptor(null)) |err| { log("ERR closefd: {}\n", .{err}); } } const CmdEnvIter = struct { env: *const bun.StringArrayHashMap([:0]const u8), iter: bun.StringArrayHashMap([:0]const u8).Iterator, const Entry = struct { key: Key, value: Value, }; const Value = struct { val: [:0]const u8, pub fn format(self: Value, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { try writer.writeAll(self.val); } }; const Key = struct { val: []const u8, pub fn format(self: Key, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { try writer.writeAll(self.val); } pub fn eqlComptime(this: Key, comptime str: []const u8) bool { return bun.strings.eqlComptime(this.val, str); } }; pub fn fromEnv(env: *const bun.StringArrayHashMap([:0]const u8)) CmdEnvIter { const iter = env.iterator(); return .{ .env = env, .iter = iter, }; } pub fn len(self: *const CmdEnvIter) usize { return self.env.unmanaged.entries.len; } pub fn next(self: *CmdEnvIter) !?Entry { const entry = self.iter.next() orelse return null; return .{ .key = .{ .val = entry.key_ptr.* }, .value = .{ .val = entry.value_ptr.* }, }; } }; /// A concurrent task, the idea is that this task is not heap allocated because /// it will be in a field of one of the Shell state structs which will be heap /// allocated. pub fn ShellTask( comptime Ctx: type, /// Function to be called when the thread pool starts the task, this could /// be on anyone of the thread pool threads so be mindful of concurrency /// nuances comptime runFromThreadPool_: fn (*Ctx) void, /// Function that is called on the main thread, once the event loop /// processes that the task is done comptime runFromMainThread_: fn (*Ctx) void, comptime debug: bun.Output.LogFunction, ) type { return struct { task: WorkPoolTask = .{ .callback = &runFromThreadPool }, event_loop: JSC.EventLoopHandle, // This is a poll because we want it to enter the uSockets loop ref: bun.Async.KeepAlive = .{}, concurrent_task: JSC.EventLoopTask, pub const InnerShellTask = @This(); pub fn schedule(this: *@This()) void { debug("schedule", .{}); this.ref.ref(this.event_loop); WorkPool.schedule(&this.task); } pub fn onFinish(this: *@This()) void { debug("onFinish", .{}); if (this.event_loop == .js) { const ctx: *Ctx = @fieldParentPtr("task", this); this.event_loop.js.enqueueTaskConcurrent(this.concurrent_task.js.from(ctx, .manual_deinit)); } else { const ctx = this; this.event_loop.mini.enqueueTaskConcurrent(this.concurrent_task.mini.from(ctx, "runFromMainThreadMini")); } } pub fn runFromThreadPool(task: *WorkPoolTask) void { debug("runFromThreadPool", .{}); var this: *@This() = @fieldParentPtr("task", task); const ctx: *Ctx = @fieldParentPtr("task", this); runFromThreadPool_(ctx); this.onFinish(); } pub fn runFromMainThread(this: *@This()) void { debug("runFromJS", .{}); const ctx: *Ctx = @fieldParentPtr("task", this); this.ref.unref(this.event_loop); runFromMainThread_(ctx); } pub fn runFromMainThreadMini(this: *@This(), _: *void) void { this.runFromMainThread(); } }; } inline fn errnocast(errno: anytype) u16 { return @intCast(errno); } /// 'js' event loop will always return JSError /// 'mini' event loop will always return noreturn and exit 1 pub fn throwShellErr(e: *const bun.shell.ShellErr, event_loop: JSC.EventLoopHandle) bun.JSError!noreturn { return switch (event_loop) { .mini => e.throwMini(), .js => e.throwJS(event_loop.js.global), }; } pub const ReadChunkAction = enum { stop_listening, cont, }; pub const IOWriterChildPtr = Interpreter.IOWriter.ChildPtr; /// Shell modifications for syscalls, mostly to make windows work: /// - Any function that returns a file descriptor will return a uv file descriptor /// - Sometimes windows doesn't have `*at()` functions like `rmdirat` so we have to join the directory path with the target path /// - Converts Posix absolute paths to Windows absolute paths on Windows pub const ShellSyscall = struct { pub const unlinkatWithFlags = Syscall.unlinkatWithFlags; pub const rmdirat = Syscall.rmdirat; pub fn getPath(dirfd: anytype, to: [:0]const u8, buf: *bun.PathBuffer) Maybe([:0]const u8) { if (bun.Environment.isPosix) @compileError("Don't use this"); if (bun.strings.eqlComptime(to[0..to.len], "/dev/null")) { return .{ .result = shell.WINDOWS_DEV_NULL }; } if (ResolvePath.Platform.posix.isAbsolute(to[0..to.len])) { const dirpath = brk: { if (@TypeOf(dirfd) == bun.FileDescriptor) break :brk switch (Syscall.getFdPath(dirfd, buf)) { .result => |path| path, .err => |e| return .{ .err = e.withFd(dirfd) }, }; break :brk dirfd; }; const source_root = ResolvePath.windowsFilesystemRoot(dirpath); std.mem.copyForwards(u8, buf[0..source_root.len], source_root); @memcpy(buf[source_root.len..][0 .. to.len - 1], to[1..]); buf[source_root.len + to.len - 1] = 0; return .{ .result = buf[0 .. source_root.len + to.len - 1 :0] }; } if (ResolvePath.Platform.isAbsolute(.windows, to[0..to.len])) return .{ .result = to }; const dirpath = brk: { if (@TypeOf(dirfd) == bun.FileDescriptor) break :brk switch (Syscall.getFdPath(dirfd, buf)) { .result => |path| path, .err => |e| return .{ .err = e.withFd(dirfd) }, }; @memcpy(buf[0..dirfd.len], dirfd[0..dirfd.len]); break :brk buf[0..dirfd.len]; }; const parts: []const []const u8 = &.{ dirpath[0..dirpath.len], to[0..to.len], }; const joined = ResolvePath.joinZBuf(buf, parts, .auto); return .{ .result = joined }; } pub fn statat(dir: bun.FileDescriptor, path_: [:0]const u8) Maybe(bun.Stat) { if (bun.Environment.isWindows) { const buf: *bun.PathBuffer = bun.path_buffer_pool.get(); defer bun.path_buffer_pool.put(buf); const path = switch (getPath(dir, path_, buf)) { .err => |e| return .{ .err = e }, .result => |p| p, }; return switch (Syscall.stat(path)) { .err => |e| .{ .err = e.clone(bun.default_allocator) }, .result => |s| .{ .result = s }, }; } return Syscall.fstatat(dir, path_); } /// Same thing as bun.sys.openat on posix /// On windows it will convert paths for us pub fn openat(dir: bun.FileDescriptor, path: [:0]const u8, flags: i32, perm: bun.Mode) Maybe(bun.FileDescriptor) { if (bun.Environment.isWindows) { if (flags & bun.O.DIRECTORY != 0) { if (ResolvePath.Platform.posix.isAbsolute(path[0..path.len])) { const buf: *bun.PathBuffer = bun.path_buffer_pool.get(); defer bun.path_buffer_pool.put(buf); const p = switch (getPath(dir, path, buf)) { .result => |p| p, .err => |e| return .{ .err = e }, }; return switch (Syscall.openDirAtWindowsA(dir, p, .{ .iterable = true, .no_follow = flags & bun.O.NOFOLLOW != 0 })) { .result => |fd| fd.makeLibUVOwnedForSyscall(.open, .close_on_fail), .err => |e| .{ .err = e.withPath(path) }, }; } return switch (Syscall.openDirAtWindowsA(dir, path, .{ .iterable = true, .no_follow = flags & bun.O.NOFOLLOW != 0 })) { .result => |fd| fd.makeLibUVOwnedForSyscall(.open, .close_on_fail), .err => |e| .{ .err = e.withPath(path) }, }; } const buf: *bun.PathBuffer = bun.path_buffer_pool.get(); defer bun.path_buffer_pool.put(buf); const p = switch (getPath(dir, path, buf)) { .result => |p| p, .err => |e| return .{ .err = e }, }; return bun.sys.open(p, flags, perm); } const fd = switch (Syscall.openat(dir, path, flags, perm)) { .result => |fd| fd, .err => |e| return .{ .err = e.withPath(path) }, }; if (bun.Environment.isWindows) { return fd.makeLibUVOwnedForSyscall(.open, .close_on_fail); } return .{ .result = fd }; } pub fn open(file_path: [:0]const u8, flags: bun.Mode, perm: bun.Mode) Maybe(bun.FileDescriptor) { const fd = switch (Syscall.open(file_path, flags, perm)) { .result => |fd| fd, .err => |e| return .{ .err = e }, }; if (bun.Environment.isWindows) { return fd.makeLibUVOwnedForSyscall(.open, .close_on_fail); } return .{ .result = fd }; } pub fn dup(fd: bun.FileDescriptor) Maybe(bun.FileDescriptor) { if (bun.Environment.isWindows) { return switch (Syscall.dup(fd)) { .result => |duped_fd| duped_fd.makeLibUVOwnedForSyscall(.dup, .close_on_fail), .err => |e| .{ .err = e }, }; } return Syscall.dup(fd); } }; /// A task that can write to stdout and/or stderr pub fn OutputTask( comptime Parent: type, comptime vtable: struct { writeErr: *const fn (*Parent, childptr: anytype, []const u8) ?Yield, onWriteErr: *const fn (*Parent) void, writeOut: *const fn (*Parent, childptr: anytype, *OutputSrc) ?Yield, onWriteOut: *const fn (*Parent) void, onDone: *const fn (*Parent) Yield, }, ) type { return struct { parent: *Parent, output: OutputSrc, state: enum { waiting_write_err, waiting_write_out, done, }, pub fn deinit(this: *@This()) Yield { if (comptime bun.Environment.allow_assert) assert(this.state == .done); log("OutputTask({s}, 0x{x}) deinit", .{ @typeName(Parent), @intFromPtr(this) }); defer bun.destroy(this); defer this.output.deinit(); return vtable.onDone(this.parent); } pub fn start(this: *@This(), errbuf: ?[]const u8) Yield { log("OutputTask({s}, 0x{x}) start errbuf={s}", .{ @typeName(Parent), @intFromPtr(this), if (errbuf) |err| err[0..@min(128, err.len)] else "null" }); this.state = .waiting_write_err; if (errbuf) |err| { if (vtable.writeErr(this.parent, this, err)) |yield| return yield; return this.next(); } this.state = .waiting_write_out; if (vtable.writeOut(this.parent, this, &this.output)) |yield| return yield; vtable.onWriteOut(this.parent); this.state = .done; return this.deinit(); } pub fn next(this: *@This()) Yield { switch (this.state) { .waiting_write_err => { vtable.onWriteErr(this.parent); this.state = .waiting_write_out; if (vtable.writeOut(this.parent, this, &this.output)) |yield| return yield; vtable.onWriteOut(this.parent); this.state = .done; return this.deinit(); }, .waiting_write_out => { vtable.onWriteOut(this.parent); this.state = .done; return this.deinit(); }, .done => @panic("Invalid state"), } } pub fn onIOWriterChunk(this: *@This(), _: usize, err: ?JSC.SystemError) Yield { log("OutputTask({s}, 0x{x}) onIOWriterChunk", .{ @typeName(Parent), @intFromPtr(this) }); if (err) |e| { e.deref(); } switch (this.state) { .waiting_write_err => { vtable.onWriteErr(this.parent); this.state = .waiting_write_out; if (vtable.writeOut(this.parent, this, &this.output)) |yield| return yield; vtable.onWriteOut(this.parent); this.state = .done; return this.deinit(); }, .waiting_write_out => { vtable.onWriteOut(this.parent); this.state = .done; return this.deinit(); }, .done => @panic("Invalid state"), } } }; } /// All owned memory is assumed to be allocated with `bun.default_allocator` pub const OutputSrc = union(enum) { arrlist: std.ArrayListUnmanaged(u8), owned_buf: []const u8, borrowed_buf: []const u8, pub fn slice(this: *OutputSrc) []const u8 { return switch (this.*) { .arrlist => this.arrlist.items[0..], .owned_buf => this.owned_buf, .borrowed_buf => this.borrowed_buf, }; } pub fn deinit(this: *OutputSrc) void { switch (this.*) { .arrlist => { this.arrlist.deinit(bun.default_allocator); }, .owned_buf => { bun.default_allocator.free(this.owned_buf); }, .borrowed_buf => {}, } } }; /// Custom parse error for invalid options pub const ParseError = union(enum) { illegal_option: []const u8, unsupported: []const u8, show_usage, }; pub fn unsupportedFlag(comptime name: []const u8) []const u8 { return "unsupported option, please open a GitHub issue -- " ++ name ++ "\n"; } pub const ParseFlagResult = union(enum) { continue_parsing, done, illegal_option: []const u8, unsupported: []const u8, show_usage }; pub fn FlagParser(comptime Opts: type) type { return struct { pub const Result = @import("../result.zig").Result; pub fn parseFlags(opts: Opts, args: []const [*:0]const u8) Result(?[]const [*:0]const u8, ParseError) { var idx: usize = 0; if (args.len == 0) { return .{ .ok = null }; } while (idx < args.len) : (idx += 1) { const flag = args[idx]; switch (parseFlag(opts, flag[0..std.mem.len(flag)])) { .done => { const filepath_args = args[idx..]; return .{ .ok = filepath_args }; }, .continue_parsing => {}, .illegal_option => |opt_str| return .{ .err = .{ .illegal_option = opt_str } }, .unsupported => |unsp| return .{ .err = .{ .unsupported = unsp } }, .show_usage => return .{ .err = .show_usage }, } } return .{ .err = .show_usage }; } pub fn parseFlag(opts: Opts, flag: []const u8) ParseFlagResult { if (flag.len == 0) return .done; if (flag[0] != '-') return .done; if (flag.len == 1) return .{ .illegal_option = "-" }; if (flag.len > 2 and flag[1] == '-') { if (opts.parseLong(flag)) |result| return result; } const small_flags = flag[1..]; for (small_flags, 0..) |char, i| { if (opts.parseShort(char, small_flags, i)) |err| { return err; } } return .continue_parsing; } }; } pub fn isPollable(fd: bun.FileDescriptor, mode: bun.Mode) bool { return switch (bun.Environment.os) { .windows, .wasm => false, .linux => posix.S.ISFIFO(mode) or posix.S.ISSOCK(mode) or posix.isatty(fd.native()), // macos DOES allow regular files to be pollable, but we don't want that because // our IOWriter code has a separate and better codepath for writing to files. .mac => if (posix.S.ISREG(mode)) false else posix.S.ISFIFO(mode) or posix.S.ISSOCK(mode) or posix.isatty(fd.native()), }; } pub fn isPollableFromMode(mode: bun.Mode) bool { return switch (bun.Environment.os) { .windows, .wasm => false, .linux => posix.S.ISFIFO(mode) or posix.S.ISSOCK(mode), // macos DOES allow regular files to be pollable, but we don't want that because // our IOWriter code has a separate and better codepath for writing to files. .mac => if (posix.S.ISREG(mode)) false else posix.S.ISFIFO(mode) or posix.S.ISSOCK(mode), }; } pub fn unreachableState(context: []const u8, state: []const u8) noreturn { @branchHint(.cold); return bun.Output.panic("Bun shell has reached an unreachable state \"{s}\" in the {s} context. This indicates a bug, please open a GitHub issue.", .{ state, context }); }