Skip to content

Commit 1b52d5c

Browse files
committed
termio, flatpak: implement process watcher with xev
This allows `termio.Exec` to track processes spawned via `FlatpakHostCommand`, finally allowing Ghostty to function as a Flatpak. Alongside this is a few bug fixes: * Don't add ghostty to PATH when running in flatpak mode since it's unreachable. * Correctly handle exit status returned by Flatpak. Previously this was not processed and contains extra status bits. * Use correct type for PID returned by Flatpak.
1 parent 0ecee3e commit 1b52d5c

File tree

2 files changed

+182
-36
lines changed

2 files changed

+182
-36
lines changed

src/os/flatpak.zig

+122-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const assert = std.debug.assert;
33
const Allocator = std.mem.Allocator;
44
const builtin = @import("builtin");
55
const posix = std.posix;
6+
const xev = @import("../global.zig").xev;
67

78
const log = std.log.scoped(.flatpak);
89

@@ -71,18 +72,28 @@ pub const FlatpakHostCommand = struct {
7172

7273
/// Process started with the given pid on the host.
7374
started: struct {
74-
pid: c_int,
75+
pid: u32,
76+
loop_xev: ?*xev.Loop,
77+
completion: ?*Completion,
7578
subscription: c.guint,
7679
loop: *c.GMainLoop,
7780
},
7881

7982
/// Process exited
8083
exited: struct {
81-
pid: c_int,
84+
pid: u32,
8285
status: u8,
8386
},
8487
};
8588

