diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/termio/Exec.zig | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff) | |
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/termio/Exec.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/termio/Exec.zig | 320 |
1 files changed, 187 insertions, 133 deletions
diff --git a/vendor/libghostty-vt/src/termio/Exec.zig b/vendor/libghostty-vt/src/termio/Exec.zig index 0894f87b..01629171 100644 --- a/vendor/libghostty-vt/src/termio/Exec.zig +++ b/vendor/libghostty-vt/src/termio/Exec.zig @@ -9,7 +9,8 @@ const assert = @import("../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const posix = std.posix; -const xev = @import("../global.zig").xev; +const global = @import("../global.zig"); +const xev = global.xev; const apprt = @import("../apprt.zig"); const build_config = @import("../build_config.zig"); const configpkg = @import("../config.zig"); @@ -21,13 +22,13 @@ const shell_integration = @import("shell_integration.zig"); const terminal = @import("../terminal/main.zig"); const termio = @import("../termio.zig"); const Command = @import("../Command.zig"); -const SegmentedPool = @import("../datastruct/main.zig").SegmentedPool; const ptypkg = @import("../pty.zig"); const Pty = ptypkg.Pty; -const EnvMap = std.process.EnvMap; +const EnvMap = std.process.Environ.Map; const PasswdEntry = internal_os.passwd.Entry; const windows = internal_os.windows; const ProcessInfo = @import("../pty.zig").ProcessInfo; +const compat_fd = @import("../lib/compat/fd.zig"); const log = std.log.scoped(.io_exec); @@ -98,7 +99,7 @@ pub fn threadEnter( // We're in the child. Nothing more we can do but abnormal exit. // The Command will output some additional information. - posix.exit(1); + std.process.exit(1); }; errdefer self.subprocess.stop(); @@ -117,13 +118,13 @@ pub fn threadEnter( errdefer if (process) |*p| p.deinit(); // Track our process start time for abnormal exits - const process_start = try std.time.Instant.now(); + const process_start: std.Io.Timestamp = .now(global.io(), .awake); // Create our pipe that we'll use to kill our read thread. // pipe[0] is the read end, pipe[1] is the write end. const pipe = try internal_os.pipe(); - errdefer posix.close(pipe[0]); - errdefer posix.close(pipe[1]); + errdefer _ = posix.system.close(pipe[0]); + errdefer _ = posix.system.close(pipe[1]); // Setup our stream so that we can write. var stream = xev.Stream.initFd(pty_fds.write); @@ -141,7 +142,7 @@ pub fn threadEnter( if (builtin.os.tag == .windows) ReadThread.threadMainWindows else ReadThread.threadMainPosix, .{ pty_fds.read, io, pipe[0] }, ); - read_thread.setName("io-reader") catch {}; + read_thread.setName(global.io(), "io-reader") catch {}; // Setup our threadata backend state to be our own td.backend = .{ .exec = .{ @@ -202,22 +203,23 @@ pub fn threadExit(self: *Exec, td: *termio.Termio.ThreadData) void { // Quit our read thread after exiting the subprocess so that // we don't get stuck waiting for data to stop flowing if it is // a particularly noisy process. - _ = posix.write(exec.read_thread_pipe, "x") catch |err| switch (err) { - // BrokenPipe means that our read thread is closed already, - // which is completely fine since that is what we were trying - // to achieve. - error.BrokenPipe => {}, - - else => log.warn( - "error writing to read thread quit pipe err={}", - .{err}, + switch (posix.errno(posix.system.write(exec.read_thread_pipe, "x", 1))) { + .SUCCESS => {}, + + // EPIPE means that our read thread is closed already, which is + // completely fine since that is what we were trying to achieve. + .PIPE => {}, + + else => |e| log.warn( + "error writing to read thread quit pipe err=E{s}", + .{@tagName(e)}, ), - }; + } if (comptime builtin.os.tag == .windows) { // Interrupt the blocking read so the thread can see the quit message - if (windows.kernel32.CancelIoEx(exec.read_thread_fd, null) == 0) { - switch (windows.kernel32.GetLastError()) { + if (windows.exp.kernel32.CancelIoEx(exec.read_thread_fd, null) == windows.FALSE) { + switch (windows.GetLastError()) { .NOT_FOUND => {}, else => |err| log.warn("error interrupting read thread err={}", .{err}), } @@ -275,23 +277,18 @@ fn processExitCommon(td: *termio.Termio.ThreadData, exit_code: u32) void { execdata.exited = true; // Determine how long the process was running for. - const runtime_ms: ?u64 = runtime: { - const process_end = std.time.Instant.now() catch break :runtime null; - const runtime_ns = process_end.since(execdata.start); - const runtime_ms = runtime_ns / std.time.ns_per_ms; - break :runtime runtime_ms; - }; - log.debug( - "child process exited status={} runtime={}ms", - .{ exit_code, runtime_ms orelse 0 }, + const runtime_ms: u64 = @max( + 0, + execdata.start.untilNow(global.io(), .awake).toMilliseconds(), ); + log.debug("child process exited status={} runtime={}ms", .{ exit_code, runtime_ms }); // We always notify the surface immediately that the child has // exited and some metadata about the exit. _ = td.surface_mailbox.push(.{ .child_exited = .{ .exit_code = exit_code, - .runtime_ms = runtime_ms orelse 0, + .runtime_ms = runtime_ms, }, }, .{ .forever = {} }); } @@ -372,8 +369,8 @@ fn termiosTimer( // If our password input state changed on the terminal then // we notify the surface. { - td.renderer_state.mutex.lock(); - defer td.renderer_state.mutex.unlock(); + td.renderer_state.mutex.lockUncancelable(global.io()); + defer td.renderer_state.mutex.unlock(global.io()); const t = td.renderer_state.terminal; if (t.flags.password_input == password_input) { break :mode_change; @@ -420,8 +417,9 @@ pub fn queueWrite( // our cached buffers that we can queue to the stream. var i: usize = 0; while (i < data.len) { - const req = try exec.write_req_pool.getGrow(alloc); - const buf = try exec.write_buf_pool.getGrow(alloc); + const w = try exec.write_pool.create(alloc); + w.td = exec; + const buf = &w.buf; const slice = slice: { // The maximum end index is either the end of our data or // the end of our buffer, whichever is smaller. @@ -461,26 +459,25 @@ pub fn queueWrite( exec.write_stream.queueWrite( td.loop, &exec.write_queue, - req, + &w.req, .{ .slice = slice }, - termio.Exec.ThreadData, - exec, + ThreadData.Write, + w, ttyWrite, ); } } fn ttyWrite( - td_: ?*ThreadData, + w_: ?*ThreadData.Write, _: *xev.Loop, _: *xev.Completion, _: xev.Stream, _: xev.WriteBuffer, r: xev.WriteError!usize, ) xev.CallbackAction { - const td = td_.?; - td.write_req_pool.put(); - td.write_buf_pool.put(); + const w = w_.?; + w.td.write_pool.destroy(w); const d = r catch |err| { log.err("write error: {}", .{err}); @@ -494,12 +491,24 @@ fn ttyWrite( /// The thread local data for the exec implementation. pub const ThreadData = struct { - // The preallocation size for the write request pool. This should be big - // enough to satisfy most write requests. It must be a power of 2. - const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5); + /// The state for a single queued pty write. The write request and + /// the buffer it writes from must both remain pointer-stable until + /// the write completes, so they're pooled together and checked out + /// per write. + pub const Write = struct { + /// Backpointer to the thread data so the write completion + /// callback can put this back into the pool. + td: *ThreadData, + + /// The libxev write request. + req: xev.WriteRequest, + + /// The buffer for the data being written. + buf: [64]u8, + }; /// Process start time and boolean of whether its already exited. - start: std.time.Instant, + start: std.Io.Timestamp, exited: bool = false, /// The data stream is the main IO for the pty. @@ -508,12 +517,9 @@ pub const ThreadData = struct { /// The process watcher process: ?xev.Process, - /// This is the pool of available (unused) write requests. If you grab + /// This is the pool of available (unused) write states. If you grab /// one from the pool, you must put it back when you're done! - write_req_pool: SegmentedPool(xev.WriteRequest, WRITE_REQ_PREALLOC) = .{}, - - /// The pool of available buffers for writing to the pty. - write_buf_pool: SegmentedPool([64]u8, WRITE_REQ_PREALLOC) = .{}, + write_pool: std.heap.MemoryPool(Write) = .empty, /// The write queue for the data stream. write_queue: xev.WriteQueue = .{}, @@ -541,13 +547,12 @@ pub const ThreadData = struct { termios_mode: ptypkg.Mode = .{}, pub fn deinit(self: *ThreadData, alloc: Allocator) void { - posix.close(self.read_thread_pipe); + _ = posix.system.close(self.read_thread_pipe); - // Clear our write pools. We know we aren't ever going to do + // Clear our write pool. We know we aren't ever going to do // any more IO since we stop our data stream below so we can just // drop this. - self.write_req_pool.deinit(alloc); - self.write_buf_pool.deinit(alloc); + self.write_pool.deinit(alloc); // Stop our process watcher if (self.process) |*p| p.deinit(); @@ -671,10 +676,13 @@ const Subprocess = struct { } var exe_buf: [std.fs.max_path_bytes]u8 = undefined; - const exe_bin_path = std.fs.selfExePath(&exe_buf) catch |err| { + const exe_bin_path = exe_buf[0 .. std.process.executablePath( + global.io(), + &exe_buf, + ) catch |err| { log.warn("failed to get ghostty exe path err={}", .{err}); break :ghostty_path; - }; + }]; const exe_dir = std.fs.path.dirname(exe_bin_path) orelse break :ghostty_path; log.debug("appending ghostty bin to path dir={s}", .{exe_dir}); @@ -695,7 +703,7 @@ const Subprocess = struct { try env.put( "PATH", - try internal_os.appendEnv(alloc, path, exe_dir), + try appendEnv(alloc, path, exe_dir), ); } else { try env.put("PATH", exe_dir); @@ -713,7 +721,7 @@ const Subprocess = struct { if (std.fmt.bufPrint(&buf, "{s}/..", .{resources_dir})) |data_dir| { try env.put( xdg_data_dir_key, - try internal_os.appendEnv( + try appendEnv( alloc, env.get(xdg_data_dir_key) orelse "/usr/local/share:/usr/share", data_dir, @@ -730,7 +738,7 @@ const Subprocess = struct { // path instead of overriding all paths set by OS. try env.put( manpath_key, - try internal_os.appendEnvAlways( + try appendEnvAlways( alloc, env.get(manpath_key) orelse "", man_dir, @@ -749,7 +757,7 @@ const Subprocess = struct { // VTE_VERSION is set by gnome-terminal and other VTE-based terminals. // We don't want our child processes to think we're running under VTE. // This is not apprt-specific, so we do it here. - env.remove("VTE_VERSION"); + _ = env.orderedRemove("VTE_VERSION"); // Setup our shell integration, if we can. const shell_command: configpkg.Command = shell: { @@ -907,7 +915,7 @@ const Subprocess = struct { self.pty = pty; errdefer if (!in_child) { if (comptime builtin.os.tag != .windows) { - _ = posix.close(pty.slave); + _ = posix.system.close(pty.slave); } pty.deinit(); @@ -921,7 +929,7 @@ const Subprocess = struct { // Once our subcommand is started we can close the slave // side. This prevents the slave fd from being leaked to // future children. - _ = posix.close(pty.slave); + _ = posix.system.close(pty.slave); } // Successful start we can clear out some memory. @@ -944,8 +952,12 @@ const Subprocess = struct { // // https://docs.flatpak.org/en/latest/sandbox-permissions.html#reserved-paths log.info("flatpak detected, will use host command to verify cwd access", .{}); - const dev_null = try std.fs.cwd().openFile("/dev/null", .{ .mode = .read_write }); - defer dev_null.close(); + const dev_null = try std.Io.Dir.cwd().openFile( + global.io(), + "/dev/null", + .{ .mode = .read_write }, + ); + defer dev_null.close(global.io()); var cmd: internal_os.FlatpakHostCommand = .{ .argv = &[_][]const u8{ "/bin/sh", @@ -966,7 +978,7 @@ const Subprocess = struct { break :cwd proposed; } - if (std.fs.cwd().access(proposed, .{})) { + if (std.Io.Dir.cwd().access(global.io(), proposed, .{})) { break :cwd proposed; } else |err| { log.warn("cannot access cwd, ignoring: {}", .{err}); @@ -1011,9 +1023,18 @@ const Subprocess = struct { .args = self.args, .env = if (self.env) |*env| env else null, .cwd = cwd, - .stdin = if (builtin.os.tag == .windows) null else .{ .handle = pty.slave }, - .stdout = if (builtin.os.tag == .windows) null else .{ .handle = pty.slave }, - .stderr = if (builtin.os.tag == .windows) null else .{ .handle = pty.slave }, + .stdin = if (builtin.os.tag == .windows) null else .{ + .handle = pty.slave, + .flags = .{ .nonblocking = false }, + }, + .stdout = if (builtin.os.tag == .windows) null else .{ + .handle = pty.slave, + .flags = .{ .nonblocking = false }, + }, + .stderr = if (builtin.os.tag == .windows) null else .{ + .handle = pty.slave, + .flags = .{ .nonblocking = false }, + }, .pseudo_console = if (builtin.os.tag == .windows) pty.pseudo_console else {}, .os_pre_exec = switch (comptime builtin.os.tag) { .windows => null, @@ -1140,8 +1161,8 @@ const Subprocess = struct { if (command.pid) |pid| { switch (builtin.os.tag) { .windows => { - if (windows.kernel32.TerminateProcess(pid, 0) == 0) { - return windows.unexpectedError(windows.kernel32.GetLastError()); + if (windows.exp.kernel32.TerminateProcess(pid, 0) == windows.FALSE) { + return windows.unexpectedError(windows.GetLastError()); } _ = try command.wait(false); @@ -1182,10 +1203,14 @@ const Subprocess = struct { // The gist is that it lets us detect when children // are still alive without blocking so that we can // kill them again. - const res = posix.waitpid(pid, std.c.W.NOHANG); - log.debug("waitpid result={}", .{res.pid}); - if (res.pid != 0) break; - std.Thread.sleep(10 * std.time.ns_per_ms); + const res_pid = while (true) { + const rc = posix.system.waitpid(pid, null, std.c.W.NOHANG); + if (posix.errno(rc) == .INTR) continue; + break rc; + }; + log.debug("waitpid result={}", .{res_pid}); + if (res_pid != 0) break; + try std.Io.sleep(global.io(), .fromMilliseconds(10), .awake); } } @@ -1205,7 +1230,7 @@ const Subprocess = struct { const pgid = c.getpgid(pid); if (pgid == my_pgid) { log.warn("pgid is our own, retrying", .{}); - std.Thread.sleep(10 * std.time.ns_per_ms); + std.Io.sleep(global.io(), .fromMilliseconds(10), .awake) catch {}; continue; } @@ -1336,15 +1361,15 @@ pub const ReadThread = struct { /// stage at a time, so buffer contents need no locking. Only the /// ring metadata is guarded by the mutex. const Pipeline = struct { - mutex: std.Thread.Mutex = .{}, + mutex: std.Io.Mutex = .init, /// Signaled when a batch is published or the gather stage is /// done. Waited on by the parse stage. - batch_ready: std.Thread.Condition = .{}, + batch_ready: std.Io.Condition = .init, /// Signaled when a batch has been consumed. Waited on by the /// gather stage when all buffers are in flight (backpressure). - slot_free: std.Thread.Condition = .{}, + slot_free: std.Io.Condition = .init, /// The number of valid bytes in each buffer. Set at publish /// time by the gather stage, read by the parse stage. @@ -1385,7 +1410,7 @@ pub const ReadThread = struct { fn threadMainPosix(fd: posix.fd_t, io: *termio.Termio, quit: posix.fd_t) void { // Always close our end of the pipe when we exit. - defer posix.close(quit); + defer _ = posix.system.close(quit); // Right now, on Darwin, `std.Thread.setName` can only name the current // thread, and we have no way to get the current thread from within it, @@ -1419,7 +1444,7 @@ pub const ReadThread = struct { // can't create it we still run correctly, bridge polls are // just bounded by their timeout instead of being interrupted // when the parse stage goes idle. - if (posix.pipe2(.{ + if (compat_fd.pipe2(.{ .CLOEXEC = true, .NONBLOCK = true, })) |fds| { @@ -1429,8 +1454,8 @@ pub const ReadThread = struct { log.warn("read thread failed to create idle pipe err={}", .{err}); } defer if (pipeline.idle_read_fd >= 0) { - posix.close(pipeline.idle_read_fd); - posix.close(pipeline.idle_write_fd); + compat_fd.close(pipeline.idle_read_fd); + compat_fd.close(pipeline.idle_write_fd); }; const gather_thread = std.Thread.spawn( @@ -1446,7 +1471,7 @@ pub const ReadThread = struct { }; defer gather_thread.join(); if (comptime !builtin.os.tag.isDarwin()) { - gather_thread.setName("io-gather") catch {}; + gather_thread.setName(global.io(), "io-gather") catch {}; } // This thread is the parse stage. We consume batches in ring @@ -1454,11 +1479,11 @@ pub const ReadThread = struct { // the ring is drained. while (true) { const batch: []const u8 = batch: { - pipeline.mutex.lock(); - defer pipeline.mutex.unlock(); + pipeline.mutex.lockUncancelable(global.io()); + defer pipeline.mutex.unlock(global.io()); while (pipeline.count == 0) { if (pipeline.done) return; - pipeline.batch_ready.wait(&pipeline.mutex); + pipeline.batch_ready.waitUncancelable(global.io(), &pipeline.mutex); } const slot = pipeline.tail; break :batch pipeline.bufs[slot][0..pipeline.lens[slot]]; @@ -1469,27 +1494,27 @@ pub const ReadThread = struct { io.processOutput(batch); { - pipeline.mutex.lock(); + pipeline.mutex.lockUncancelable(global.io()); pipeline.tail = (pipeline.tail + 1) % buffer_count; pipeline.count -= 1; const wake = pipeline.count == 0 and pipeline.bridging and pipeline.idle_write_fd >= 0; - pipeline.mutex.unlock(); - pipeline.slot_free.signal(); + pipeline.mutex.unlock(global.io()); + pipeline.slot_free.signal(global.io()); // We ran out of batches while the gather stage is // bridging a refill gap: interrupt its poll so it // delivers what it has instead of sleeping out the // timeout while we sit idle. if (wake) { - _ = posix.write(pipeline.idle_write_fd, "i") catch {}; + _ = posix.system.write(pipeline.idle_write_fd, "i", 1); } } // Batch boundary: hand the renderer state mutex off if // the renderer is waiting. See renderer.State.lockDemand. - io.renderer_state.yieldToDemand(); + io.renderer_state.yieldToDemand(global.io()); } } @@ -1506,10 +1531,10 @@ pub const ReadThread = struct { // However we exit, tell the parse stage the stream is over so // it drains the ring and joins us. defer { - pipeline.mutex.lock(); + pipeline.mutex.lockUncancelable(global.io()); pipeline.done = true; - pipeline.mutex.unlock(); - pipeline.batch_ready.signal(); + pipeline.mutex.unlock(global.io()); + pipeline.batch_ready.signal(global.io()); } // The fds we poll: data on the pty, our quit notification, @@ -1528,16 +1553,16 @@ pub const ReadThread = struct { // we should stop reading and let the kernel queue exert // backpressure on the child. const buf: *[buffer_capacity]u8 = buf: { - pipeline.mutex.lock(); - defer pipeline.mutex.unlock(); + pipeline.mutex.lockUncancelable(global.io()); + defer pipeline.mutex.unlock(global.io()); while (pipeline.count == buffer_count) { - pipeline.slot_free.wait(&pipeline.mutex); + pipeline.slot_free.waitUncancelable(global.io(), &pipeline.mutex); } break :buf &pipeline.bufs[pipeline.head]; }; var total: usize = 0; - var bridge_start: ?std.time.Instant = null; + var bridge_start: ?std.Io.Timestamp = null; var spins: usize = 0; var fatal = false; @@ -1566,10 +1591,9 @@ pub const ReadThread = struct { // Still dry, so we want to sleep in poll for // the next refill, within our latency budget. - const now = std.time.Instant.now() catch - break :gather; + const now = std.Io.Timestamp.now(global.io(), .awake); if (bridge_start) |start| { - if (now.since(start) >= gather_budget_ns) + if (start.durationTo(now).toNanoseconds() >= gather_budget_ns) break :gather; } else bridge_start = now; @@ -1587,8 +1611,8 @@ pub const ReadThread = struct { // the idle wake so it can interrupt our poll // the moment that changes. { - pipeline.mutex.lock(); - defer pipeline.mutex.unlock(); + pipeline.mutex.lockUncancelable(global.io()); + defer pipeline.mutex.unlock(global.io()); if (pipeline.count == 0) break :gather; pipeline.bridging = true; } @@ -1671,12 +1695,12 @@ pub const ReadThread = struct { // Publish the batch (if any) to the parse stage and rotate // to the next buffer. if (total > 0) { - pipeline.mutex.lock(); + pipeline.mutex.lockUncancelable(global.io()); pipeline.lens[pipeline.head] = total; pipeline.head = (pipeline.head + 1) % buffer_count; pipeline.count += 1; - pipeline.mutex.unlock(); - pipeline.batch_ready.signal(); + pipeline.mutex.unlock(global.io()); + pipeline.batch_ready.signal(global.io()); } if (fatal) return; @@ -1710,8 +1734,8 @@ pub const ReadThread = struct { /// Clears the bridging flag armed before a bridge poll, closing /// the window in which the parse stage writes idle wakes. fn clearBridging(pipeline: *Pipeline) void { - pipeline.mutex.lock(); - defer pipeline.mutex.unlock(); + pipeline.mutex.lockUncancelable(global.io()); + defer pipeline.mutex.unlock(global.io()); pipeline.bridging = false; } @@ -1730,30 +1754,29 @@ pub const ReadThread = struct { /// Sets the fd to non-blocking mode. Returns false on failure. fn setNonblock(fd: posix.fd_t) bool { - const flags = posix.fcntl( - fd, - posix.F.GETFL, - 0, - ) catch |err| { - log.warn("read thread failed to get flags err={}", .{err}); + const flags = posix.system.fcntl(fd, posix.F.GETFL); + if (flags == -1) { + log.warn("read thread failed to get flags err=E{s}", .{@tagName(posix.errno(-1))}); return false; - }; + } - _ = posix.fcntl( + switch (posix.errno(posix.system.fcntl( fd, posix.F.SETFL, flags | @as(u32, @bitCast(posix.O{ .NONBLOCK = true })), - ) catch |err| { - log.warn("read thread failed to set flags err={}", .{err}); - return false; - }; - + ))) { + .SUCCESS => {}, + else => |e| { + log.warn("read thread failed to set flags err=E{s}", .{@tagName(e)}); + return false; + }, + } return true; } fn threadMainWindows(fd: posix.fd_t, io: *termio.Termio, quit: posix.fd_t) void { // Always close our end of the pipe when we exit. - defer posix.close(quit); + defer _ = posix.system.close(quit); // Setup our crash metadata crash.sentry.thread_state = .{ @@ -1766,8 +1789,8 @@ pub const ReadThread = struct { while (true) { while (true) { var n: windows.DWORD = 0; - if (windows.kernel32.ReadFile(fd, &buf, buf.len, &n, null) == 0) { - const err = windows.kernel32.GetLastError(); + if (windows.exp.kernel32.ReadFile(fd, &buf, buf.len, &n, null) == windows.FALSE) { + const err = windows.GetLastError(); switch (err) { // Check for a quit signal .OPERATION_ABORTED => break, @@ -1784,12 +1807,12 @@ pub const ReadThread = struct { // See threadMainPosix: hand the renderer state mutex // off if the renderer is waiting, since this loop // would otherwise starve it under heavy output. - io.renderer_state.yieldToDemand(); + io.renderer_state.yieldToDemand(global.io()); } var quit_bytes: windows.DWORD = 0; - if (windows.exp.kernel32.PeekNamedPipe(quit, null, 0, null, &quit_bytes, null) == 0) { - const err = windows.kernel32.GetLastError(); + if (windows.exp.kernel32.PeekNamedPipe(quit, null, 0, null, &quit_bytes, null) == windows.FALSE) { + const err = windows.GetLastError(); log.err("quit pipe reader error err={}", .{err}); unreachable; } @@ -1837,16 +1860,16 @@ fn execCommand( }; const hush = if (passwd.home) |home| hush: { - var dir = std.fs.openDirAbsolute(home, .{}) catch |err| { + var dir = std.Io.Dir.openDirAbsolute(global.io(), home, .{}) catch |err| { log.warn( "failed to open home dir, not checking for hushlogin err={}", .{err}, ); break :hush false; }; - defer dir.close(); + defer dir.close(global.io()); - break :hush if (dir.access(".hushlogin", .{})) true else |_| false; + break :hush if (dir.access(global.io(), ".hushlogin", .{})) true else |_| false; } else false; // If we made it this far we're going to start building @@ -1945,7 +1968,7 @@ fn execCommand( return switch (command) { // We need to clone the command since there's no guarantee the config remains valid. - .direct => |_| (try command.clone(alloc)).direct, + .direct => (try command.clone(alloc)).direct, .shell => |v| shell: { var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 4); @@ -1973,7 +1996,7 @@ fn execCommand( // Other values are passed as-is and resolved by // `internal_os.path.expand` in Command.startWindows. const argv0 = if (std.ascii.eqlIgnoreCase(v, "cmd.exe")) - std.process.getEnvVarOwned(alloc, "COMSPEC") catch + global.environ().getAlloc(alloc, "COMSPEC") catch try alloc.dupe(u8, v) else try alloc.dupe(u8, v); @@ -2002,6 +2025,37 @@ fn execCommand( }; } +/// Append a value to an environment variable such as PATH. +/// The returned value is always allocated so it must be freed. +fn appendEnv( + alloc: Allocator, + current: []const u8, + value: []const u8, +) Allocator.Error![]u8 { + // If there is no prior value, we return it as-is + if (current.len == 0) return try alloc.dupe(u8, value); + + // Otherwise we must prefix. + return try appendEnvAlways(alloc, current, value); +} + +/// Always append value to environment, even when it is empty. +/// This is useful because some env vars (like MANPATH) want there +/// to be an empty prefix to preserve existing values. +/// +/// The returned value is always allocated so it must be freed. +fn appendEnvAlways( + alloc: Allocator, + current: []const u8, + value: []const u8, +) Allocator.Error![]u8 { + return try std.fmt.allocPrint(alloc, "{s}{c}{s}", .{ + current, + std.fs.path.delimiter, + value, + }); +} + /// Get information about the process(es) running within the backend. Returns /// `null` if there was an error getting the information or the information is /// not available on a particular platform. @@ -2190,7 +2244,7 @@ test "execCommand windows: bare cmd.exe resolves via COMSPEC" { try testing.expectEqual(1, result.len); // Expect COMSPEC if available, otherwise the documented fallback. - const expected = std.process.getEnvVarOwned(alloc, "COMSPEC") catch + const expected = testing.environ.getAlloc(alloc, "COMSPEC") catch try alloc.dupe(u8, "C:\\Windows\\System32\\cmd.exe"); try testing.expectEqualStrings(expected, result[0]); } |
