summaryrefslogtreecommitdiff
path: root/src/host_io.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/host_io.zig')
-rw-r--r--src/host_io.zig130
1 files changed, 103 insertions, 27 deletions
diff --git a/src/host_io.zig b/src/host_io.zig
index 0f6fd87e..a019e15d 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -12,6 +12,9 @@ pub const Host = struct {
pub const VTable = struct {
wait_input: ?*const fn (ctx: ?*anyopaque, timeout_ms: u32) void = null,
+ /// The shell's monotonic clock in ns: the only time the core sees.
+ /// Without one, core animation moves only by `.tick` events.
+ now: ?*const fn (ctx: ?*anyopaque) u64 = null,
present: ?*const fn (ctx: ?*anyopaque, surface: *const pardes.Surface) void = null,
post_present: ?*const fn (ctx: ?*anyopaque) void = null,
poll_frame: ?*const fn (ctx: ?*anyopaque) void = null,
@@ -39,6 +42,20 @@ pub const Host = struct {
};
};
+/// The shells' monotonic clock in ns, the one `Host.now` answers with.
+/// PARDES_TEST_CLOCK makes a shell answer a virtual clock instead, moved
+/// only when a timed wait runs out -- straight to the core's next wake -- so
+/// the snapshot harness sees the same frames whatever the machine's load.
+pub fn monotonicNs() u64 {
+ var ts: libc.timespec = undefined;
+ if (libc.clock_gettime(.MONOTONIC, &ts) != 0) return 0;
+ return @as(u64, @intCast(ts.sec)) * std.time.ns_per_s + @as(u64, @intCast(ts.nsec));
+}
+
+pub fn testClock() ?u64 {
+ return if (libc.getenv("PARDES_TEST_CLOCK")) |v| (if (std.mem.span(v).len != 0) 0 else null) else null;
+}
+
pub const Fallback = struct {
pub const dump_path = "pardes.dump.zon";
@@ -743,8 +760,13 @@ pub const Shell = struct {
return .{ .path = path, .argv = .{ path, marks[0], marks[1], null } };
}
- fn find(bin: []const u8, buf: *[std.fs.max_path_bytes]u8) ?[*:0]const u8 {
+ /// A shell by its path or its name in the usual directories, or null.
+ /// A path may be under a mount this editor serves: the turn goes out
+ /// with the lookups (pardes.Turn.yield).
+ pub fn find(bin: []const u8, buf: *[std.fs.max_path_bytes]u8) ?[*:0]const u8 {
if (bin.len == 0 or bin.len + 1 > buf.len) return null;
+ pardes.turn.yield();
+ defer pardes.turn.back();
if (std.mem.indexOfScalar(u8, bin, '/') != null) {
@memcpy(buf[0..bin.len], bin);
buf[bin.len] = 0;
@@ -975,14 +997,22 @@ pub fn forkShell(
if (stat.kind != .directory) return error.NotDir;
break :dir path;
};
- // The turn was given up for the stat: a 9P client may have closed the
- // pane, and another taken its slot, whose command this is not.
- if (core) |c| if ((if (c.panes[pane]) |pn| pn.serial else 0) != serial) return error.PaneGone;
var master: c_int = -1;
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
- // A terminal opened on a shell of its own (`Tty fish`) runs that one.
- const own = if (core) |c| if (c.panes[pane]) |pn| pn.shell else null else null;
+ // A terminal opened on a shell of its own (`Tty fish`) runs that one,
+ // copied: looking it up gives the turn up, and the pane may go.
+ var own_buf: [256]u8 = undefined;
+ var own: ?[]const u8 = null;
+ if (core) |c| if (c.panes[pane]) |pn| if (pn.shell) |name| {
+ const n = @min(name.len, own_buf.len);
+ @memcpy(own_buf[0..n], name[0..n]);
+ own = own_buf[0..n];
+ };
var spawn = Shell.resolve(own orelse bin, &path_buf, prompt_rcs);
+ // The turn was given up for the stat and the lookup: a 9P client may
+ // have closed the pane, and another taken its slot, whose command this
+ // is not.
+ if (core) |c| if ((if (c.panes[pane]) |pn| pn.serial else 0) != serial) return error.PaneGone;
// A command pane's child is the shell running its one line, which has
// no prompt to mark (exec.zig runCommand).
const one_line = if (core) |c| if (c.panes[pane]) |pn| pn.command else null else null;
@@ -1413,12 +1443,6 @@ fn watchChild(pid: libc.pid_t) void {
_ = libc.nanosleep(&ts, null);
}
wakeForExit();
- // Once more a little later: a job the command left holding its pty
- // may keep its end of file from ever coming, and the host tells the
- // exit then (`CommandWatch.due`).
- const ts: libc.timespec = .{ .sec = 0, .nsec = (CommandWatch.grace_ms + 10) * std.time.ns_per_ms };
- _ = libc.nanosleep(&ts, null);
- wakeForExit();
}
fn wakeForExit() void {
@@ -1427,33 +1451,85 @@ fn wakeForExit() void {
if (exit_wake) |wake| wake.f(wake.ctx);
}
-/// Where a host's command pane is between its child's exit and its pty's
-/// end of file. The exit is told once the output before it has been read,
-/// at end of file -- the exit is often seen first, with output still in
-/// the pty -- or after `grace_ms` without one, a job the command left in
-/// the background holding the pty.
+/// Where a host's command pane is between its child's exit and the end of
+/// its output. The exit is told once the output before it is in: at the
+/// pty's end of file; or, when something else still holds the pty (a job
+/// the command left in the background), once nothing is waiting to be read
+/// -- checked when the exit is seen and again after each chunk of output,
+/// with no timer to miss. acme's waitthread tells an exit when it is
+/// reaped (acme.c:587-700); a terminal also has output in flight.
pub const CommandWatch = struct {
- pub const grace_ms = 50;
watched: bool = false,
+ /// The master, to ask whether output is still waiting.
+ fd: c_int = -1,
eof: bool = false,
exited: bool = false,
told: bool = false,
status: ?u8 = null,
- at: i64 = 0,
-
- /// The child is reaped, with this status.
- pub fn exit(w: *CommandWatch, status: ?u8) void {
- w.exited = true;
- w.status = status;
- w.at = nowMs();
- }
/// Whether the exit is to be told now.
pub fn due(w: *const CommandWatch) bool {
- return w.exited and !w.told and (w.eof or nowMs() - w.at >= grace_ms);
+ if (!w.exited or w.told) return false;
+ if (w.eof) return true;
+ var pfd = [1]libc.pollfd{.{ .fd = w.fd, .events = libc.POLL.IN, .revents = 0 }};
+ if (libc.poll(&pfd, 1, 0) < 0) return true;
+ // Every holder of its terminal gone: the end of file comes, after
+ // the rest of the output.
+ if (pfd[0].revents & libc.POLL.HUP != 0) return false;
+ return pfd[0].revents & libc.POLL.IN == 0;
}
};
+/// A host's pty slot, whichever way it keeps it: an optional, or one whose
+/// `fd` is -1 when empty.
+fn SlotPty(comptime S: type) type {
+ return switch (@typeInfo(S)) {
+ .optional => |o| *o.child,
+ else => *S,
+ };
+}
+
+fn slotPty(slot: anytype) ?SlotPty(@TypeOf(slot.*)) {
+ return switch (@typeInfo(@TypeOf(slot.*))) {
+ .optional => if (slot.*) |*pt| pt else null,
+ else => if (slot.fd < 0) null else slot,
+ };
+}
+
+/// Each watched child that exited: reaped, and the core told once its
+/// output is in, its pty closed (`close(ctx, id)`) if its end of file came
+/// first. Every host runs this after an exit's wake and after each chunk
+/// of a command's output; `ptys` is its slot array, each slot with a `pid`
+/// and a `cmd`.
+pub fn takeExits(core: *pardes.Pardes, ptys: anytype, ctx: anytype, comptime close: anytype) void {
+ while (takeExited()) |pid| for (ptys) |*slot| {
+ const pt = slotPty(slot) orelse continue;
+ if (!pt.cmd.watched or pt.pid != pid or pt.cmd.exited) continue;
+ pt.cmd.status = (reapExited(pid) orelse break).status;
+ pt.cmd.exited = true;
+ pt.pid = 0; // reaped: no signal or retire may reach whoever gets it next
+ break;
+ };
+ for (ptys, 0..) |*slot, id| {
+ const pt = slotPty(slot) orelse continue;
+ if (!pt.cmd.watched or !pt.cmd.due()) continue;
+ pt.cmd.told = true;
+ const eof = pt.cmd.eof;
+ core.update(.{ .exited = .{ .pane = @intCast(id), .status = pt.cmd.status } });
+ if (eof) close(ctx, id);
+ }
+}
+
+/// A watched command's pty reached end of file: the core hears its output
+/// is over; the pty is closed now if its exit was told, else when it is.
+pub fn commandEof(core: *pardes.Pardes, ptys: anytype, id: usize, ctx: anytype, comptime close: anytype) void {
+ const pt = slotPty(&ptys[id]) orelse return;
+ pt.cmd.eof = true;
+ core.update(.{ .eof = .{ .pane = @intCast(id) } });
+ if (pt.cmd.told) return close(ctx, id);
+ takeExits(core, ptys, ctx, close);
+}
+
/// Reaps `pid` if it has exited: its status (null: unknown, or not ours
/// to read), or null when it is still running -- a pid a watcher reported
/// may since have been reaped by another and taken by a new child.