89+
pub const Completion = struct {
90+
callback: *const fn (ud: ?*anyopaque, l: *xev.Loop, c: *Completion, r: WaitError!u8) void = noopCallback,
91+
c_xev: xev.Completion = .{},
92+
userdata: ?*anyopaque = null,
93+
timer: ?xev.Timer = null,
94+
result: ?WaitError!u8 = null,
95+
};
96+
8697
/// Errors that are possible from us.
8798
pub const Error = error{
8899
FlatpakMustBeStarted,
@@ -91,12 +102,14 @@ pub const FlatpakHostCommand = struct {
91102
FlatpakRPCFail,
92103
};
93104

105+
pub const WaitError = xev.Timer.RunError || Error;
106+
94107
/// Spawn the command. This will start the host command. On return,
95108
/// the pid will be available. This must only be called with the
96109
/// state in "init".
97110
///
98111
/// Precondition: The self pointer MUST be stable.
99-
pub fn spawn(self: *FlatpakHostCommand, alloc: Allocator) !c_int {
112+
pub fn spawn(self: *FlatpakHostCommand, alloc: Allocator) !u32 {
100113
const thread = try std.Thread.spawn(.{}, threadMain, .{ self, alloc });
101114
thread.setName("flatpak-host-command") catch {};
102115

@@ -135,6 +148,77 @@ pub const FlatpakHostCommand = struct {
135148
}
136149
}
137150

151+
/// Wait for the process to end asynchronously via libxev. This
152+
/// can only be called ONCE.
153+
pub fn waitXev(
154+
self: *FlatpakHostCommand,
155+
loop: *xev.Loop,
156+
completion: *Completion,
157+
comptime Userdata: type,
158+
userdata: ?*Userdata,
159+
comptime cb: *const fn (
160+
ud: ?*Userdata,
161+
l: *xev.Loop,
162+
c: *Completion,
163+
r: WaitError!u8,
164+
) void,
165+
) void {
166+
self.state_mutex.lock();
167+
defer self.state_mutex.unlock();
168+
169+
completion.* = .{
170+
.callback = (struct {
171+
fn callback(
172+
ud_: ?*anyopaque,
173+
l_inner: *xev.Loop,
174+
c_inner: *Completion,
175+
r: WaitError!u8,
176+
) void {
177+
const ud = @as(?*Userdata, if (Userdata == void) null else @ptrCast(@alignCast(ud_)));
178+
@call(.always_inline, cb, .{ ud, l_inner, c_inner, r });
179+
}
180+
}).callback,
181+
.userdata = userdata,
182+
.timer = xev.Timer.init() catch unreachable, // not great, but xev timer can't fail atm
183+
};
184+
185+
switch (self.state) {
186+
.init => completion.result = Error.FlatpakMustBeStarted,
187+
.err => completion.result = Error.FlatpakSpawnFail,
188+
.started => |*v| {
189+
v.loop_xev = loop;
190+
v.completion = completion;
191+
return;
192+
},
193+
.exited => |v| {
194+
completion.result = v.status;
195+
},
196+
}
197+
198+
completion.timer.?.run(
199+
loop,
200+
&completion.c_xev,
201+
0,
202+
anyopaque,
203+
completion.userdata,
204+
(struct {
205+
fn callback(
206+
ud: ?*anyopaque,
207+
l_inner: *xev.Loop,
208+
c_inner: *xev.Completion,
209+
r: xev.Timer.RunError!void,
210+
) xev.CallbackAction {
211+
const c_outer: *Completion = @fieldParentPtr("c_xev", c_inner);
212+
defer if (c_outer.timer) |*t| t.deinit();
213+
214+
const result = if (r) |_| c_outer.result.? else |err| err;
215+
c_outer.callback(ud, l_inner, c_outer, result);
216+
return .disarm;
217+
}
218+
}).callback,
219+
);
220+
}
221+
138222
/// Send a signal to the started command. This does nothing if the
139223
/// command is not in the started state.
140224
pub fn signal(self: *FlatpakHostCommand, sig: u8, pg: bool) !void {
@@ -326,7 +410,7 @@ pub const FlatpakHostCommand = struct {
326410
};
327411
defer c.g_variant_unref(reply);
328412

329-
var pid: c_int = 0;
413+
var pid: u32 = 0;
330414
c.g_variant_get(reply, "(u)", &pid);
331415
log.debug("HostCommand started pid={} subscription={}", .{
332416
pid,
@@ -338,6 +422,8 @@ pub const FlatpakHostCommand = struct {
338422
.pid = pid,
339423
.subscription = subscription_id,
340424
.loop = loop,
425+
.completion = null,
426+
.loop_xev = null,
341427
},
342428
});
343429
}
@@ -366,18 +452,44 @@ pub const FlatpakHostCommand = struct {
366452
break :state self.state.started;
367453
};
368454

369-
var pid: c_int = 0;
370-
var exit_status: c_int = 0;
371-
c.g_variant_get(params.?, "(uu)", &pid, &exit_status);
455+
var pid: u32 = 0;
456+
var exit_status_raw: u32 = 0;
457+
c.g_variant_get(params.?, "(uu)", &pid, &exit_status_raw);
372458
if (state.pid != pid) return;
373459

460+
const exit_status = posix.W.EXITSTATUS(exit_status_raw);
374461
// Update our state
375462
self.updateState(.{
376463
.exited = .{
377464
.pid = pid,
378-
.status = std.math.cast(u8, exit_status) orelse 255,
465+
.status = exit_status,
379466
},
380467
});
468+
if (state.completion) |completion| {
469+
completion.result = exit_status;
470+
completion.timer.?.run(
471+
state.loop_xev.?,
472+
&completion.c_xev,
473+
0,
474+
anyopaque,
475+
completion.userdata,
476+
(struct {
477+
fn callback(
478+
ud_inner: ?*anyopaque,
479+
l_inner: *xev.Loop,
480+
c_inner: *xev.Completion,
481+
r: xev.Timer.RunError!void,
482+
) xev.CallbackAction {
483+
const c_outer: *Completion = @fieldParentPtr("c_xev", c_inner);
484+
defer if (c_outer.timer) |*t| t.deinit();
485+
486+
const result = if (r) |_| c_outer.result.? else |err| err;
487+
c_outer.callback(ud_inner, l_inner, c_outer, result);
488+
return .disarm;
489+
}
490+
}).callback,
491+
);
492+
}
381493
log.debug("HostCommand exited pid={} status={}", .{ pid, exit_status });
382494

383495
// We're done now, so we can unsubscribe
@@ -386,4 +498,6 @@ pub const FlatpakHostCommand = struct {
386498
// We are also done with our loop so we can exit.
387499
c.g_main_loop_quit(state.loop);
388500
}
501+
502+
fn noopCallback(_: ?*anyopaque, _: *xev.Loop, _: *Completion, _: WaitError!u8) void {}
389503
};

src/termio/Exec.zig

+60-28
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,20 @@ pub fn threadEnter(
9595
};
9696
errdefer self.subprocess.stop();
9797

98-
// Get the pid from the subprocess
99-
const pid = pid: {
100-
const command = self.subprocess.command orelse return error.ProcessNotStarted;
101-
break :pid command.pid orelse return error.ProcessNoPid;
98+
// Watcher to detect subprocess exit
99+
var process = process: {
100+
// Get the pid from the subprocess
101+
const pid = pid: {
102+
if (self.subprocess.flatpak_command) |_| {
103+
break :process null;
104+
}
105+
const command = self.subprocess.command orelse return error.ProcessNotStarted;
106+
break :pid command.pid orelse return error.ProcessNoPid;
107+
};
108+
109+
break :process try xev.Process.init(pid);
102110
};
111+
errdefer if (process) |*p| p.deinit();
103112

104113
// Track our process start time for abnormal exits
105114
const process_start = try std.time.Instant.now();
@@ -114,10 +123,6 @@ pub fn threadEnter(
114123
var stream = xev.Stream.initFd(pty_fds.write);
115124
errdefer stream.deinit();
116125

117-
// Watcher to detect subprocess exit
118-
var process = try xev.Process.init(pid);
119-
errdefer process.deinit();
120-
121126
// Start our timer to read termios state changes. This is used
122127
// to detect things such as when password input is being done
123128
// so we can render the terminal in a different way.
@@ -146,13 +151,23 @@ pub fn threadEnter(
146151
} };
147152

148153
// Start our process watcher
149-
process.wait(
150-
td.loop,
151-
&td.backend.exec.process_wait_c,
152-
termio.Termio.ThreadData,
153-
td,
154-
processExit,
155-
);
154+
if (self.subprocess.flatpak_command) |*c| {
155+
c.waitXev(
156+
td.loop,
157+
&td.backend.exec.flatpak_wait_c,
158+
termio.Termio.ThreadData,
159+
td,
160+
flatpakExit,
161+
);
162+
} else {
163+
process.?.wait(
164+
td.loop,
165+
&td.backend.exec.process_wait_c,
166+
termio.Termio.ThreadData,
167+
td,
168+
processExit,
169+
);
170+
}
156171

157172
// Start our termios timer. We don't support this on Windows.
158173
// Fundamentally, we could support this on Windows so we're just
@@ -339,15 +354,7 @@ fn execFailedInChild() !void {
339354
_ = try reader.read(&buf);
340355
}
341356

342-
fn processExit(
343-
td_: ?*termio.Termio.ThreadData,
344-
_: *xev.Loop,
345-
_: *xev.Completion,
346-
r: xev.Process.WaitError!u32,
347-
) xev.CallbackAction {
348-
const exit_code = r catch unreachable;
349-
350-
const td = td_.?;
357+
fn processExitCommon(td: *termio.Termio.ThreadData, exit_code: u32) void {
351358
assert(td.backend == .exec);
352359
const execdata = &td.backend.exec;
353360
execdata.exited = true;
@@ -393,7 +400,7 @@ fn processExit(
393400
}, null);
394401
td.mailbox.notify();
395402

396-
return .disarm;
403+
return;
397404
}
398405

399406
// If we're purposely waiting then we just return since the process
@@ -413,17 +420,36 @@ fn processExit(
413420
t.modes.set(.cursor_visible, false);
414421
}
415422

416-
return .disarm;
423+
return;
417424
}
418425

419426
// Notify our surface we want to close
420427
_ = td.surface_mailbox.push(.{
421428
.child_exited = {},
422429
}, .{ .forever = {} });
430+
}
423431

