diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/os/flatpak.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/os/flatpak.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/os/flatpak.zig | 507 |
1 files changed, 507 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/os/flatpak.zig b/vendor/libghostty-vt/src/os/flatpak.zig new file mode 100644 index 00000000..78692089 --- /dev/null +++ b/vendor/libghostty-vt/src/os/flatpak.zig @@ -0,0 +1,507 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const builtin = @import("builtin"); +const posix = std.posix; +const xev = @import("../global.zig").xev; + +const log = std.log.scoped(.flatpak); + +/// Returns true if we're running in a Flatpak environment. +pub fn isFlatpak() bool { + // If we're not on Linux then we'll make this comptime false. + if (comptime builtin.os.tag != .linux) return false; + return if (std.fs.accessAbsolute("/.flatpak-info", .{})) true else |_| false; +} + +/// A struct to help execute commands on the host via the +/// org.freedesktop.Flatpak.Development DBus module. This uses GIO/GLib +/// under the hood. +/// +/// This always spawns its own thread and maintains its own GLib event loop. +/// This makes it easy for the command to behave synchronously similar to +/// std.process.Child. +/// +/// There are lots of chances for low-hanging improvements here (automatic +/// pipes, /dev/null, etc.) but this was purpose built for my needs so +/// it doesn't have all of those. +/// +/// Requires GIO, GLib to be available and linked. +pub const FlatpakHostCommand = struct { + const fd_t = posix.fd_t; + const EnvMap = std.process.EnvMap; + const c = @cImport({ + @cInclude("gio/gio.h"); + @cInclude("gio/gunixfdlist.h"); + }); + + /// Argv are the arguments to call on the host with argv[0] being + /// the command to execute. + argv: []const []const u8, + + /// The cwd for the new process. If this is not set then it will use + /// the current cwd of the calling process. + cwd: ?[:0]const u8 = null, + + /// Environment variables for the child process. If this is null, this + /// does not send any environment variables. + env: ?*const EnvMap = null, + + /// File descriptors to send to the child process. It is up to the + /// caller to create the file descriptors and set them up. + stdin: fd_t, + stdout: fd_t, + stderr: fd_t, + + /// State of the process. This is updated by the dedicated thread it + /// runs in and is protected by the given lock and condition variable. + state: State = .{ .init = {} }, + state_mutex: std.Thread.Mutex = .{}, + state_cv: std.Thread.Condition = .{}, + + /// State the process is in. This can't be inspected directly, you + /// must use getters on the struct to get access. + const State = union(enum) { + /// Initial state + init: void, + + /// Error starting. The error message is only available via logs. + /// (This isn't a fundamental limitation, just didn't need the + /// error message yet) + err: void, + + /// Process started with the given pid on the host. + started: struct { + pid: u32, + loop_xev: ?*xev.Loop, + completion: ?*Completion, + subscription: c.guint, + loop: *c.GMainLoop, + }, + + /// Process exited + exited: struct { + pid: u32, + status: u8, + }, + }; + + pub const Completion = struct { + callback: *const fn (ud: ?*anyopaque, l: *xev.Loop, c: *Completion, r: WaitError!u8) void = noopCallback, + c_xev: xev.Completion = .{}, + userdata: ?*anyopaque = null, + timer: ?xev.Timer = null, + result: ?WaitError!u8 = null, + }; + + /// Errors that are possible from us. + pub const Error = error{ + FlatpakMustBeStarted, + FlatpakSpawnFail, + FlatpakSetupFail, + FlatpakRPCFail, + }; + + pub const WaitError = xev.Timer.RunError || Error; + + /// Spawn the command. This will start the host command. On return, + /// the pid will be available. This must only be called with the + /// state in "init". + /// + /// Precondition: The self pointer MUST be stable. + 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(); + defer self.state_mutex.unlock(); + while (self.state == .init) self.state_cv.wait(&self.state_mutex); + + return switch (self.state) { + .init => unreachable, + .err => Error.FlatpakSpawnFail, + .started => |v| v.pid, + .exited => |v| v.pid, + }; + } + + /// Wait for the process to end and return the exit status. This + /// can only be called ONCE. Once this returns, the state is reset. + pub fn wait(self: *FlatpakHostCommand) !u8 { + self.state_mutex.lock(); + defer self.state_mutex.unlock(); + + while (true) { + switch (self.state) { + .init => return Error.FlatpakMustBeStarted, + .err => return Error.FlatpakSpawnFail, + .started => {}, + .exited => |v| { + self.state = .{ .init = {} }; + self.state_cv.broadcast(); + return v.status; + }, + } + + self.state_cv.wait(&self.state_mutex); + } + } + + /// Wait for the process to end asynchronously via libxev. This + /// can only be called ONCE. + pub fn waitXev( + self: *FlatpakHostCommand, + loop: *xev.Loop, + completion: *Completion, + comptime Userdata: type, + userdata: ?*Userdata, + comptime cb: *const fn ( + ud: ?*Userdata, + l: *xev.Loop, + c: *Completion, + r: WaitError!u8, + ) void, + ) void { + self.state_mutex.lock(); + defer self.state_mutex.unlock(); + + completion.* = .{ + .callback = (struct { + fn callback( + ud_: ?*anyopaque, + l_inner: *xev.Loop, + c_inner: *Completion, + r: WaitError!u8, + ) void { + const ud = @as(?*Userdata, if (Userdata == void) null else @ptrCast(@alignCast(ud_))); + @call(.always_inline, cb, .{ ud, l_inner, c_inner, r }); + } + }).callback, + .userdata = userdata, + .timer = xev.Timer.init() catch unreachable, // not great, but xev timer can't fail atm + }; + + switch (self.state) { + .init => completion.result = Error.FlatpakMustBeStarted, + .err => completion.result = Error.FlatpakSpawnFail, + .started => |*v| { + v.loop_xev = loop; + v.completion = completion; + return; + }, + .exited => |v| { + completion.result = v.status; + }, + } + + completion.timer.?.run( + loop, + &completion.c_xev, + 0, + anyopaque, + completion.userdata, + (struct { + fn callback( + ud: ?*anyopaque, + l_inner: *xev.Loop, + c_inner: *xev.Completion, + r: xev.Timer.RunError!void, + ) xev.CallbackAction { + const c_outer: *Completion = @fieldParentPtr("c_xev", c_inner); + defer if (c_outer.timer) |*t| t.deinit(); + + const result = if (r) |_| c_outer.result.? else |err| err; + c_outer.callback(ud, l_inner, c_outer, result); + return .disarm; + } + }).callback, + ); + } + + /// Send a signal to the started command. This does nothing if the + /// command is not in the started state. + pub fn signal(self: *FlatpakHostCommand, sig: u8, pg: bool) !void { + const pid = pid: { + self.state_mutex.lock(); + defer self.state_mutex.unlock(); + switch (self.state) { + .started => |v| break :pid v.pid, + else => return, + } + }; + + // Get our bus connection. + 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}); + return Error.FlatpakSetupFail; + }; + defer c.g_object_unref(bus); + + const reply = c.g_dbus_connection_call_sync( + bus, + "org.freedesktop.Flatpak", + "/org/freedesktop/Flatpak/Development", + "org.freedesktop.Flatpak.Development", + "HostCommandSignal", + c.g_variant_new( + "(uub)", + pid, + sig, + @as(c_int, @intCast(@intFromBool(pg))), + ), + c.G_VARIANT_TYPE("()"), + c.G_DBUS_CALL_FLAGS_NONE, + c.G_MAXINT, + null, + &g_err, + ); + if (g_err != null) { + log.warn("signal send error: {s}", .{g_err.?.*.message}); + return; + } + defer c.g_variant_unref(reply); + } + + fn threadMain(self: *FlatpakHostCommand, alloc: Allocator) void { + // Create a new thread-local context so that all our sources go + // to this context and we can run our loop correctly. + const ctx = c.g_main_context_new(); + defer c.g_main_context_unref(ctx); + c.g_main_context_push_thread_default(ctx); + defer c.g_main_context_pop_thread_default(ctx); + + // Get our loop for the current thread + const loop = c.g_main_loop_new(ctx, 1).?; + defer c.g_main_loop_unref(loop); + + // 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.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}); + self.updateState(.{ .err = {} }); + return; + }; + defer c.g_object_unref(bus); + + // Spawn the command first. This will setup all our IO. + self.start(alloc, bus, loop) catch |err| { + log.warn("error starting host command: {}", .{err}); + self.updateState(.{ .err = {} }); + return; + }; + + // Run the event loop. It quits in the exit callback. + c.g_main_loop_run(loop); + } + + /// Start the command. This will start the host command and set the + /// pid field on success. This will not wait for completion. + /// + /// Once this is called, the self pointer MUST remain stable. This + /// requirement is due to using GLib under the covers with callbacks. + fn start( + self: *FlatpakHostCommand, + alloc: Allocator, + bus: *c.GDBusConnection, + loop: *c.GMainLoop, + ) !void { + 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(); + + // Our list of file descriptors that we need to send to the process. + 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}); + return Error.FlatpakSetupFail; + } + if (c.g_unix_fd_list_append(fd_list, self.stdout, &err) < 0) { + 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}); + return Error.FlatpakSetupFail; + } + + // Build our arguments for the file descriptors. + const fd_builder = c.g_variant_builder_new(c.G_VARIANT_TYPE("a{uh}")); + defer c.g_variant_builder_unref(fd_builder); + c.g_variant_builder_add(fd_builder, "{uh}", @as(c_int, 0), self.stdin); + c.g_variant_builder_add(fd_builder, "{uh}", @as(c_int, 1), self.stdout); + c.g_variant_builder_add(fd_builder, "{uh}", @as(c_int, 2), self.stderr); + + // Build our env vars + const env_builder = c.g_variant_builder_new(c.G_VARIANT_TYPE("a{ss}")); + defer c.g_variant_builder_unref(env_builder); + if (self.env) |env| { + var it = env.iterator(); + while (it.next()) |pair| { + const key = try arena.dupeZ(u8, pair.key_ptr.*); + const value = try arena.dupeZ(u8, pair.value_ptr.*); + c.g_variant_builder_add(env_builder, "{ss}", key.ptr, value.ptr); + } + } + + // Build our args + const args = try arena.alloc(?[*:0]u8, self.argv.len + 1); + for (0.., self.argv) |i, arg| { + const argZ = try arena.dupeZ(u8, arg); + args[i] = argZ.ptr; + } + args[args.len - 1] = null; + + // Get the cwd in case we don't have ours set. A small optimization + // would be to do this only if we need it but this isn't a + // common code path. + const g_cwd = c.g_get_current_dir(); + defer c.g_free(g_cwd); + + // The params for our RPC call + const params = c.g_variant_new( + "(^ay^aay@a{uh}@a{ss}u)", + @as(*const anyopaque, if (self.cwd) |*cwd| cwd.ptr else g_cwd), + args.ptr, + c.g_variant_builder_end(fd_builder), + c.g_variant_builder_end(env_builder), + @as(c_int, 0), + ); + _ = c.g_variant_ref_sink(params); // take ownership + defer c.g_variant_unref(params); + + // Subscribe to exit notifications + const subscription_id = c.g_dbus_connection_signal_subscribe( + bus, + "org.freedesktop.Flatpak", + "org.freedesktop.Flatpak.Development", + "HostCommandExited", + "/org/freedesktop/Flatpak/Development", + null, + 0, + onExit, + self, + null, + ); + errdefer c.g_dbus_connection_signal_unsubscribe(bus, subscription_id); + + // Go! + const reply = c.g_dbus_connection_call_with_unix_fd_list_sync( + bus, + "org.freedesktop.Flatpak", + "/org/freedesktop/Flatpak/Development", + "org.freedesktop.Flatpak.Development", + "HostCommand", + params, + c.G_VARIANT_TYPE("(u)"), + c.G_DBUS_CALL_FLAGS_NONE, + c.G_MAXINT, + fd_list, + null, + null, + &err, + ) orelse { + log.warn("Flatpak.HostCommand failed: {s}", .{err.?.*.message}); + return Error.FlatpakRPCFail; + }; + defer c.g_variant_unref(reply); + + var pid: u32 = 0; + c.g_variant_get(reply, "(u)", &pid); + log.debug("HostCommand started pid={} subscription={}", .{ + pid, + subscription_id, + }); + + self.updateState(.{ + .started = .{ + .pid = pid, + .subscription = subscription_id, + .loop = loop, + .completion = null, + .loop_xev = null, + }, + }); + } + + /// Helper to update the state and notify waiters via the cv. + fn updateState(self: *FlatpakHostCommand, state: State) void { + self.state_mutex.lock(); + defer self.state_mutex.unlock(); + defer self.state_cv.broadcast(); + self.state = state; + } + + fn onExit( + bus: ?*c.GDBusConnection, + _: [*c]const u8, + _: [*c]const u8, + _: [*c]const u8, + _: [*c]const u8, + params: ?*c.GVariant, + ud: ?*anyopaque, + ) callconv(.c) void { + const self = @as(*FlatpakHostCommand, @ptrCast(@alignCast(ud))); + const state = state: { + self.state_mutex.lock(); + defer self.state_mutex.unlock(); + break :state self.state.started; + }; + + var pid: u32 = 0; + var exit_status_raw: u32 = 0; + c.g_variant_get(params.?, "(uu)", &pid, &exit_status_raw); + if (state.pid != pid) return; + + const exit_status = posix.W.EXITSTATUS(exit_status_raw); + // Update our state + self.updateState(.{ + .exited = .{ + .pid = pid, + .status = exit_status, + }, + }); + if (state.completion) |completion| { + completion.result = exit_status; + completion.timer.?.run( + state.loop_xev.?, + &completion.c_xev, + 0, + anyopaque, + completion.userdata, + (struct { + fn callback( + ud_inner: ?*anyopaque, + l_inner: *xev.Loop, + c_inner: *xev.Completion, + r: xev.Timer.RunError!void, + ) xev.CallbackAction { + const c_outer: *Completion = @fieldParentPtr("c_xev", c_inner); + defer if (c_outer.timer) |*t| t.deinit(); + + const result = if (r) |_| c_outer.result.? else |err| err; + c_outer.callback(ud_inner, l_inner, c_outer, result); + return .disarm; + } + }).callback, + ); + } + log.debug("HostCommand exited pid={} status={}", .{ pid, exit_status }); + + // We're done now, so we can unsubscribe + c.g_dbus_connection_signal_unsubscribe(bus.?, state.subscription); + + // We are also done with our loop so we can exit. + c.g_main_loop_quit(state.loop); + } + + fn noopCallback(_: ?*anyopaque, _: *xev.Loop, _: *Completion, _: WaitError!u8) void {} +}; |
