diff options
| author | Alan Moyano <alanmoyano203@gmail.com> | 2025-06-29 15:59:29 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-29 15:59:29 -0300 |
| commit | 43a3338491b3f6c409606ebfaab3a38c96455144 (patch) | |
| tree | ef2dd3fa426b45307e376a5809d2d41d23afb26b /src/os | |
| parent | cf7e76d8f23f7e048ff55fd4db5da7a08e5176c6 (diff) | |
| parent | 0d55a1deefa3f67abb2981cc83b46af5058789b5 (diff) | |
Merge branch 'ghostty-org:main' into main
Diffstat (limited to 'src/os')
| -rw-r--r-- | src/os/args.zig | 2 | ||||
| -rw-r--r-- | src/os/cf_release_thread.zig | 8 | ||||
| -rw-r--r-- | src/os/cgroup.zig | 19 | ||||
| -rw-r--r-- | src/os/dbus.zig | 21 | ||||
| -rw-r--r-- | src/os/desktop.zig | 18 | ||||
| -rw-r--r-- | src/os/flatpak.zig | 25 | ||||
| -rw-r--r-- | src/os/homedir.zig | 4 | ||||
| -rw-r--r-- | src/os/hostname.zig | 194 | ||||
| -rw-r--r-- | src/os/i18n.zig | 38 | ||||
| -rw-r--r-- | src/os/locale.zig | 7 | ||||
| -rw-r--r-- | src/os/macos.zig | 4 | ||||
| -rw-r--r-- | src/os/main.zig | 6 | ||||
| -rw-r--r-- | src/os/open.zig | 91 | ||||
| -rw-r--r-- | src/os/resourcesdir.zig | 53 | ||||
| -rw-r--r-- | src/os/systemd.zig | 65 |
15 files changed, 460 insertions, 95 deletions
diff --git a/src/os/args.zig b/src/os/args.zig index 9f7401c94..a531a418b 100644 --- a/src/os/args.zig +++ b/src/os/args.zig @@ -12,7 +12,7 @@ const macos = @import("macos"); /// 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); + return .initWithAllocator(allocator); } /// Duck-typed to std.process.ArgIterator diff --git a/src/os/cf_release_thread.zig b/src/os/cf_release_thread.zig index dbf8e6592..445dc4864 100644 --- a/src/os/cf_release_thread.zig +++ b/src/os/cf_release_thread.zig @@ -8,6 +8,7 @@ const std = @import("std"); const builtin = @import("builtin"); const macos = @import("macos"); +const internal_os = @import("../os/main.zig"); const xev = @import("../global.zig").xev; const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue; @@ -119,6 +120,13 @@ pub fn threadMain(self: *Thread) void { fn threadMain_(self: *Thread) !void { defer log.debug("cf release thread exited", .{}); + // 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, + // so instead we use this code to name the thread instead. + if (builtin.os.tag.isDarwin()) { + internal_os.macos.pthread_setname_np(&"cf_release".*); + } + // Start the async handlers. We start these first so that they're // registered even if anything below fails so we can drain the mailbox. self.wakeup.wait(&self.loop, &self.wakeup_c, Thread, self, wakeupCallback); diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig index 5645e337a..4f13921c5 100644 --- a/src/os/cgroup.zig +++ b/src/os/cgroup.zig @@ -56,6 +56,25 @@ pub fn create( } } +/// Remove a cgroup. This will only succeed if the cgroup is empty +/// (has no processes). The cgroup path should be relative to the +/// cgroup root (e.g. "/user.slice/surfaces/abc123.scope"). +pub fn remove(cgroup: []const u8) !void { + assert(cgroup.len > 0); + assert(cgroup[0] == '/'); + + var buf: [std.fs.max_path_bytes]u8 = undefined; + const path = try std.fmt.bufPrint(&buf, "/sys/fs/cgroup{s}", .{cgroup}); + std.fs.cwd().deleteDir(path) catch |err| switch (err) { + // If it doesn't exist, that's fine - maybe it was already cleaned up + error.FileNotFound => {}, + + // Any other error we failed to delete it so we want to notify + // the user. + else => return err, + }; +} + /// Move the given PID into the given cgroup. pub fn moveInto( cgroup: []const u8, diff --git a/src/os/dbus.zig b/src/os/dbus.zig new file mode 100644 index 000000000..99824db71 --- /dev/null +++ b/src/os/dbus.zig @@ -0,0 +1,21 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +/// Returns true if the program was launched by D-Bus activation. +/// +/// On Linux GTK, this returns true if the program was launched using D-Bus +/// activation. It will return false if Ghostty was launched any other way. +/// +/// For other platforms and app runtimes, this returns false. +pub fn launchedByDbusActivation() bool { + return switch (builtin.os.tag) { + // On Linux, D-Bus activation sets `DBUS_STARTER_ADDRESS` and + // `DBUS_STARTER_BUS_TYPE`. If these environment variables are present + // (no matter the value) we were launched by D-Bus activation. + .linux => std.posix.getenv("DBUS_STARTER_ADDRESS") != null and + std.posix.getenv("DBUS_STARTER_BUS_TYPE") != null, + + // No other system supports D-Bus so always return false. + else => false, + }; +} diff --git a/src/os/desktop.zig b/src/os/desktop.zig index c73f150e0..3bc843e5c 100644 --- a/src/os/desktop.zig +++ b/src/os/desktop.zig @@ -30,24 +30,24 @@ pub fn launchedFromDesktop() bool { break :macos c.getppid() == 1; }, - // On Linux, GTK sets GIO_LAUNCHED_DESKTOP_FILE and + // On Linux and BSD, GTK sets GIO_LAUNCHED_DESKTOP_FILE and // GIO_LAUNCHED_DESKTOP_FILE_PID. We only check the latter to see if // we match the PID and assume that if we do, we were launched from // the desktop file. Pid comparing catches the scenario where // another terminal was launched from a desktop file and then launches // Ghostty and Ghostty inherits the env. - .linux => linux: { + .linux, .freebsd => ul: { const gio_pid_str = posix.getenv("GIO_LAUNCHED_DESKTOP_FILE_PID") orelse - break :linux false; + break :ul false; const pid = c.getpid(); const gio_pid = std.fmt.parseInt( @TypeOf(pid), gio_pid_str, 10, - ) catch break :linux false; + ) catch break :ul false; - break :linux gio_pid == pid; + break :ul gio_pid == pid; }, // TODO: This should have some logic to detect this. Perhaps std.builtin.subsystem @@ -71,14 +71,14 @@ pub const DesktopEnvironment = enum { }; /// Detect what desktop environment we are running under. This is mainly used -/// on Linux to enable or disable certain features but there may be more uses in +/// on Linux and BSD to enable or disable certain features but there may be more uses in /// the future. pub fn desktopEnvironment() DesktopEnvironment { return switch (comptime builtin.os.tag) { .macos => .macos, .windows => .windows, - .linux => de: { - if (@inComptime()) @compileError("Checking for the desktop environment on Linux must be done at runtime."); + .linux, .freebsd => de: { + if (@inComptime()) @compileError("Checking for the desktop environment on Linux/BSD must be done at runtime."); // Use $XDG_SESSION_DESKTOP to determine what DE we are using on Linux // https://www.freedesktop.org/software/systemd/man/latest/pam_systemd.html#desktop= @@ -110,7 +110,7 @@ test "desktop environment" { switch (builtin.os.tag) { .macos => try testing.expectEqual(.macos, desktopEnvironment()), .windows => try testing.expectEqual(.windows, desktopEnvironment()), - .linux => { + .linux, .freebsd => { const getenv = std.posix.getenv; const setenv = @import("env.zig").setenv; const unsetenv = @import("env.zig").unsetenv; diff --git a/src/os/flatpak.zig b/src/os/flatpak.zig index 7b92a8ba9..7bd84bc27 100644 --- a/src/os/flatpak.zig +++ b/src/os/flatpak.zig @@ -112,6 +112,8 @@ pub const FlatpakHostCommand = struct { pub fn spawn(self: *FlatpakHostCommand, alloc: Allocator) !u32 { const thread = try std.Thread.spawn(.{}, threadMain, .{ self, alloc }); thread.setName("flatpak-host-command") catch {}; + // We don't track this thread, it will terminate on its own on command exit + thread.detach(); // Wait for the process to start or error. self.state_mutex.lock(); @@ -232,9 +234,10 @@ pub const FlatpakHostCommand = struct { }; // Get our bus connection. - var g_err: [*c]c.GError = null; + var g_err: ?*c.GError = null; + defer if (g_err) |ptr| c.g_error_free(ptr); const bus = c.g_bus_get_sync(c.G_BUS_TYPE_SESSION, null, &g_err) orelse { - log.warn("signal error getting bus: {s}", .{g_err.*.message}); + log.warn("signal error getting bus: {s}", .{g_err.?.*.message}); return Error.FlatpakSetupFail; }; defer c.g_object_unref(bus); @@ -258,7 +261,7 @@ pub const FlatpakHostCommand = struct { &g_err, ); if (g_err != null) { - log.warn("signal send error: {s}", .{g_err.*.message}); + log.warn("signal send error: {s}", .{g_err.?.*.message}); return; } defer c.g_variant_unref(reply); @@ -278,9 +281,10 @@ pub const FlatpakHostCommand = struct { // Get our bus connection. This has to remain active until we exit // the thread otherwise our signals won't be called. - var g_err: [*c]c.GError = null; + var g_err: ?*c.GError = null; + defer if (g_err) |ptr| c.g_error_free(ptr); const bus = c.g_bus_get_sync(c.G_BUS_TYPE_SESSION, null, &g_err) orelse { - log.warn("spawn error getting bus: {s}", .{g_err.*.message}); + log.warn("spawn error getting bus: {s}", .{g_err.?.*.message}); self.updateState(.{ .err = {} }); return; }; @@ -308,7 +312,8 @@ pub const FlatpakHostCommand = struct { bus: *c.GDBusConnection, loop: *c.GMainLoop, ) !void { - var err: [*c]c.GError = null; + var err: ?*c.GError = null; + defer if (err) |ptr| c.g_error_free(ptr); var arena_allocator = std.heap.ArenaAllocator.init(alloc); defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); @@ -317,15 +322,15 @@ pub const FlatpakHostCommand = struct { const fd_list = c.g_unix_fd_list_new(); defer c.g_object_unref(fd_list); if (c.g_unix_fd_list_append(fd_list, self.stdin, &err) < 0) { - log.warn("error adding fd: {s}", .{err.*.message}); + log.warn("error adding fd: {s}", .{err.?.*.message}); return Error.FlatpakSetupFail; } if (c.g_unix_fd_list_append(fd_list, self.stdout, &err) < 0) { - log.warn("error adding fd: {s}", .{err.*.message}); + log.warn("error adding fd: {s}", .{err.?.*.message}); return Error.FlatpakSetupFail; } if (c.g_unix_fd_list_append(fd_list, self.stderr, &err) < 0) { - log.warn("error adding fd: {s}", .{err.*.message}); + log.warn("error adding fd: {s}", .{err.?.*.message}); return Error.FlatpakSetupFail; } @@ -405,7 +410,7 @@ pub const FlatpakHostCommand = struct { null, &err, ) orelse { - log.warn("Flatpak.HostCommand failed: {s}", .{err.*.message}); + log.warn("Flatpak.HostCommand failed: {s}", .{err.?.*.message}); return Error.FlatpakRPCFail; }; defer c.g_variant_unref(reply); diff --git a/src/os/homedir.zig b/src/os/homedir.zig index b5629fd65..f3d6e4498 100644 --- a/src/os/homedir.zig +++ b/src/os/homedir.zig @@ -14,7 +14,7 @@ const Error = error{ /// is generally an expensive process so the value should be cached. pub inline fn home(buf: []u8) !?[]const u8 { return switch (builtin.os.tag) { - inline .linux, .macos => try homeUnix(buf), + inline .linux, .freebsd, .macos => try homeUnix(buf), .windows => try homeWindows(buf), // iOS doesn't have a user-writable home directory @@ -122,7 +122,7 @@ pub const ExpandError = error{ /// than `buf.len`. pub fn expandHome(path: []const u8, buf: []u8) ExpandError![]const u8 { return switch (builtin.os.tag) { - .linux, .macos => try expandHomeUnix(path, buf), + .linux, .freebsd, .macos => try expandHomeUnix(path, buf), .ios => return path, else => @compileError("unimplemented"), }; diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 22f29ceff..a75ca1cbb 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const posix = std.posix; pub const HostnameParsingError = error{ @@ -6,6 +7,96 @@ pub const HostnameParsingError = error{ NoSpaceLeft, }; +pub const UrlParsingError = std.Uri.ParseError || error{ + HostnameIsNotMacAddress, + NoSchemeProvided, +}; + +const mac_address_length = 17; + +fn isUriPathSeparator(c: u8) bool { + return switch (c) { + '?', '#' => true, + else => false, + }; +} + +fn isValidMacAddress(mac_address: []const u8) bool { + // A valid mac address has 6 two-character components with 5 colons, e.g. 12:34:56:ab:cd:ef. + if (mac_address.len != 17) { + return false; + } + + for (mac_address, 0..) |c, i| { + if ((i + 1) % 3 == 0) { + if (c != ':') { + return false; + } + } else if (!std.mem.containsAtLeastScalar(u8, "0123456789ABCDEFabcdef", 1, c)) { + return false; + } + } + + return true; +} + +/// Parses the provided url to a `std.Uri` struct. This is very specific to getting hostname and +/// path information for Ghostty's PWD reporting functionality. Takes into account that on macOS +/// the url passed to this function might have a mac address as its hostname and parses it +/// correctly. +pub fn parseUrl(url: []const u8) UrlParsingError!std.Uri { + return std.Uri.parse(url) catch |e| { + // The mac-address-as-hostname issue is specific to macOS so we just return an error if we + // hit it on other platforms. + if (comptime builtin.os.tag != .macos) return e; + + // It's possible this is a mac address on macOS where the last 2 characters in the + // address are non-digits, e.g. 'ff', and thus an invalid port. + // + // Example: file://12:34:56:78:90:12/path/to/file + if (e != error.InvalidPort) return e; + + const url_without_scheme_start = std.mem.indexOf(u8, url, "://") orelse { + return error.NoSchemeProvided; + }; + const scheme = url[0..url_without_scheme_start]; + const url_without_scheme = url[url_without_scheme_start + 3 ..]; + + // The first '/' after the scheme marks the end of the hostname. If the first '/' + // following the end of the scheme is not at the right position this is not a + // valid mac address. + if (url_without_scheme.len != mac_address_length and + std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != mac_address_length) + { + return error.HostnameIsNotMacAddress; + } + + // At this point we may have a mac address as the hostname. + const mac_address = url_without_scheme[0..mac_address_length]; + + if (!isValidMacAddress(mac_address)) { + return error.HostnameIsNotMacAddress; + } + + var uri_path_end_idx: usize = mac_address_length; + while (uri_path_end_idx < url_without_scheme.len and + !isUriPathSeparator(url_without_scheme[uri_path_end_idx])) + { + uri_path_end_idx += 1; + } + + // Same compliance factor as std.Uri.parse(), i.e. not at all compliant with the URI + // spec. + return .{ + .scheme = scheme, + .host = .{ .percent_encoded = mac_address }, + .path = .{ + .percent_encoded = url_without_scheme[mac_address_length..uri_path_end_idx], + }, + }; + }; +} + /// Print the hostname from a file URI into a buffer. pub fn bufPrintHostnameFromFileUri( buf: []u8, @@ -70,6 +161,101 @@ pub fn isLocalHostname(hostname: []const u8) LocalHostnameValidationError!bool { return std.mem.eql(u8, hostname, ourHostname); } +test parseUrl { + // 1. Typical hostnames. + + var uri = try parseUrl("file://personal.computer/home/test/"); + + try std.testing.expectEqualStrings("file", uri.scheme); + try std.testing.expectEqualStrings("personal.computer", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == null); + + uri = try parseUrl("kitty-shell-cwd://personal.computer/home/test/"); + + try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); + try std.testing.expectEqualStrings("personal.computer", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == null); + + // 2. Hostnames that are mac addresses. + + // Numerical mac addresses. + + uri = try parseUrl("file://12:34:56:78:90:12/home/test/"); + + try std.testing.expectEqualStrings("file", uri.scheme); + try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == 12); + + uri = try parseUrl("kitty-shell-cwd://12:34:56:78:90:12/home/test/"); + + try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); + try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == 12); + + // Alphabetical mac addresses. + + uri = try parseUrl("file://ab:cd:ef:ab:cd:ef/home/test/"); + + try std.testing.expectEqualStrings("file", uri.scheme); + try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == null); + + uri = try parseUrl("kitty-shell-cwd://ab:cd:ef:ab:cd:ef/home/test/"); + + try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); + try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == null); + + // 3. Hostnames that are mac addresses with no path. + + // Numerical mac addresses. + + uri = try parseUrl("file://12:34:56:78:90:12"); + + try std.testing.expectEqualStrings("file", uri.scheme); + try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("", uri.path.percent_encoded); + try std.testing.expect(uri.port == 12); + + uri = try parseUrl("kitty-shell-cwd://12:34:56:78:90:12"); + + try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); + try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("", uri.path.percent_encoded); + try std.testing.expect(uri.port == 12); + + // Alphabetical mac addresses. + + uri = try parseUrl("file://ab:cd:ef:ab:cd:ef"); + + try std.testing.expectEqualStrings("file", uri.scheme); + try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("", uri.path.percent_encoded); + try std.testing.expect(uri.port == null); + + uri = try parseUrl("kitty-shell-cwd://ab:cd:ef:ab:cd:ef"); + + try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); + try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("", uri.path.percent_encoded); + try std.testing.expect(uri.port == null); +} + +test "parseUrl succeeds even if path component is missing" { + const uri = try parseUrl("file://12:34:56:78:90:ab"); + + try std.testing.expectEqualStrings("file", uri.scheme); + try std.testing.expectEqualStrings("12:34:56:78:90:ab", uri.host.?.percent_encoded); + try std.testing.expect(uri.path.isEmpty()); + try std.testing.expect(uri.port == null); +} + test "bufPrintHostnameFromFileUri succeeds with ascii hostname" { const uri = try std.Uri.parse("file://localhost/"); @@ -86,6 +272,14 @@ test "bufPrintHostnameFromFileUri succeeds with hostname as mac address" { try std.testing.expectEqualStrings("12:34:56:78:90:12", actual); } +test "bufPrintHostnameFromFileUri succeeds with hostname as mac address with the last component as ascii" { + const uri = try parseUrl("file://12:34:56:78:90:ab"); + + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const actual = try bufPrintHostnameFromFileUri(&buf, uri); + try std.testing.expectEqualStrings("12:34:56:78:90:ab", actual); +} + test "bufPrintHostnameFromFileUri succeeds with hostname as a mac address and the last section is < 10" { const uri = try std.Uri.parse("file://12:34:56:78:90:05"); diff --git a/src/os/i18n.zig b/src/os/i18n.zig index fd1d44ab0..1ba8a676c 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -69,23 +69,27 @@ pub const InitError = error{ /// want to set the domain for the entire application since this is also /// used by libghostty. pub fn init(resources_dir: []const u8) InitError!void { - // i18n is unsupported on Windows - if (builtin.os.tag == .windows) return; - - // Our resources dir is always nested below the share dir that - // is standard for translations. - const share_dir = std.fs.path.dirname(resources_dir) orelse - return error.InvalidResourcesDir; - - // Build our locale path - var buf: [std.fs.max_path_bytes]u8 = undefined; - const path = std.fmt.bufPrintZ(&buf, "{s}/locale", .{share_dir}) catch - return error.OutOfMemory; - - // Bind our bundle ID to the given locale path - log.debug("binding domain={s} path={s}", .{ build_config.bundle_id, path }); - _ = bindtextdomain(build_config.bundle_id, path.ptr) orelse - return error.OutOfMemory; + switch (builtin.os.tag) { + // i18n is unsupported on Windows + .windows => return, + + else => { + // Our resources dir is always nested below the share dir that + // is standard for translations. + const share_dir = std.fs.path.dirname(resources_dir) orelse + return error.InvalidResourcesDir; + + // Build our locale path + var buf: [std.fs.max_path_bytes]u8 = undefined; + const path = std.fmt.bufPrintZ(&buf, "{s}/locale", .{share_dir}) catch + return error.OutOfMemory; + + // Bind our bundle ID to the given locale path + log.debug("binding domain={s} path={s}", .{ build_config.bundle_id, path }); + _ = bindtextdomain(build_config.bundle_id, path.ptr) orelse + return error.OutOfMemory; + }, + } } /// Set the global gettext domain to our bundle ID, allowing unqualified diff --git a/src/os/locale.zig b/src/os/locale.zig index 17e4d163c..b391d690f 100644 --- a/src/os/locale.zig +++ b/src/os/locale.zig @@ -108,11 +108,8 @@ fn setLangFromCocoa() void { } // Get our preferred languages and set that to the LANGUAGE - // env var in case our language differs from our locale. We only - // do this when the app is launched from the desktop because then - // we're in an app bundle and we are expected to read from our - // Bundle's preferred languages. - if (internal_os.launchedFromDesktop()) language: { + // env var in case our language differs from our locale. + language: { var buf: [1024]u8 = undefined; const pref_ = preferredLanguageFromCocoa( &buf, diff --git a/src/os/macos.zig b/src/os/macos.zig index ca7c81a47..100d0fe44 100644 --- a/src/os/macos.zig +++ b/src/os/macos.zig @@ -88,6 +88,10 @@ extern "c" fn pthread_set_qos_class_self_np( relative_priority: c_int, ) c_int; +pub extern "c" fn pthread_setname_np( + name: [*:0]const u8, +) void; + pub const NSOperatingSystemVersion = extern struct { major: i64, minor: i64, diff --git a/src/os/main.zig b/src/os/main.zig index 36833f427..906e3d150 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -2,6 +2,7 @@ //! system. These aren't restricted to syscalls or low-level operations, but //! also OS-specific features and conventions. +const dbus = @import("dbus.zig"); const desktop = @import("desktop.zig"); const env = @import("env.zig"); const file = @import("file.zig"); @@ -12,6 +13,7 @@ const mouse = @import("mouse.zig"); const openpkg = @import("open.zig"); const pipepkg = @import("pipe.zig"); const resourcesdir = @import("resourcesdir.zig"); +const systemd = @import("systemd.zig"); // Namespaces pub const args = @import("args.zig"); @@ -27,6 +29,7 @@ pub const shell = @import("shell.zig"); // Functions and types pub const CFReleaseThread = @import("cf_release_thread.zig"); pub const TempDir = @import("TempDir.zig"); +pub const GetEnvResult = env.GetEnvResult; pub const getEnvMap = env.getEnvMap; pub const appendEnv = env.appendEnv; pub const appendEnvAlways = env.appendEnvAlways; @@ -35,6 +38,8 @@ pub const getenv = env.getenv; pub const setenv = env.setenv; pub const unsetenv = env.unsetenv; pub const launchedFromDesktop = desktop.launchedFromDesktop; +pub const launchedByDbusActivation = dbus.launchedByDbusActivation; +pub const launchedBySystemd = systemd.launchedBySystemd; pub const desktopEnvironment = desktop.desktopEnvironment; pub const rlimit = file.rlimit; pub const fixMaxFiles = file.fixMaxFiles; @@ -51,6 +56,7 @@ pub const open = openpkg.open; pub const OpenType = openpkg.Type; pub const pipe = pipepkg.pipe; pub const resourcesDir = resourcesdir.resourcesDir; +pub const ResourcesDir = resourcesdir.ResourcesDir; pub const ShellEscapeWriter = shell.ShellEscapeWriter; test { diff --git a/src/os/open.zig b/src/os/open.zig index f7eadd06e..ce62a7e0b 100644 --- a/src/os/open.zig +++ b/src/os/open.zig @@ -2,6 +2,8 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; +const log = std.log.scoped(.@"os-open"); + /// The type of the data at the URL to open. This is used as a hint /// to potentially open the URL in a different way. pub const Type = enum { @@ -12,68 +14,73 @@ pub const Type = enum { /// Open a URL in the default handling application. /// /// Any output on stderr is logged as a warning in the application logs. -/// Output on stdout is ignored. +/// Output on stdout is ignored. The allocator is used to buffer the +/// log output and may allocate from another thread. pub fn open( alloc: Allocator, typ: Type, url: []const u8, ) !void { - const cmd: OpenCommand = switch (builtin.os.tag) { - .linux => .{ .child = std.process.Child.init( + var exe: std.process.Child = switch (builtin.os.tag) { + .linux, .freebsd => .init( &.{ "xdg-open", url }, alloc, - ) }, + ), - .windows => .{ .child = std.process.Child.init( + .windows => .init( &.{ "rundll32", "url.dll,FileProtocolHandler", url }, alloc, - ) }, + ), - .macos => .{ - .child = std.process.Child.init( - switch (typ) { - .text => &.{ "open", "-t", url }, - .unknown => &.{ "open", url }, - }, - alloc, - ), - .wait = true, - }, + .macos => .init( + switch (typ) { + .text => &.{ "open", "-t", url }, + .unknown => &.{ "open", url }, + }, + alloc, + ), .ios => return error.Unimplemented, else => @compileError("unsupported OS"), }; - var exe = cmd.child; - if (cmd.wait) { - // Pipe stdout/stderr so we can collect output from the command - exe.stdout_behavior = .Pipe; - exe.stderr_behavior = .Pipe; - } + // Pipe stdout/stderr so we can collect output from the command. + // This must be set before spawning the process. + exe.stdout_behavior = .Pipe; + exe.stderr_behavior = .Pipe; + // Spawn the process on our same thread so we can detect failure + // quickly. try exe.spawn(); - if (cmd.wait) { - // 50 KiB is the default value used by std.process.Child.run - const output_max_size = 50 * 1024; - - var stdout: std.ArrayListUnmanaged(u8) = .{}; - var stderr: std.ArrayListUnmanaged(u8) = .{}; - defer { - stdout.deinit(alloc); - stderr.deinit(alloc); - } + // Create a thread that handles collecting output and reaping + // the process. This is done in a separate thread because SOME + // open implementations block and some do not. It's easier to just + // spawn a thread to handle this so that we never block. + const thread = try std.Thread.spawn(.{}, openThread, .{ alloc, exe }); + thread.detach(); +} - try exe.collectOutput(alloc, &stdout, &stderr, output_max_size); - _ = try exe.wait(); +fn openThread(alloc: Allocator, exe_: std.process.Child) !void { + // 50 KiB is the default value used by std.process.Child.run and should + // be enough to get the output we care about. + const output_max_size = 50 * 1024; - // If we have any stderr output we log it. This makes it easier for - // users to debug why some open commands may not work as expected. - if (stderr.items.len > 0) std.log.err("open stderr={s}", .{stderr.items}); + var stdout: std.ArrayListUnmanaged(u8) = .{}; + var stderr: std.ArrayListUnmanaged(u8) = .{}; + defer { + stdout.deinit(alloc); + stderr.deinit(alloc); } -} -const OpenCommand = struct { - child: std.process.Child, - wait: bool = false, -}; + // Copy the exe so it is non-const. This is necessary because wait() + // requires a mutable reference and we can't have one as a thread + // param. + var exe = exe_; + try exe.collectOutput(alloc, &stdout, &stderr, output_max_size); + _ = try exe.wait(); + + // If we have any stderr output we log it. This makes it easier for + // users to debug why some open commands may not work as expected. + if (stderr.items.len > 0) log.warn("wait stderr={s}", .{stderr.items}); +} diff --git a/src/os/resourcesdir.zig b/src/os/resourcesdir.zig index 6f69b91d3..278de44fc 100644 --- a/src/os/resourcesdir.zig +++ b/src/os/resourcesdir.zig @@ -2,13 +2,42 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; +pub const ResourcesDir = struct { + /// Avoid accessing these directly, use the app() and host() methods instead. + app_path: ?[]const u8 = null, + host_path: ?[]const u8 = null, + + /// Free resources held. Requires the same allocator as when resourcesDir() + /// is called. + pub fn deinit(self: *ResourcesDir, alloc: Allocator) void { + if (self.app_path) |p| alloc.free(p); + if (self.host_path) |p| alloc.free(p); + } + + /// Get the directory to the bundled resources directory accessible + /// by the application. + pub fn app(self: *ResourcesDir) ?[]const u8 { + return self.app_path; + } + + /// Get the directory to the bundled resources directory accessible + /// by the host environment (i.e. for sandboxed applications). The + /// returned directory might not be accessible from the application + /// itself. + /// + /// In non-sandboxed environment, this should be the same as app(). + pub fn host(self: *ResourcesDir) ?[]const u8 { + return self.host_path orelse self.app_path; + } +}; + /// Gets the directory to the bundled resources directory, if it /// exists (not all platforms or packages have it). The output is /// owned by the caller. /// /// This is highly Ghostty-specific and can likely be generalized at /// some point but we can cross that bridge if we ever need to. -pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 { +pub fn resourcesDir(alloc: Allocator) !ResourcesDir { // Use the GHOSTTY_RESOURCES_DIR environment variable in release builds. // // In debug builds we try using terminfo detection first instead, since @@ -20,7 +49,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 { // freed, do not try to use internal_os.getenv or posix getenv. if (comptime builtin.mode != .Debug) { if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| { - if (dir.len > 0) return dir; + if (dir.len > 0) return .{ .app_path = dir }; } else |err| switch (err) { error.EnvironmentVariableNotFound => {}, else => return err, @@ -32,12 +61,13 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 { const sentinels = switch (comptime builtin.target.os.tag) { .windows => .{"terminfo/ghostty.terminfo"}, .macos => .{"terminfo/78/xterm-ghostty"}, + .freebsd => .{ "site-terminfo/g/ghostty", "site-terminfo/x/xterm-ghostty" }, else => .{ "terminfo/g/ghostty", "terminfo/x/xterm-ghostty" }, }; // Get the path to our running binary var exe_buf: [std.fs.max_path_bytes]u8 = undefined; - var exe: []const u8 = std.fs.selfExePath(&exe_buf) catch return null; + var exe: []const u8 = std.fs.selfExePath(&exe_buf) catch return .{}; // We have an exe path! Climb the tree looking for the terminfo // bundle as we expect it. @@ -49,17 +79,22 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 { if (comptime builtin.target.os.tag.isDarwin()) { inline for (sentinels) |sentinel| { if (try maybeDir(&dir_buf, dir, "Contents/Resources", sentinel)) |v| { - return try std.fs.path.join(alloc, &.{ v, "ghostty" }); + return .{ .app_path = try std.fs.path.join(alloc, &.{ v, "ghostty" }) }; } } } - // On all platforms, we look for a /usr/share style path. This + // On all platforms (except BSD), we look for a /usr/share style path. This // is valid even on Mac since there is nothing that requires // Ghostty to be in an app bundle. inline for (sentinels) |sentinel| { - if (try maybeDir(&dir_buf, dir, "share", sentinel)) |v| { - return try std.fs.path.join(alloc, &.{ v, "ghostty" }); + if (try maybeDir( + &dir_buf, + dir, + if (builtin.target.os.tag == .freebsd) "local/share" else "share", + sentinel, + )) |v| { + return .{ .app_path = try std.fs.path.join(alloc, &.{ v, "ghostty" }) }; } } } @@ -68,14 +103,14 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 { // fallback and use the provided resources dir. if (comptime builtin.mode == .Debug) { if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| { - if (dir.len > 0) return dir; + if (dir.len > 0) return .{ .app_path = dir }; } else |err| switch (err) { error.EnvironmentVariableNotFound => {}, else => return err, } } - return null; + return .{}; } /// Little helper to check if the "base/sub/suffix" directory exists and diff --git a/src/os/systemd.zig b/src/os/systemd.zig new file mode 100644 index 000000000..9b67296d6 --- /dev/null +++ b/src/os/systemd.zig @@ -0,0 +1,65 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const log = std.log.scoped(.systemd); + +/// Returns true if the program was launched as a systemd service. +/// +/// On Linux, this returns true if the program was launched as a systemd +/// service. It will return false if Ghostty was launched any other way. +/// +/// For other platforms and app runtimes, this returns false. +pub fn launchedBySystemd() bool { + return switch (builtin.os.tag) { + .linux => linux: { + // On Linux, systemd sets the `INVOCATION_ID` (v232+) and the + // `JOURNAL_STREAM` (v231+) environment variables. If these + // environment variables are not present we were not launched by + // systemd. + if (std.posix.getenv("INVOCATION_ID") == null) break :linux false; + if (std.posix.getenv("JOURNAL_STREAM") == null) break :linux false; + + // If `INVOCATION_ID` and `JOURNAL_STREAM` are present, check to make sure + // that our parent process is actually `systemd`, not some other terminal + // emulator that doesn't clean up those environment variables. + const ppid = std.os.linux.getppid(); + if (ppid == 1) break :linux true; + + // If the parent PID is not 1 we need to check to see if we were launched by + // a user systemd daemon. Do that by checking the `/proc/<ppid>/comm` + // to see if it ends with `systemd`. + var comm_path_buf: [std.fs.max_path_bytes]u8 = undefined; + const comm_path = std.fmt.bufPrint(&comm_path_buf, "/proc/{d}/comm", .{ppid}) catch { + log.err("unable to format comm path for pid {d}", .{ppid}); + break :linux false; + }; + const comm_file = std.fs.openFileAbsolute(comm_path, .{ .mode = .read_only }) catch { + log.err("unable to open '{s}' for reading", .{comm_path}); + break :linux false; + }; + defer comm_file.close(); + + // The maximum length of the command name is defined by + // `TASK_COMM_LEN` in the Linux kernel. This is usually 16 + // bytes at the time of writing (Jun 2025) so its set to that. + // Also, since we only care to compare to "systemd", anything + // longer can be assumed to not be systemd. + const TASK_COMM_LEN = 16; + var comm_data_buf: [TASK_COMM_LEN]u8 = undefined; + const comm_size = comm_file.readAll(&comm_data_buf) catch { + log.err("problems reading from '{s}'", .{comm_path}); + break :linux false; + }; + const comm_data = comm_data_buf[0..comm_size]; + + break :linux std.mem.eql( + u8, + std.mem.trimRight(u8, comm_data, "\n"), + "systemd", + ); + }, + + // No other system supports systemd so always return false. + else => false, + }; +} |
