summaryrefslogtreecommitdiff
path: root/src/ninep
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep')
-rw-r--r--src/ninep/pty.zig68
-rw-r--r--src/ninep/tree.zig20
2 files changed, 86 insertions, 2 deletions
diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig
index 37132f90..393516ce 100644
--- a/src/ninep/pty.zig
+++ b/src/ninep/pty.zig
@@ -296,6 +296,12 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void {
const done = marks.finished orelse return;
if (done.seq != slot.want) return;
pf.run = null;
+ finish(p, slot, pane, done.status);
+}
+
+/// Answers a run whose command ended with `status`: the header and the
+/// output it printed.
+fn finish(p: *Pardes, slot: *Run, pane: *Pane, status_code: ?i32) void {
const printed = pardes.panes.Terminal.commandOutput(pane, p.gpa, output_rows) catch
return answer(p, slot, "error out of memory", .{});
defer p.gpa.free(printed.text);
@@ -314,7 +320,7 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void {
}
// A D that carries no status says nothing of how the command went.
var code: [16]u8 = undefined;
- const status = if (done.status) |s| std.fmt.bufPrint(&code, "{d}", .{s}) catch unreachable else "?";
+ const status = if (status_code) |s| std.fmt.bufPrint(&code, "{d}", .{s}) catch unreachable else "?";
// The header is the whole first line, so a count there can never be
// mistaken for output; `cut` with no count: its start is not there to
// count from.
@@ -326,6 +332,22 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void {
answer(p, slot, "exit {s}", .{status});
}
+/// The shell itself exited (a run's `exit 3`, or one typed): a run waiting
+/// on the command it ran answers with the shell's status and what it
+/// printed, and the log says `exit` before the pane's `del`. Unknown, the
+/// status is not guessed: the run hears `error shell gone` when the pane
+/// goes.
+pub fn shellExited(p: *Pardes, pane: *Pane, status: ?u8) void {
+ const code = status orelse return;
+ if (waitingRun(p, pane)) |slot| if (slot.phase == .sent) {
+ pane.fs.run = null;
+ pardes.panes.Terminal.endOutputHere(pane);
+ finish(p, slot, pane, code);
+ };
+ var buf: [4]u8 = undefined;
+ pardes.exec.noteRun(p, pane, "exit", std.fmt.bufPrint(&buf, "{d}", .{code}) catch unreachable);
+}
+
/// The pane closed or its shell was replaced: the command's end will never
/// be reported, so the run says so instead of waiting forever. A line not
/// sent yet waits on for the replacement's first prompt.
@@ -763,8 +785,52 @@ test "run answers how its command ended once the shell is back at a prompt" {
_ = call(p, .{ .tag = 18, .op = .write, .node = node, .handle = gone.reply.handle, .data = "sleep 9\n" });
try p.removePane(0, null);
try testing.expectEqualStrings("error shell gone\n", call(p, .{ .tag = 19, .op = .read, .node = node, .handle = gone.reply.handle, .size = 64 }).bytes);
+ // It still stats while the run is open (a reader's fstat), on the
+ // open or on the file's name, and not once it is released.
+ try testing.expectEqual(Status.ok, call(p, .{ .tag = 19, .op = .getattr, .node = node, .handle = gone.reply.handle }).reply.status);
+ try testing.expectEqual(Status.ok, call(p, .{ .tag = 19, .op = .getattr, .node = node }).reply.status);
_ = call(p, .{ .tag = 20, .op = .release, .node = node, .handle = gone.reply.handle });
for (p.fs.opens) |o| try testing.expect(o.node == 0);
+ try testing.expectEqual(E.NOENT, call(p, .{ .tag = 21, .op = .getattr, .node = node }).errno());
+}
+
+test "a shell that exits under a run answers its status, and the log says exit before del" {
+ const gpa = testing.allocator;
+ const p = try withTerm(gpa);
+ defer p.deinit();
+ const serial = serialOf(p);
+ const node = Node.of(serial, .pty_run);
+ const prompt = "\x1b]133;A;cl=line;aid=pardes\x07$ \x1b]133;B;aid=pardes\x07";
+ p.update(.{ .output = .{ .pane = 0, .bytes = prompt } });
+ while (p.nextEffect()) |_| {}
+ const log = @intFromEnum(tree.TopFile.log);
+ const f = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle;
+ const frozen = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = f, .size = 1 << 16 }).bytes.len;
+ _ = call(p, .{ .tag = 3, .op = .write, .node = log, .handle = f, .data = "follow" });
+
+ const r = call(p, .{ .tag = 4, .op = .open, .node = node });
+ _ = call(p, .{ .tag = 5, .op = .write, .node = node, .handle = r.reply.handle, .data = "echo bye; exit 3\n" });
+ p.update(.{ .output = .{ .pane = 0, .bytes = "echo bye; exit 3\r\n\x1b]133;C;aid=pardes\x07bye\r\n" } });
+ p.update(.{ .exited = .{ .pane = 0, .status = 3 } });
+ p.update(.{ .eof = .{ .pane = 0 } });
+ while (p.nextEffect()) |_| {}
+ try testing.expect(p.paneBySerial(serial) == null);
+ try testing.expectEqualStrings("exit 3\nbye\n", call(p, .{ .tag = 6, .op = .read, .node = node, .handle = r.reply.handle, .size = 64 }).bytes);
+ var want: [128]u8 = undefined;
+ // A followed log reads a record at a time: the exit, then the del.
+ var records: [2][]const u8 = undefined;
+ var at = frozen;
+ for (&records) |*rec| {
+ rec.* = try gpa.dupe(u8, call(p, .{ .tag = 7, .op = .read, .node = log, .handle = f, .off = at, .size = 4096 }).bytes);
+ at += rec.len;
+ }
+ defer for (records) |rec| gpa.free(rec);
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "exit {d} 3\n", .{serial}), records[0]);
+ try testing.expect(std.mem.startsWith(u8, records[1], try std.fmt.bufPrint(&want, "del {d} ", .{serial})));
+ // A write to its pty/data now finds no pane.
+ try testing.expectEqual(E.NOENT, wr(p, Node.of(serial, .pty_data), "x").errno());
+ _ = call(p, .{ .tag = 8, .op = .release, .node = node, .handle = r.reply.handle });
+ _ = call(p, .{ .tag = 9, .op = .release, .node = log, .handle = f });
}
test "run on a command pane says no shell is there" {
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index b3f3dde6..779ecff3 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -385,7 +385,7 @@ pub fn handle(p: *Pardes, req: Req) Reply {
};
return switch (req.op) {
.lookup => lookup(p, req, target),
- .getattr => .{ .tag = req.tag, .attr = attrOf(p, target) orelse return Reply.fail(req.tag, E.NOENT) },
+ .getattr => .{ .tag = req.tag, .attr = attrOf(p, target) orelse runAttr(p, req, target) orelse return Reply.fail(req.tag, E.NOENT) },
.setattr => setattr(p, req, target),
.open => open(p, req, target),
.release => release(p, req),
@@ -395,6 +395,24 @@ pub fn handle(p: *Pardes, req: Req) Reply {
};
}
+/// A run's answer outlives its pane, so its file still stats while a run
+/// is open on it (a reader's fstat, cat's), as acme's fid keeps its window.
+/// The stat names the open when it came on one; a mount's comes on the
+/// file's walked name, and serials are never reused, so any run open on
+/// this node is the reader's.
+fn runAttr(p: *Pardes, req: Req, target: Target) ?Reply.Attr {
+ const t = switch (target) {
+ .pane => |t| t,
+ .top => return null,
+ };
+ if (t.file != .pty_run) return null;
+ const run = if (req.handle != 0) openOf(p, req) else for (&p.fs.opens) |*o| {
+ if (o.node == req.node and o.what == .run) break o;
+ } else null;
+ if ((run orelse return null).what != .run) return null;
+ return .{ .name = t.file.fileName(), .node = Node.of(t.serial, t.file), .mode = t.file.mode(), .mtime = p.fs.started };
+}
+
fn attrOf(p: *Pardes, target: Target) ?Reply.Attr {
switch (target) {
.top => |f| return .{