Skip to content

Commit f338dd0

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 f8f9f70 commit f338dd0

File tree

2 files changed

+187
-36
lines changed

2 files changed

+187
-36
lines changed

src/os/flatpak.zig

Lines changed: 122 additions & 8 deletions
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

Lines changed: 65 additions & 28 deletions
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,28 @@ 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+
watcher: {
155+
if (comptime build_config.flatpak) {
156+
if (self.subprocess.flatpak_command) |*c| {
157+
c.waitXev(
158+
td.loop,
159+
&td.backend.exec.flatpak_wait_c,
160+
termio.Termio.ThreadData,
161+
td,
162+
flatpakExit,
163+
);
164+
break :watcher;
165+
}
166+
}
167+
168+
process.?.wait(
169+
td.loop,
170+
&td.backend.exec.process_wait_c,
171+
termio.Termio.ThreadData,
172+
td,
173+
processExit,
174+
);
175+
}
156176

157177
// Start our termios timer. We don't support this on Windows.
158178
// Fundamentally, we could support this on Windows so we're just
@@ -339,15 +359,7 @@ fn execFailedInChild() !void {
339359
_ = try reader.read(&buf);
340360
}
341361

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_.?;
362+
fn processExitCommon(td: *termio.Termio.ThreadData, exit_code: u32) void {
351363
assert(td.backend == .exec);
352364
const execdata = &td.backend.exec;
353365
execdata.exited = true;
@@ -393,7 +405,7 @@ fn processExit(
393405
}, null);
394406
td.mailbox.notify();
395407

396-
return .disarm;
408+
return;
397409
}
398410

399411
// If we're purposely waiting then we just return since the process
@@ -413,17 +425,36 @@ fn processExit(
413425
t.modes.set(.cursor_visible, false);
414426
}
415427

416-
return .disarm;
428+
return;
417429
}
418430

419431
// Notify our surface we want to close
420432
_ = td.surface_mailbox.push(.{
421433
.child_exited = {},
422434
}, .{ .forever = {} });
435+
}
423436

437+
fn processExit(
438+
td_: ?*termio.Termio.ThreadData,
439+
_: *xev.Loop,
440+
_: *xev.Completion,
441+
r: xev.Process.WaitError!u32,
442+
) xev.CallbackAction {
443+
const exit_code = r catch unreachable;
444+
processExitCommon(td_.?, exit_code);
424445
return .disarm;
425446
}
426447

448+
fn flatpakExit(
449+
td_: ?*termio.Termio.ThreadData,
450+
_: *xev.Loop,
451+
_: *internal_os.FlatpakHostCommand.Completion,
452+
r: internal_os.FlatpakHostCommand.WaitError!u8,
453+
) void {
454+
const exit_code = r catch unreachable;
455+
processExitCommon(td_.?, exit_code);
456+
}
457+
427458
fn termiosTimer(
428459
td_: ?*termio.Termio.ThreadData,
429460
_: *xev.Loop,
@@ -610,6 +641,7 @@ pub const ThreadData = struct {
610641
// The preallocation size for the write request pool. This should be big
611642
// enough to satisfy most write requests. It must be a power of 2.
612643
const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5);
644+
const FlatpakCompletion = if (Subprocess.FlatpakHostCommand != void) Subprocess.FlatpakHostCommand.Completion else struct {};
613645

614646
/// Process start time and boolean of whether its already exited.
615647
start: std.time.Instant,
@@ -630,7 +662,7 @@ pub const ThreadData = struct {
630662
write_stream: xev.Stream,
631663

632664
/// The process watcher
633-
process: xev.Process,
665+
process: ?xev.Process,
634666

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

681+
flatpak_wait_c: FlatpakCompletion = .{},
682+
649683
/// Reader thread state
650684
read_thread: std.Thread,
651685
read_thread_pipe: posix.fd_t,
@@ -670,7 +704,7 @@ pub const ThreadData = struct {
670704
self.write_buf_pool.deinit(alloc);
671705

672706
// Stop our process watcher
673-
self.process.deinit();
707+
if (self.process) |*p| p.deinit();
674708

675709
// Stop our write stream
676710
self.write_stream.deinit();
@@ -763,6 +797,9 @@ const Subprocess = struct {
763797

764798
// Add our binary to the path if we can find it.
765799
ghostty_path: {
800+
// Skip this for flatpak since host cannot reach them
801+
if (internal_os.isFlatpak() and FlatpakHostCommand != void) break :ghostty_path;
802+
766803
var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
767804
const exe_bin_path = std.fs.selfExePath(&exe_buf) catch |err| {
768805
log.warn("failed to get ghostty exe path err={}", .{err});

0 commit comments

Comments
 (0)