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.zig276
1 files changed, 271 insertions, 5 deletions
diff --git a/src/host_io.zig b/src/host_io.zig
index f1a4eb98..a019e15d 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -760,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;
@@ -966,6 +971,9 @@ test "the child environment replaces the launcher's terminal identity exactly on
extern "c" fn unsetenv(name: [*:0]const u8) c_int;
+/// The longest line a command pane runs (exec.zig refuses a longer one).
+pub const exec_command_max = pardes.exec.command_max;
+
pub fn forkShell(
core: ?*pardes.Pardes,
pane: usize,
@@ -979,6 +987,7 @@ pub fn forkShell(
const native_cwd = filesystem.localPath(cwd) orelse cwd;
if (std.mem.indexOfScalar(u8, native_cwd, 0) != null) return error.InvalidPath;
var cwd_buf: [4096]u8 = undefined;
+ const serial = if (core) |c| if (c.panes[pane]) |pn| pn.serial else 0 else 0;
const cwd_z: ?[:0]const u8 = if (native_cwd.len == 0) null else dir: {
const path = std.fmt.bufPrintSentinel(&cwd_buf, "{s}", .{native_cwd}, 0) catch return error.NameTooLong;
// A shell's directory may be inside a mount this editor serves.
@@ -990,9 +999,32 @@ pub fn forkShell(
};
var master: c_int = -1;
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
- const spawn = Shell.resolve(bin, &path_buf, prompt_rcs);
+ // 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;
+ var command_buf: [exec_command_max + 1]u8 = undefined;
+ if (one_line) |line| {
+ if (line.len > exec_command_max or std.mem.indexOfScalar(u8, line, 0) != null) return error.InvalidCommand;
+ @memcpy(command_buf[0..line.len], line);
+ command_buf[line.len] = 0;
+ spawn.argv = .{ spawn.path, "-c", command_buf[0..line.len :0].ptr, null };
+ }
var helper_buf: [4096]u8 = undefined;
- const mounted = if (core) |c| if (c.panes[pane]) |pn| pn.v9fs_on_spawn else false else false;
+ const mounted = one_line == null and if (core) |c| if (c.panes[pane]) |pn| pn.v9fs_on_spawn else false else false;
const helper: ?[:0]u8 = if (mounted) blk: {
if (comptime builtin.os.tag != .linux) return error.LinuxRequired;
if (fs == null) return error.SessionSocketRequired;
@@ -1033,7 +1065,7 @@ pub fn forkShell(
_exit(127);
}
ninep_io.setCloexec(master);
- if (core) |c| c.acknowledgeShell(pane, std.mem.span(spawn.path), spawn.argv[1] != null);
+ if (core) |c| c.acknowledgeShell(pane, std.mem.span(spawn.path), one_line == null and spawn.argv[1] != null);
return .{ .file = .{ .handle = master, .flags = .{ .nonblocking = false } }, .pid = pid };
}
@@ -1366,6 +1398,185 @@ pub fn retireShell(pid: libc.pid_t) void {
thread.detach();
}
+// ---- a command pane's child, watched to its exit ----
+//
+// A command pane's command is done when its process exits, not when its pty
+// reaches end of file: a job it put in the background can hold the pty open
+// after it has gone, and a command can close its terminal and run on. A
+// thread per command waits for the exit without reaping (WNOWAIT), so the
+// pid is still the child's -- no Kill can reach another process that took
+// it -- until the host, told through `takeExited`, reaps it in its own loop
+// with `exitStatus` and tells the core.
+
+const waitid_p_pid: c_int = 1;
+const waitid_wexited: c_int = 4;
+const waitid_wnowait: c_int = if (builtin.os.tag == .linux) 0x01000000 else 0x20;
+extern "c" fn waitid(idtype: c_int, id: c_uint, infop: *anyopaque, options: c_int) c_int;
+
+var exited: [pardes.MAX_PANES * 2]std.atomic.Value(libc.pid_t) = @splat(.init(0));
+var exit_lock: libc.pthread_mutex_t = .{};
+var exit_wake: ?struct { ctx: ?*anyopaque, f: *const fn (?*anyopaque) void } = null;
+
+/// Who wakes the host's loop when a watched child exits; null at shutdown,
+/// after which a watcher still waiting tells nobody.
+pub fn setExitWake(ctx: ?*anyopaque, f: ?*const fn (?*anyopaque) void) void {
+ _ = libc.pthread_mutex_lock(&exit_lock);
+ defer _ = libc.pthread_mutex_unlock(&exit_lock);
+ exit_wake = if (f) |wake| .{ .ctx = ctx, .f = wake } else null;
+}
+
+/// Watches a command pane's child; false when no thread could be started,
+/// and the host then reads its exit at end of file instead.
+pub fn watchExit(pid: libc.pid_t) bool {
+ const thread = std.Thread.spawn(.{}, watchChild, .{pid}) catch return false;
+ thread.detach();
+ return true;
+}
+
+fn watchChild(pid: libc.pid_t) void {
+ var info: [128]u8 align(16) = undefined;
+ while (waitid(waitid_p_pid, @intCast(pid), &info, waitid_wexited | waitid_wnowait) < 0 and libc.errno(-1) == .INTR) {}
+ // A cell for it, waiting for one if the host is slow to take them.
+ place: while (true) {
+ for (&exited) |*cell| if (cell.cmpxchgStrong(0, pid, .acq_rel, .acquire) == null) break :place;
+ const ts: libc.timespec = .{ .sec = 0, .nsec = 5 * std.time.ns_per_ms };
+ _ = libc.nanosleep(&ts, null);
+ }
+ wakeForExit();
+}
+
+fn wakeForExit() void {
+ _ = libc.pthread_mutex_lock(&exit_lock);
+ defer _ = libc.pthread_mutex_unlock(&exit_lock);
+ if (exit_wake) |wake| wake.f(wake.ctx);
+}
+
+/// 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 {
+ 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,
+
+ /// Whether the exit is to be told now.
+ pub fn due(w: *const CommandWatch) bool {
+ 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.
+pub fn reapExited(pid: libc.pid_t) ?struct { status: ?u8 } {
+ if (pid <= 0) return .{ .status = null };
+ var status: c_int = 0;
+ const got = libc.waitpid(pid, &status, libc.W.NOHANG);
+ if (got == 0) return null;
+ if (got != pid) return .{ .status = null };
+ const s: u32 = @bitCast(status);
+ if (libc.W.IFEXITED(s)) return .{ .status = @truncate(libc.W.EXITSTATUS(s)) };
+ if (libc.W.IFSIGNALED(s)) return .{ .status = @truncate(128 + @as(u32, @intFromEnum(libc.W.TERMSIG(s)))) };
+ return .{ .status = null };
+}
+
+/// A watched child that has exited, not yet reaped, or null.
+pub fn takeExited() ?libc.pid_t {
+ for (&exited) |*cell| {
+ const pid = cell.swap(0, .acq_rel);
+ if (pid != 0) return pid;
+ }
+ return null;
+}
+
+/// A pane's child at its pty's end of file: reaped, and how it exited
+/// (its code, or 128 and the signal that ended it), if it is gone within
+/// `wait_ms`; null if it is still running (a job it left holds nothing
+/// open, but the child itself may be a moment behind its pty), which the
+/// caller then retires as it would any shell.
+pub fn exitStatus(pid: libc.pid_t, wait_ms: u32) ?u8 {
+ if (pid <= 0) return null;
+ var waited: u32 = 0;
+ while (true) : (waited += 2) {
+ var status: c_int = 0;
+ const got = libc.waitpid(pid, &status, libc.W.NOHANG);
+ if (got == pid) {
+ const s: u32 = @bitCast(status);
+ if (libc.W.IFEXITED(s)) return @truncate(libc.W.EXITSTATUS(s));
+ if (libc.W.IFSIGNALED(s)) return @truncate(128 + @as(u32, @intFromEnum(libc.W.TERMSIG(s))));
+ return null;
+ }
+ if (got < 0 or waited >= wait_ms) return null;
+ const ts: libc.timespec = .{ .sec = 0, .nsec = 2 * std.time.ns_per_ms };
+ _ = libc.nanosleep(&ts, null);
+ }
+}
+
fn reapShell(pid: libc.pid_t, grace_ms: u32) void {
var waited: u32 = 0;
while (waited < grace_ms) : (waited += 5) {
@@ -1669,7 +1880,7 @@ fn haveFile(path: [*:0]const u8) bool {
return true;
}
-fn nowMs() i64 {
+pub fn nowMs() i64 {
var ts: libc.timespec = undefined;
_ = libc.clock_gettime(.MONOTONIC, &ts);
return @as(i64, @intCast(ts.sec)) * 1000 + @divFloor(@as(i64, @intCast(ts.nsec)), 1_000_000);
@@ -1728,6 +1939,61 @@ test "Kill stops the foreground job, never the shell, and finds none with job co
try std.testing.expectEqual(@as(libc.pid_t, 0), libc.waitpid(sh.pid, null, libc.W.NOHANG));
}
+test "a command pane's child is its shell running the one line, and its exit status is read at the end" {
+ if (comptime !tty_probe_platform) return error.SkipZigTest;
+ const core = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer core.deinit();
+ while (core.nextEffect()) |_| {}
+ const id = core.freeSlot().?;
+ _ = try core.newCommand(id, "", "printf 'said '; exit 7");
+ while (core.nextEffect()) |_| {}
+ const rcs: Shell.PromptFiles = .{};
+ const child = try forkShell(core, id, &rcs, "/bin/sh", "", 24, 80, null);
+ defer _ = libc.close(child.file.handle);
+ var got: [256]u8 = undefined;
+ var n: usize = 0;
+ while (n < got.len) {
+ const r = libc.read(child.file.handle, got[n..].ptr, got.len - n);
+ if (r <= 0) break; // EIO once the child is gone: the pty's end
+ n += @intCast(r);
+ }
+ try std.testing.expect(std.mem.indexOf(u8, got[0..n], "said") != null);
+ try std.testing.expectEqual(@as(?u8, 7), exitStatus(child.pid, 2000));
+}
+
+test "a command's exit is its process's, told by its watcher, whether or not its pty has ended" {
+ if (comptime !tty_probe_platform) return error.SkipZigTest;
+ const core = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer core.deinit();
+ while (core.nextEffect()) |_| {}
+ const rcs: Shell.PromptFiles = .{};
+ const Case = struct { line: []const u8, status: u8 };
+ // A job left in the background holds the pty open past the command's
+ // exit; a command that lets go of its terminal runs on past its end of
+ // file. Either way the exit is the process's.
+ for ([_]Case{
+ .{ .line = "sleep 3 & exit 4", .status = 4 },
+ .{ .line = "exec </dev/null >/dev/null 2>&1; sleep 0.3; exit 5", .status = 5 },
+ }) |c| {
+ const id = core.freeSlot().?;
+ _ = try core.newCommand(id, "", c.line);
+ while (core.nextEffect()) |_| {}
+ const child = try forkShell(core, id, &rcs, "/bin/sh", "", 24, 80, null);
+ defer _ = libc.close(child.file.handle);
+ try std.testing.expect(watchExit(child.pid));
+ var got: ?libc.pid_t = null;
+ var waited: i64 = 0;
+ while (got == null and waited < 5000) : (waited += 10) {
+ got = takeExited();
+ if (got == null) sleepMs(10);
+ }
+ try std.testing.expectEqual(@as(?libc.pid_t, child.pid), got);
+ // Told before it is reaped: the pid is still the child's.
+ try std.testing.expectEqual(@as(?u8, c.status), reapExited(child.pid).?.status);
+ try core.removePane(id, null);
+ }
+}
+
test "a retired shell that ignores the hangup is killed and reaped, not left a zombie" {
const pid = libc.fork();
if (pid == 0) {