432+
fn processExit(
433+
td_: ?*termio.Termio.ThreadData,
434+
_: *xev.Loop,
435+
_: *xev.Completion,
436+
r: xev.Process.WaitError!u32,
437+
) xev.CallbackAction {
438+
const exit_code = r catch unreachable;
439+
processExitCommon(td_.?, exit_code);
424440
return .disarm;
425441
}
426442

443+
fn flatpakExit(
444+
td_: ?*termio.Termio.ThreadData,
445+
_: *xev.Loop,
446+
_: *internal_os.FlatpakHostCommand.Completion,
447+
r: internal_os.FlatpakHostCommand.WaitError!u8,
448+
) void {
449+
const exit_code = r catch unreachable;
450+
processExitCommon(td_.?, exit_code);
451+
}
452+
427453
fn termiosTimer(
428454
td_: ?*termio.Termio.ThreadData,
429455
_: *xev.Loop,
@@ -610,6 +636,7 @@ pub const ThreadData = struct {
610636
// The preallocation size for the write request pool. This should be big
611637
// enough to satisfy most write requests. It must be a power of 2.
612638
const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5);
639+
const FlatpakCompletion = if (Subprocess.FlatpakHostCommand != void) Subprocess.FlatpakHostCommand.Completion else void;
613640

614641
/// Process start time and boolean of whether its already exited.
615642
start: std.time.Instant,
@@ -630,7 +657,7 @@ pub const ThreadData = struct {
630657
write_stream: xev.Stream,
631658

632659
/// The process watcher
633-
process: xev.Process,
660+
process: ?xev.Process,
634661

635662
/// This is the pool of available (unused) write requests. If you grab
636663
/// one from the pool, you must put it back when you're done!
@@ -646,6 +673,8 @@ pub const ThreadData = struct {
646673
/// subsequently to wait for the data_stream to close.
647674
process_wait_c: xev.Completion = .{},
648675

676+
flatpak_wait_c: FlatpakCompletion = .{},
677+
649678
/// Reader thread state
650679
read_thread: std.Thread,
651680
read_thread_pipe: posix.fd_t,
@@ -670,7 +699,7 @@ pub const ThreadData = struct {
670699
self.write_buf_pool.deinit(alloc);
671700

672701
// Stop our process watcher
673-
self.process.deinit();
702+
if (self.process) |*p| p.deinit();
674703

675704
// Stop our write stream
676705
self.write_stream.deinit();
@@ -763,6 +792,9 @@ const Subprocess = struct {
763792

764793
// Add our binary to the path if we can find it.
765794
ghostty_path: {
795+
// Skip this for flatpak since host cannot reach them
796+
if (internal_os.isFlatpak() and FlatpakHostCommand != void) break :ghostty_path;
797+
766798
var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
767799
const exe_bin_path = std.fs.selfExePath(&exe_buf) catch |err| {
768800
log.warn("failed to get ghostty exe path err={}", .{err});

0 commit comments

Comments
 (0)