macos: NSProcessInfo-based arg iterator

Fixes #2432

On macOS, processes with an NSApplicationMain entrypoint do not have
access to libc argc/argv. Instead, we must use NSProcessInfo. This
commit introduces an args iterator that uses NSProcessInfo, giving us
access to the args.

This also fixes an issue where we were not properly skipping argv0 when
iterating over the args. This happened to be fine because we happened to
ignore invalid args but it introduces a config error.
This commit is contained in:
Mitchell Hashimoto
2024-10-14 10:59:46 -07:00
parent 0cf12cd88b
commit 115e14f19f
3 changed files with 144 additions and 4 deletions

View File

@ -2290,8 +2290,12 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
switch (builtin.os.tag) {
.windows => {},
// Fast-path if we are non-Windows and no args, do nothing.
else => if (std.os.argv.len <= 1) return,
// Fast-path if we are Linux and have no args.
.linux => if (std.os.argv.len <= 1) return,
// Everything else we have to at least try because it may
// not use std.os.argv.
else => {},
}
// On Linux, we have a special case where if the executing
@ -2360,9 +2364,13 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
counter[i] = @field(self, field).list.items.len;
}
// Parse the config from the CLI args
var iter = try std.process.argsWithAllocator(alloc_gpa);
// Initialize our CLI iterator. The first argument is always assumed
// to be the program name so we skip over that.
var iter = try internal_os.args.iterator(alloc_gpa);
defer iter.deinit();
if (iter.next()) |argv0| log.debug("skipping argv0 value={s}", .{argv0});
// Parse the config from the CLI args
try self.loadIter(alloc_gpa, &iter);
// If we are not loading the default files, then we need to

131
src/os/args.zig Normal file
View File

@ -0,0 +1,131 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const objc = @import("objc");
const macos = @import("macos");
/// Returns an iterator over the command line arguments. This may or may
/// not allocate depending on the platform.
///
/// For Zig-aware readers: this is the same as std.process.argsWithAllocator
/// but handles macOS using NSProcessInfo instead of libc argc/argv.
pub fn iterator(allocator: Allocator) ArgIterator.InitError!ArgIterator {
//if (true) return try std.process.argsWithAllocator(allocator);
return ArgIterator.initWithAllocator(allocator);
}
/// Duck-typed to std.process.ArgIterator
pub const ArgIterator = switch (builtin.os.tag) {
.macos => IteratorMacOS,
else => std.process.ArgIterator,
};
/// This is an ArgIterator (duck-typed for std.process.ArgIterator) for
/// NSApplicationMain-based applications on macOS. It uses NSProcessInfo to
/// get the command line arguments since libc argc/argv pointers are not
/// valid.
///
/// I believe this should work for all macOS applications even if
/// NSApplicationMain is not used, but I haven't tested that so I'm not
/// sure. If/when libghostty is ever used outside of NSApplicationMain
/// then we can revisit this.
const IteratorMacOS = struct {
alloc: Allocator,
index: usize,
count: usize,
buf: [:0]u8,
args: objc.Object,
pub const InitError = Allocator.Error;
pub fn initWithAllocator(alloc: Allocator) InitError!IteratorMacOS {
const NSProcessInfo = objc.getClass("NSProcessInfo").?;
const info = NSProcessInfo.msgSend(objc.Object, objc.sel("processInfo"), .{});
const args = info.getProperty(objc.Object, "arguments");
errdefer args.release();
// Determine our maximum length so we can allocate the buffer to
// fit all values.
var max: usize = 0;
const count: usize = @intCast(args.getProperty(c_ulong, "count"));
for (0..count) |i| {
const nsstr = args.msgSend(
objc.Object,
objc.sel("objectAtIndex:"),
.{@as(c_ulong, @intCast(i))},
);
const maxlen: usize = @intCast(nsstr.msgSend(
c_ulong,
objc.sel("maximumLengthOfBytesUsingEncoding:"),
.{@as(c_ulong, 4)},
));
max = @max(max, maxlen);
}
// Allocate our buffer. We add 1 for the null terminator.
const buf = try alloc.allocSentinel(u8, max, 0);
errdefer alloc.free(buf);
return .{
.alloc = alloc,
.index = 0,
.count = count,
.buf = buf,
.args = args,
};
}
pub fn deinit(self: *IteratorMacOS) void {
self.alloc.free(self.buf);
// Note: we don't release self.args because it is a pointer copy
// not a retained object.
}
pub fn next(self: *IteratorMacOS) ?[:0]const u8 {
if (self.index == self.count) return null;
// NSString. No release because not a copy.
const nsstr = self.args.msgSend(
objc.Object,
objc.sel("objectAtIndex:"),
.{@as(c_ulong, @intCast(self.index))},
);
self.index += 1;
// Convert to string using getCString. Our buffer should always
// be big enough because we precomputed the maximum length.
if (!nsstr.msgSend(
bool,
objc.sel("getCString:maxLength:encoding:"),
.{
@as([*]u8, @ptrCast(self.buf.ptr)),
@as(c_ulong, @intCast(self.buf.len)),
@as(c_ulong, 4), // NSUTF8StringEncoding
},
)) {
// This should never happen... if it does, we just return empty.
return "";
}
return std.mem.sliceTo(self.buf, 0);
}
pub fn skip(self: *IteratorMacOS) bool {
if (self.index == self.count) return false;
self.index += 1;
return true;
}
};
test "args" {
const testing = std.testing;
const alloc = testing.allocator;
var iter = try iterator(alloc);
defer iter.deinit();
try testing.expect(iter.next().?.len > 0);
}

View File

@ -15,6 +15,7 @@ const pipepkg = @import("pipe.zig");
const resourcesdir = @import("resourcesdir.zig");
// Namespaces
pub const args = @import("args.zig");
pub const cgroup = @import("cgroup.zig");
pub const passwd = @import("passwd.zig");
pub const xdg = @import("xdg.zig");