From a77c706a180528f8197771abd51436d98ccd854a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 22 Jul 2026 06:32:37 -0700 Subject: [PATCH] fix process and global error handling Restore the error handling that the removed std.posix fork and waitpid wrappers previously provided. Raw fork failures now propagate, waitpid retries interruptions before reading status, and edit-config constructs the sentinel-terminated argv required by execve. Let global initialization own cleanup through its existing errdefer so temporary paths are freed once. Report initialization failures with the static synchronous I/O provider because global I/O has already been torn down by that point. --- src/Command.zig | 48 +++++++++++++++++++++++++++++++++-------- src/cli/edit_config.zig | 8 +++++-- src/global.zig | 1 - src/main_ghostty.zig | 5 ++++- 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/Command.zig b/src/Command.zig index 8406bc5b3..ec363e770 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -189,7 +189,7 @@ fn startPosix(self: *Command, arena: Allocator) !void { @compileError("missing env vars"); // Fork. - const pid = posix.system.fork(); + const pid = try fork(); if (pid != 0) { // Parent, return immediately. @@ -291,6 +291,17 @@ fn startPosix(self: *Command, arena: Allocator) !void { return error.ExecFailedInChild; } +/// Wrapper for the raw fork syscall. This preserves the error handling from +/// the std.posix wrapper that was removed in Zig 0.16. +fn fork() !posix.pid_t { + const rc = posix.system.fork(); + switch (posix.errno(rc)) { + .SUCCESS => return @intCast(rc), + .AGAIN, .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } +} + fn startWindows(self: *Command, arena: Allocator) !void { const cwd_w = if (self.cwd) |cwd| try std.unicode.utf8ToUtf16LeAllocZ(arena, cwd) else null; @@ -494,10 +505,10 @@ pub fn wait(self: Command, block: bool) !Exit { return .{ .Exited = exit_code }; } - const status = if (block) wait_block: { - var status: c_int = undefined; - _ = posix.system.waitpid(self.pid.?, &status, 0); - break :wait_block status; + const status: u32 = if (block) wait_block: { + var status: if (builtin.link_libc) c_int else u32 = undefined; + _ = try waitPid(self.pid.?, &status, 0); + break :wait_block @bitCast(status); } else wait_nohang: { // We specify NOHANG because its not our fault if the process we launch // for the tty doesn't properly waitpid its children. We don't want @@ -507,13 +518,32 @@ pub fn wait(self: Command, block: bool) !Exit { // wait call has not been performed, so we need to keep trying until we get // a non-zero pid back, otherwise we end up with zombie processes. while (true) { - var status: c_int = undefined; - const pid = posix.system.waitpid(self.pid.?, &status, posix.system.W.NOHANG); - if (pid != 0) break :wait_nohang status; + var status: if (builtin.link_libc) c_int else u32 = undefined; + const pid = try waitPid(self.pid.?, &status, posix.system.W.NOHANG); + if (pid != 0) break :wait_nohang @bitCast(status); } }; - return .init(@bitCast(status)); + return .init(status); +} + +/// Wrapper for the raw waitpid syscall. Status is only initialized on success; +/// interrupted waits are retried and all other errors are propagated. +fn waitPid( + pid: posix.pid_t, + status: *if (builtin.link_libc) c_int else u32, + flags: u32, +) !posix.pid_t { + while (true) { + const rc = posix.system.waitpid(pid, status, @intCast(flags)); + switch (posix.errno(rc)) { + .SUCCESS => return @intCast(rc), + .INTR => continue, + .CHILD => return error.NoChildProcess, + .INVAL => return error.InvalidWaitOptions, + else => |err| return posix.unexpectedErrno(err), + } + } } /// Sets command->data to data. diff --git a/src/cli/edit_config.zig b/src/cli/edit_config.zig index ccddc561d..9662ea444 100644 --- a/src/cli/edit_config.zig +++ b/src/cli/edit_config.zig @@ -158,9 +158,13 @@ fn runInner(alloc: Allocator, stderr: *std.Io.Writer) !u8 { defer alloc.free(command); // Run/replace process (using execve) - const argv = &.{ "/bin/sh", "-c", command }; + const argv = [_:null]?[*:0]const u8{ + "/bin/sh", + "-c", + command.ptr, + }; const envp = std.c.environ; - const err = std.posix.errno(std.posix.system.execve(argv[0], @ptrCast(argv), envp)); + const err = std.posix.errno(std.posix.system.execve(argv[0].?, &argv, envp)); // If we reached this point then exec failed. try stderr.print( diff --git a/src/global.zig b/src/global.zig index e69b3042d..3c2ddbb72 100644 --- a/src/global.zig +++ b/src/global.zig @@ -121,7 +121,6 @@ pub fn init(opts: InitOpts) !void { // Discover and save the temporary directory path self.tmp_dir_path = try allocTmpDir(self.alloc, self.environ); - errdefer freeTmpDir(self.alloc, self.tmp_dir_path.?); // We first try to parse any action that we may be executing. // Tool binaries (ghostty-bench, ghostty-gen) have their own action diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index b83505f47..cd2ef0c17 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -30,7 +30,10 @@ pub fn main(minimal: std.process.Init.Minimal) !MainReturn { // no other Zig code should EVER access the global state. global.init(.{ .main = minimal }) catch |err| { var buffer: [1024]u8 = undefined; - var stderr_writer = std.Io.File.stderr().writer(global.io(), &buffer); + var stderr_writer = std.Io.File.stderr().writer( + std.Io.Threaded.global_single_threaded.io(), + &buffer, + ); const stderr = &stderr_writer.interface; defer std.process.exit(1); const ErrSet = @TypeOf(err) || error{Unknown};