diff --git a/src/cli/ghostty.zig b/src/cli/ghostty.zig index f6ac7d93d..701c06de8 100644 --- a/src/cli/ghostty.zig +++ b/src/cli/ghostty.zig @@ -19,6 +19,7 @@ const crash_report = @import("crash_report.zig"); const show_face = @import("show_face.zig"); const boo = @import("boo.zig"); const new_window = @import("new_window.zig"); +const macos_disclaim = @import("macos_disclaim.zig"); /// Special commands that can be invoked via CLI flags. These are all /// invoked by using `+` as a CLI flag. The only exception is @@ -69,6 +70,9 @@ pub const Action = enum { // Use IPC to tell the running Ghostty to open a new window. @"new-window", + // Internal helper for posix_spawn that performs pre-exec setup + @"_macos-disclaim", + pub fn detectSpecialCase(arg: []const u8) ?SpecialCase(Action) { // If we see a "-e" and we haven't seen a command yet, then // we are done looking for commands. This special case enables @@ -102,6 +106,9 @@ pub const Action = enum { // to find this action in the help strings and output that. help_error => err: { inline for (@typeInfo(Action).@"enum".fields) |field| { + // Skip internal commands (prefixed with underscore) + if (field.name[0] == '_') continue; + // Future note: for now we just output the help text directly // to stdout. In the future we can style this much prettier // for all commands by just changing this one place. @@ -147,6 +154,7 @@ pub const Action = enum { .@"show-face" => try show_face.run(alloc), .boo => try boo.run(alloc), .@"new-window" => try new_window.run(alloc), + .@"_macos-disclaim" => try macos_disclaim.run(alloc), }; } @@ -186,6 +194,7 @@ pub const Action = enum { .@"show-face" => show_face.Options, .boo => boo.Options, .@"new-window" => new_window.Options, + .@"_macos-disclaim" => macos_disclaim.Options, }; } } diff --git a/src/cli/help.zig b/src/cli/help.zig index a2b4dde80..5bfdf20f0 100644 --- a/src/cli/help.zig +++ b/src/cli/help.zig @@ -63,6 +63,8 @@ pub fn run(alloc: Allocator) !u8 { ); inline for (@typeInfo(Action).@"enum".fields) |field| { + // Skip internal commands (prefixed with underscore) + if (field.name[0] == '_') continue; try stdout.print(" +{s}\n", .{field.name}); } diff --git a/src/cli/macos_disclaim.zig b/src/cli/macos_disclaim.zig new file mode 100644 index 000000000..ead18524e --- /dev/null +++ b/src/cli/macos_disclaim.zig @@ -0,0 +1,76 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const posix = std.posix; +const Allocator = std.mem.Allocator; +const posix_spawn = @import("../os/posix_spawn.zig"); + +const log = std.log.scoped(.macos_disclaim); + +pub const Options = struct { + pub fn deinit(self: Options) void { + _ = self; + } +}; + +/// The `_macos-disclaim` command is an internal-only Ghostty command that +/// is only available on macOS. It uses private posix_spawn APIs to +/// make the child process the "responssible process" in macOS so it is +/// in charge of its own TCC (permissions like Downloads folder access or +/// camera) and resource accounting rather than Ghostty. +pub fn run(alloc: Allocator) !u8 { + // This helper is only for Apple systems. POSIX in general has posix_spawn + // but we only use it on Apple platforms because it lets us shed our + // responsible process bit. + if (comptime builtin.os.tag != .macos) { + log.warn("macos-disclaim is only supported on macOS", .{}); + return 1; + } + + // Get the command to exec from the remaining args + // Skip arg 0 (our program name) and arg 1 (the action "+_macos-disclaim") + var arg_iter = try std.process.argsWithAllocator(alloc); + defer arg_iter.deinit(); + _ = arg_iter.skip(); + _ = arg_iter.skip(); + + // Collect remaining args for exec + var args: std.ArrayList(?[*:0]const u8) = .empty; + defer args.deinit(alloc); + while (arg_iter.next()) |arg| try args.append(alloc, arg); + if (args.items.len == 0) { + log.err("no command specified to exec", .{}); + return 1; + } + try args.append(alloc, null); + + var attrs = try posix_spawn.spawn_attr.create(); + defer posix_spawn.spawn_attr.destroy(&attrs); + { + try posix_spawn.spawn_attr.setflags(&attrs, .{ + // Act like exec(): replace this process. + .setexec = true, + }); + + // This is the magical private API that makes it so that this + // child process doesn't get looped into the TCC and resource + // accounting of Ghostty. + try posix_spawn.spawn_attr.disclaim(&attrs, true); + } + + _ = posix_spawn.spawnp( + std.mem.span(args.items[0].?), + null, + &attrs, + args.items[0 .. args.items.len - 1 :null].ptr, + std.c.environ, + ) catch |err| { + log.err("failed to posix_spawn command '{s}': {}", .{ + std.mem.span(args.items[0].?), + err, + }); + return 1; + }; + + // We set the exec flag so we can't reach this point. + unreachable; +} diff --git a/src/helpgen.zig b/src/helpgen.zig index fe30db10c..1961a2a70 100644 --- a/src/helpgen.zig +++ b/src/helpgen.zig @@ -82,6 +82,9 @@ fn genActions(alloc: std.mem.Allocator, writer: *std.Io.Writer) !void { ); inline for (@typeInfo(Action).@"enum".fields) |field| { + // Skip internal commands (prefixed with underscore) + if (field.name[0] == '_') continue; + const action_file = comptime action_file: { const action = @field(Action, field.name); break :action_file action.file(); diff --git a/src/pty.zig b/src/pty.zig index 1ab88d40f..6beed058b 100644 --- a/src/pty.zig +++ b/src/pty.zig @@ -83,7 +83,7 @@ const NullPty = struct { /// Linux PTY creation and management. This is just a thin layer on top /// of Linux syscalls. The caller is responsible for detail-oriented handling /// of the returned file handles. -const PosixPty = struct { +pub const PosixPty = struct { pub const Error = OpenError || GetModeError || GetSizeError || SetSizeError || ChildPreExecError; pub const Fd = posix.fd_t; @@ -249,6 +249,20 @@ const PosixPty = struct { posix.close(self.slave); posix.close(self.master); } + + /// This is the pre-exec that needs to happen for posix_spawn on macOS + /// because the API doesn't support all the actions/attrs we need to + /// create a proper terminal environment. + pub fn posixSpawnPreExec(self: PosixPty) !void { + // Set controlling terminal + switch (posix.errno(c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)))) { + .SUCCESS => {}, + else => |err| { + log.err("error setting controlling terminal errno={}", .{err}); + return error.SetControllingTerminalFailed; + }, + } + } }; /// Windows PTY creation and management.