summaryrefslogtreecommitdiff
path: root/src/ninep/pty.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-27 19:37:05 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit7f31e62b63a64cef3916aac21417b7b1414000aa (patch)
tree75ae00e196e7eff22fc4659b512b12ce81f10cf6 /src/ninep/pty.zig
parent594970f18dee92308fd1554663cc408cda4d102c (diff)
downloadpardes-7f31e62b63a64cef3916aac21417b7b1414000aa.tar.gz
pardes-7f31e62b63a64cef3916aac21417b7b1414000aa.zip
pty/run answers with the command's output; pin cloud9 with 9ns concurrency and E.BUSY
The answer is now the header line (exit N, or exit N cut M when only the last 64 KiB were kept, or exit N cut when its start scrolled out of the history), then what the command printed: the screen text between its C and D marks, which the marks handler pins so scrolling keeps them. Also from review: a plain /log open honours its offset until follow is written, so tail -n and less work; whitespace-only run lines are refused. cloud9 is pinned at 82d8152c: 9ns keeps up to 32 requests in flight per mount so a waiting read no longer freezes the rest of it, and E.BUSY replaces the errno pardes spelled locally. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep/pty.zig')
-rw-r--r--src/ninep/pty.zig75
1 files changed, 63 insertions, 12 deletions
diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig
index ec2e18e5..a689d87f 100644
--- a/src/ninep/pty.zig
+++ b/src/ninep/pty.zig
@@ -111,6 +111,10 @@ pub fn readStatus(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
pub const run_slots = 32;
+/// The most of a command's output an answer carries: its last this-many
+/// bytes, since that is where a failure says what went wrong.
+pub const output_cap = 64 * 1024;
+
pub const Run = struct {
/// 0 is a free slot.
node: u64 = 0,
@@ -129,9 +133,17 @@ pub const Run = struct {
/// `exec 3<>run` shares one offset between the write and the read, so
/// the offset a read arrives with means nothing (acme's event file and
/// Plan 9's kprint read the same way).
- read: u8 = 0,
+ read: usize = 0,
+ /// The first line: how it ended.
answer: [48]u8 = undefined,
len: u8 = 0,
+ /// Then what the command printed, gpa-owned.
+ output: []u8 = &.{},
+
+ pub fn reset(slot: *Run, gpa: std.mem.Allocator, node: u64) void {
+ gpa.free(slot.output);
+ slot.* = .{ .node = node };
+ }
};
fn runOf(p: *Pardes, req: Req) ?*Run {
@@ -158,9 +170,10 @@ fn answer(slot: *Run, comptime fmt: []const u8, args: anytype) void {
pub fn writeRun(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
const slot = runOf(p, req) orelse return Reply.fail(req.tag, E.INVAL);
- if (slot.phase == .sent) return tree.failText(req.tag, 16, tree.e_in_use);
- slot.* = .{ .node = slot.node }; // a new line on this open: a new answer
+ if (slot.phase == .sent) return tree.failText(req.tag, E.BUSY, tree.e_in_use);
+ slot.reset(p.gpa, slot.node); // a new line on this open: a new answer
const line = std.mem.trimEnd(u8, req.data, "\r\n");
+ if (std.mem.trim(u8, line, " \t").len == 0) return tree.failText(req.tag, E.INVAL, e_bad_line);
if (line.len == 0) return tree.failText(req.tag, E.INVAL, e_bad_line);
for (line) |c| if (c < ' ' and c != '\t') return tree.failText(req.tag, E.INVAL, e_bad_line);
const pf = &p.fs.panes[id];
@@ -196,11 +209,21 @@ pub fn readRun(p: *Pardes, req: Req) Reply {
.sent => return .{ .tag = req.tag, .status = .again },
.done => {},
}
- const text = slot.answer[slot.read..slot.len];
- const n = @min(text.len, req.size);
- p.fs.stage(p.gpa).appendSlice(p.gpa, text[0..n]) catch return Reply.fail(req.tag, E.NOMEM);
- slot.read += @intCast(n);
- return .{ .tag = req.tag, .payload = .{ .staged = @intCast(n) } };
+ // The answer line, then the output: one stream, read from `read` on.
+ const out = p.fs.stage(p.gpa);
+ var at = slot.read;
+ for ([_][]const u8{ slot.answer[0..slot.len], slot.output }) |part| {
+ if (at >= part.len) {
+ at -= part.len;
+ continue;
+ }
+ const n = @min(part.len - at, req.size - out.items.len);
+ out.appendSlice(p.gpa, part[at..][0..n]) catch return Reply.fail(req.tag, E.NOMEM);
+ at = 0;
+ if (out.items.len == req.size) break;
+ }
+ slot.read += out.items.len;
+ return .{ .tag = req.tag, .payload = .{ .staged = @intCast(out.items.len) } };
}
pub fn releaseRun(p: *Pardes, req: Req) Reply {
@@ -210,7 +233,7 @@ pub fn releaseRun(p: *Pardes, req: Req) Reply {
for (&p.fs.panes) |*pf| if (pf.run == idx) {
pf.run = null;
};
- slot.* = .{};
+ slot.reset(p.gpa, 0);
return .{ .tag = req.tag };
}
@@ -268,7 +291,33 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void {
}
const done = marks.finished orelse return;
if (done.seq != slot.want) return;
- if (done.status) |status| answer(slot, "exit {d}", .{status}) else answer(slot, "exit", .{});
+ const printed = pardes.panes.Terminal.commandOutput(pane, p.gpa) catch null;
+ defer if (printed) |text| p.gpa.free(text);
+ const scrolled_out = if (state.stream.handler.out_start) |pin| pin.garbage else false;
+ // Keep the tail, cut at a line start so the first line kept is whole.
+ var keep: []const u8 = printed orelse "";
+ if (keep.len > output_cap) {
+ keep = keep[keep.len - output_cap ..];
+ if (std.mem.indexOfScalar(u8, keep, '\n')) |nl| keep = keep[nl + 1 ..];
+ }
+ const cut = (if (printed) |text| text.len else 0) - keep.len;
+ const status = done.status orelse 0;
+ // The header is the whole first line, so a count there can never be
+ // mistaken for output; `cut` with no count: its start scrolled away.
+ if (printed == null or scrolled_out)
+ answer(slot, "exit {d} cut", .{status})
+ else if (cut > 0)
+ answer(slot, "exit {d} cut {d}", .{ status, cut })
+ else
+ answer(slot, "exit {d}", .{status});
+ if (keep.len > 0) {
+ const nl = @intFromBool(keep[keep.len - 1] != '\n');
+ if (p.gpa.alloc(u8, keep.len + nl)) |owned| {
+ @memcpy(owned[0..keep.len], keep);
+ if (nl == 1) owned[keep.len] = '\n';
+ slot.output = owned;
+ } else |_| {}
+ }
pf.run = null;
}
@@ -605,7 +654,8 @@ test "run answers how its command ended once the shell is back at a prompt" {
out.put(p, prompt);
// A shell's `exec 3<>run; echo make >&3; cat <&3` reads at the offset
// its write left (5): the answer is a stream and comes whole anyway.
- try testing.expectEqualStrings("exit 2\n", call(p, .{ .tag = 10, .op = .read, .node = node, .handle = h, .off = 5, .size = 64 }).bytes);
+ // The answer is how it ended, then what it printed.
+ try testing.expectEqualStrings("exit 2\nbuilding\n", call(p, .{ .tag = 10, .op = .read, .node = node, .handle = h, .off = 5, .size = 64 }).bytes);
try testing.expectEqual(@as(usize, 0), call(p, .{ .tag = 11, .op = .read, .node = node, .handle = h, .size = 64 }).bytes.len);
_ = call(p, .{ .tag = 12, .op = .release, .node = node, .handle = h });
@@ -619,7 +669,8 @@ test "run answers how its command ended once the shell is back at a prompt" {
try testing.expectEqualStrings("busy\n", call(p, .{ .tag = 34, .op = .read, .node = node, .handle = nested.reply.handle, .size = 64 }).bytes);
_ = call(p, .{ .tag = 35, .op = .release, .node = node, .handle = nested.reply.handle });
out.put(p, "logout\r\n\x1b]133;D;0;aid=pardes\x07" ++ prompt);
- try testing.expectEqualStrings("exit 0\n", call(p, .{ .tag = 36, .op = .read, .node = node, .handle = outer.reply.handle, .size = 64 }).bytes);
+ const ssh = call(p, .{ .tag = 36, .op = .read, .node = node, .handle = outer.reply.handle, .size = 64 }).bytes;
+ try testing.expect(std.mem.startsWith(u8, ssh, "exit 0\n") and std.mem.indexOf(u8, ssh, "remote$ logout\n") != null);
_ = call(p, .{ .tag = 37, .op = .release, .node = node, .handle = outer.reply.handle });
// Typed text on the prompt line: busy, rather than typing into it.