cli: _macos-disclaim

macos-disclaim
Mitchell Hashimoto 2025-10-18 20:47:58 -07:00
parent 73158249e3
commit 633bf5f021
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
5 changed files with 105 additions and 1 deletions

View File

@ -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 `+<action>` 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,
};
}
}

View File

@ -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});
}

View File

@ -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;
}

View File

@ -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();

View File

@ -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.