summaryrefslogtreecommitdiff
path: root/src/ninep
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep')
-rw-r--r--src/ninep/events.zig7
-rw-r--r--src/ninep/pty.zig116
-rw-r--r--src/ninep/screen.zig4
3 files changed, 105 insertions, 22 deletions
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index fa0a4975..20cdeda4 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -208,10 +208,11 @@ pub fn readLog(p: *Pardes, req: Req) Reply {
if (slot.node != req.node) return Reply.fail(req.tag, E.INVAL);
const frozen = slot.bytes orelse &.{};
const out = p.fs.stage(p.gpa);
- if (!slot.follow or req.off < frozen.len) {
- const off = @min(req.off, frozen.len);
+ if (!slot.follow or slot.pos < frozen.len) {
+ const off = @min(slot.pos, frozen.len);
const len = @min(frozen.len - off, req.size);
out.appendSlice(p.gpa, frozen[off..][0..len]) catch return Reply.fail(req.tag, E.NOMEM);
+ slot.pos += len;
return .{ .tag = req.tag, .payload = .{ .staged = @intCast(len) } };
}
const q = &p.fs.log;
@@ -678,6 +679,8 @@ test "the log records whether or not anyone reads, and an open that follows wait
const frozen = call(p, .{ .tag = 6, .op = .read, .node = log, .handle = fh, .size = 8192 }).bytes.len;
try testing.expectEqual(Status.err, call(p, .{ .tag = 7, .op = .write, .node = log, .handle = fh, .data = "tail\n" }).reply.status);
try testing.expectEqual(Status.ok, call(p, .{ .tag = 8, .op = .write, .node = log, .handle = fh, .data = "follow\n" }).reply.status);
+ // A stream: after `echo follow >&3` a shell reads at offset 7, and gets
+ // what comes next, not the frozen text from byte 7.
const rdf = struct {
fn next(pp: *Pardes, node: u64, h: u32, off: u64) th.Answer {
return call(pp, .{ .tag = 9, .op = .read, .node = node, .handle = h, .off = off, .size = 8192 });
diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig
index c7a32fbf..ec2e18e5 100644
--- a/src/ninep/pty.zig
+++ b/src/ninep/pty.zig
@@ -109,7 +109,7 @@ pub fn readStatus(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
// command. The shell's own OSC 133 marks say when the command started (C)
// and ended (D, with its status); pardes injects them into bash and fish.
-pub const run_slots = 8;
+pub const run_slots = 32;
pub const Run = struct {
/// 0 is a free slot.
@@ -120,8 +120,16 @@ pub const Run = struct {
/// The shell's prompt count when the line went: a newer prompt with no
/// C for it means the shell never ran the line.
prompts: u32 = 0,
- /// The refused line was taken back off the prompt.
- cleared: bool = false,
+ /// The shell's count of lines it handled without running when the line
+ /// went.
+ declined: u32 = 0,
+ /// How the refused line is being taken back.
+ cleared: enum { no, kept_line, continuation } = .no,
+ /// How much of the answer this open has read. A stream: a shell's
+ /// `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,
answer: [48]u8 = undefined,
len: u8 = 0,
};
@@ -151,6 +159,7 @@ 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
const line = std.mem.trimEnd(u8, req.data, "\r\n");
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);
@@ -159,14 +168,19 @@ pub fn writeRun(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply {
const marks = &state.stream.handler;
if (pf.unmarked) {
answer(slot, "error no prompt marks", .{});
- } else if (pf.run != null or !pardes.panes.Terminal.promptInputEmpty(pane) or p.hostTtyTaken(id)) {
+ } else if (pf.run != null or marks.phase != .input or
+ !pardes.panes.Terminal.promptInputEmpty(pane) or p.hostTtyTaken(id))
+ {
// Something is running, someone has typed at the prompt, or the
// shell has not drawn its first one yet: sending now would type into
- // the middle of it.
+ // the middle of it. The phase is pardes's own marks: a nested
+ // shell's prompt (ssh, a shell with its own integration) looks like
+ // an empty prompt to ghostty but is not the shell this run knows.
answer(slot, "busy", .{});
} else {
slot.want = marks.started +% 1;
slot.prompts = marks.prompts;
+ slot.declined = marks.declined;
p.emitWrite(id, line);
p.emitWrite(id, "\r");
slot.phase = .sent;
@@ -182,10 +196,10 @@ pub fn readRun(p: *Pardes, req: Req) Reply {
.sent => return .{ .tag = req.tag, .status = .again },
.done => {},
}
- const text = slot.answer[0..slot.len];
- const off = @min(req.off, text.len);
- const n = @min(text.len - off, req.size);
- p.fs.stage(p.gpa).appendSlice(p.gpa, text[off..][0..n]) catch return Reply.fail(req.tag, E.NOMEM);
+ 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) } };
}
@@ -214,14 +228,40 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void {
// ponytail: a prompt redrawn before the command starts (a resize in that
// instant) reads as not run; a per-command id in the marks would tell.
if (marks.started != slot.want and marks.prompts != slot.prompts) {
- // fish keeps a refused line on the prompt for editing; it is the one
- // this run typed into an empty prompt, so take it back (Ctrl-U), and
- // answer once it is gone so the next line finds the prompt clear.
- if (!pardes.panes.Terminal.promptInputEmpty(pane)) {
- if (!slot.cleared) p.emitWrite(id, "\x15");
- slot.cleared = true;
- return;
+ // fish keeps a refused line on the prompt for editing, and bash's
+ // continuation prompt holds an unfinished one (its line looks empty
+ // but the command is still pending). Either is the line this run
+ // typed into an empty prompt, so throw it away with Ctrl-C, which
+ // drops the whole pending command, and answer at the prompt after
+ // that, so the next line finds the shell clear.
+ const empty = pardes.panes.Terminal.promptInputEmpty(pane);
+ switch (slot.cleared) {
+ .no => {
+ if (marks.continuation) {
+ // bash holds the start of the command: Ctrl-C drops all of it
+ p.emitWrite(id, "\x03");
+ slot.cleared = .continuation;
+ slot.prompts = marks.prompts; // gone at the next prompt
+ return;
+ }
+ if (!empty) {
+ // fish keeps the refused line on the prompt; a run's line
+ // is one line, so Ctrl-U takes it all (a Ctrl-C sent while
+ // fish redraws is lost)
+ p.emitWrite(id, "\x15");
+ slot.cleared = .kept_line;
+ return;
+ }
+ // bash said it handled the line (a D) and cleared its prompt
+ if (marks.declined == slot.declined) return;
+ // Otherwise an empty ordinary prompt means fish has not drawn
+ // the kept line yet, or a redraw landed before the line ran
+ // (its C is still coming): the next output says which.
+ },
+ .continuation => if (marks.continuation) return,
+ .kept_line => {},
}
+ if (!empty) return;
answer(slot, "error not run", .{});
pf.run = null;
return;
@@ -563,11 +603,25 @@ test "run answers how its command ended once the shell is back at a prompt" {
out.put(p, "\x1b]133;C;aid=pardes\x07\x1b]133;D;2;aid=pardes\x07");
try testing.expectEqual(Status.again, call(p, .{ .tag = 9, .op = .read, .node = node, .handle = h, .size = 64 }).reply.status);
out.put(p, prompt);
- try testing.expectEqualStrings("exit 2\n", call(p, .{ .tag = 10, .op = .read, .node = node, .handle = h, .size = 64 }).bytes);
- // The answer stays for a re-read until the open closes.
- try testing.expectEqualStrings("exit 2\n", call(p, .{ .tag = 11, .op = .read, .node = node, .handle = h, .size = 64 }).bytes);
+ // 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);
+ 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 });
+ // While a command runs, a nested shell's own (untagged) prompt looks
+ // like an empty prompt to the emulator; it is not this shell's.
+ const outer = call(p, .{ .tag = 30, .op = .open, .node = node });
+ _ = call(p, .{ .tag = 31, .op = .write, .node = node, .handle = outer.reply.handle, .data = "ssh box\n" });
+ out.put(p, "\x1b]133;C;aid=pardes\x07\x1b]133;A\x07remote$ \x1b]133;B\x07");
+ const nested = call(p, .{ .tag = 32, .op = .open, .node = node });
+ _ = call(p, .{ .tag = 33, .op = .write, .node = node, .handle = nested.reply.handle, .data = "true\n" });
+ 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);
+ _ = call(p, .{ .tag = 37, .op = .release, .node = node, .handle = outer.reply.handle });
+
// Typed text on the prompt line: busy, rather than typing into it.
out.put(p, "ls -");
const typed = call(p, .{ .tag = 13, .op = .open, .node = node });
@@ -580,10 +634,32 @@ test "run answers how its command ended once the shell is back at a prompt" {
// a fresh prompt and no C. Waiting for a D would wait forever.
const refused = call(p, .{ .tag = 21, .op = .open, .node = node });
_ = call(p, .{ .tag = 22, .op = .write, .node = node, .handle = refused.reply.handle, .data = "(exit 7)\n" });
- out.put(p, "\r\nfish: command substitutions not allowed\r\n" ++ prompt);
+ out.put(p, "\r\nfish: command substitutions not allowed\r\n" ++ prompt ++ "(exit 7)");
+ try testing.expectEqual(Status.again, call(p, .{ .tag = 23, .op = .read, .node = node, .handle = refused.reply.handle, .size = 64 }).reply.status);
+ out.put(p, "^C\r\n" ++ prompt);
try testing.expectEqualStrings("error not run\n", call(p, .{ .tag = 23, .op = .read, .node = node, .handle = refused.reply.handle, .size = 64 }).bytes);
_ = call(p, .{ .tag = 24, .op = .release, .node = node, .handle = refused.reply.handle });
+ // bash on a syntax error prints no C but still a D before its prompt.
+ const bad = call(p, .{ .tag = 40, .op = .open, .node = node });
+ _ = call(p, .{ .tag = 41, .op = .write, .node = node, .handle = bad.reply.handle, .data = "fi\n" });
+ out.put(p, "fi\r\nbash: syntax error\r\n\x1b]133;D;2;aid=pardes\x07" ++ prompt);
+ try testing.expectEqualStrings("error not run\n", call(p, .{ .tag = 42, .op = .read, .node = node, .handle = bad.reply.handle, .size = 64 }).bytes);
+ _ = call(p, .{ .tag = 43, .op = .release, .node = node, .handle = bad.reply.handle });
+
+ // An unfinished line (an open quote) leaves bash at its continuation
+ // prompt, tagged too: the line is thrown away with Ctrl-C, then answered.
+ const open_quote = call(p, .{ .tag = 25, .op = .open, .node = node });
+ const typed_quote = call(p, .{ .tag = 26, .op = .write, .node = node, .handle = open_quote.reply.handle, .data = "echo 'x\n" });
+ try testing.expectEqualStrings("echo 'x\r", typed_quote.pty());
+ const cont = call(p, .{ .tag = 27, .op = .read, .node = node, .handle = open_quote.reply.handle, .size = 64 });
+ try testing.expectEqual(Status.again, cont.reply.status);
+ out.put(p, "echo 'x\r\n\x1b]133;A;k=c;aid=pardes\x07> \x1b]133;B;aid=pardes\x07");
+ try testing.expectEqual(Status.again, call(p, .{ .tag = 28, .op = .read, .node = node, .handle = open_quote.reply.handle, .size = 64 }).reply.status);
+ out.put(p, "^C\r\n" ++ prompt);
+ try testing.expectEqualStrings("error not run\n", call(p, .{ .tag = 29, .op = .read, .node = node, .handle = open_quote.reply.handle, .size = 64 }).bytes);
+ _ = call(p, .{ .tag = 29, .op = .release, .node = node, .handle = open_quote.reply.handle });
+
// The pane going away while a command runs ends the wait.
out.put(p, "\r\n" ++ prompt);
const gone = call(p, .{ .tag = 17, .op = .open, .node = node });
diff --git a/src/ninep/screen.zig b/src/ninep/screen.zig
index 1743fbb6..89ca369b 100644
--- a/src/ninep/screen.zig
+++ b/src/ninep/screen.zig
@@ -19,6 +19,10 @@ pub const Snapshot = struct {
next: u64 = 0,
/// How much of record `next` a short read already took.
part: u32 = 0,
+ /// /log only: how much of `bytes` this open has read. Reads walk it as a
+ /// stream and ignore their offset, since `exec 3<>log; echo follow >&3`
+ /// leaves the shared offset past the word it wrote.
+ pos: u64 = 0,
};
pub const snapshot_slots = 32;