diff options
Diffstat (limited to 'src/ninep')
| -rw-r--r-- | src/ninep/ctl.zig | 2 | ||||
| -rw-r--r-- | src/ninep/events.zig | 6 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 10 | ||||
| -rw-r--r-- | src/ninep/pty.zig | 24 | ||||
| -rw-r--r-- | src/ninep/screen.zig | 4 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 99 |
6 files changed, 124 insertions, 21 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 5bf354eb..060e0d6c 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -344,7 +344,7 @@ pub fn paneText(p: *Pardes, pane: *Pane, buf: []u8) []const u8 { pane.serial, pane_files.tagOf(p, pane).len, // A terminal's body is its history, read whole on an open's first read. - if (pane.isTerminal()) panes.Terminal.screenTextLen(pane) else pane_files.bodyOf(pane).len, + if (pane.isTerminal()) panes.terminal.screenTextLen(pane) else pane_files.bodyOf(pane).len, @as(u32, 0), @intFromBool(pane_files.dirtyOf(pane)), pane.cols, diff --git a/src/ninep/events.zig b/src/ninep/events.zig index c0bf4a9a..fe7af77e 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -1289,7 +1289,7 @@ test "a record is one line of UTF-8: DEL and C1 are spaces, bytes not UTF-8 are try testing.expectEqualStrings("msg - caf\xc3\xa9\n", sanitize("msg - caf\xc3\xa9\n", &out)); } -test "a long err record is cut between words, with an ellipsis" { +test "a long err record keeps its end, its middle given up to an ellipsis" { const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); _ = wr(p, @intFromEnum(tree.TopFile.ctl), "Bogus " ++ "abcdefgh " ** 40 ++ "\n"); @@ -1298,7 +1298,9 @@ test "a long err record is cut between words, with an ellipsis" { const text = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = h, .size = 1 << 16 }).bytes; const at = std.mem.lastIndexOf(u8, text, "err - ctl: ").?; const record = text[at .. std.mem.indexOfScalarPos(u8, text, at, '\n').? + 1]; - try testing.expect(std.mem.endsWith(u8, record, "abcdefgh…\n")); + try testing.expect(std.mem.indexOf(u8, record, "…") != null); + try testing.expect(std.mem.startsWith(u8, record, "err - ctl: ")); + try testing.expect(std.mem.endsWith(u8, record, "abcdefgh\"\n") or std.mem.endsWith(u8, record, "abcdefgh \"\n")); _ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h }); } diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index bb4ebf86..3fad1fae 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -258,7 +258,7 @@ pub fn fileSize(p: *Pardes, id: usize, f: PaneFile) u64 { const pf = &pane.fs; return switch (f) { // A terminal's body reads its screen and history, as text. - .body => if (pane.file == null and pane.isTerminal()) panes.Terminal.screenTextLen(pane) else if (pdfText(p, pane)) |t| t.len else bodyOf(pane).len, + .body => if (pane.file == null and pane.isTerminal()) panes.terminal.screenTextLen(pane) else if (pdfText(p, pane)) |t| t.len else bodyOf(pane).len, .data, .xdata => bodyOf(pane).len, .tag => tagOf(p, pane).len, .name => nameOf(pane).len + 1, @@ -399,7 +399,7 @@ fn readBody(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { return tree.stagedReply(p, req); } if (req.handle != 0 and pane.isTerminal()) return screen.readSnapshot(p, req, pane); - const text = panes.Terminal.screenTextAlloc(pane, p.gpa) catch + const text = panes.terminal.screenTextAlloc(pane, p.gpa) catch return Reply.fail(req.tag, E.NOMEM); defer p.gpa.free(text); const out = p.fs.stage(p.gpa); @@ -571,7 +571,7 @@ fn writeTag(req: Req, pane: *Pane) Reply { const had = tagline.curTail(pane).len; const room = limits.max_tag_tail -| had -| @intFromBool(pf.tag_held_newline); // Whole or not at all: a write that would pass the limit changes nothing. - if (req.data.len > room) return tree.failText(req.tag, E.NOSPC, std.fmt.comptimePrint("tag: over {d} bytes", .{limits.max_tag_tail})); + if (req.data.len > room) return tree.failText(req.tag, E.NOSPC, std.fmt.comptimePrint("tag: no space: over {d} bytes", .{limits.max_tag_tail})); const take = wholeUtf8(req.data); // A truncating write drops ONE trailing newline, its whole text's: a // newline held from a write before goes in once more text follows it. @@ -1214,7 +1214,9 @@ test "a tag write past the limit is refused whole, naming the limit" { const big = "w" ** (limits.max_tag_tail + 10); const r = wr(p, tag, big); try testing.expectEqual(E.NOSPC, r.errno()); - try testing.expect(std.mem.startsWith(u8, r.reply.ename, "tag: over ")); + // `no space` is what 9ns maps to ENOSPC, so a shell through a mount + // sees the errno the tree gives. + try testing.expect(std.mem.startsWith(u8, r.reply.ename, "tag: no space: over ")); try testing.expectEqualStrings(before, rd(p, tag, 0, 1 << 16).bytes); } diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig index 8ba9f4df..0f5202cc 100644 --- a/src/ninep/pty.zig +++ b/src/ninep/pty.zig @@ -139,9 +139,9 @@ pub fn readStatus(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { // or a line is typed at the prompt. Before the shell's first prompt a // run waits for it rather than answer busy, so that is not busy. // A build with no terminal panes (the board) has no marks to read. - const marks = if (comptime !pardes.panes.Terminal.enabled) null else if (pane.terminal) |state| &state.stream.handler else null; + const marks = if (comptime !pardes.panes.terminal.enabled) null else if (pane.terminal) |state| &state.stream.handler else null; const busy = p.hostTtyTaken(id) or (!pane.fs.unmarked and (waitingRun(p, pane) != null or - (if (marks) |m| m.prompts > 0 and (m.phase != .input or !pardes.panes.Terminal.promptInputEmpty(pane)) else false))); + (if (marks) |m| m.prompts > 0 and (m.phase != .input or !pardes.panes.terminal.promptInputEmpty(pane)) else false))); const out = p.fs.stage(p.gpa); const size = winsize(pane); out.print(p.gpa, "{d:>11} {d:>11} {d:>11}\n", .{ size[0], size[1], @intFromBool(busy) }) catch {}; @@ -222,7 +222,7 @@ pub fn writeRun(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { 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); - if (comptime !pardes.panes.Terminal.enabled) return tree.failText(req.tag, E.INVAL, e_bad_line); + if (comptime !pardes.panes.terminal.enabled) return tree.failText(req.tag, E.INVAL, e_bad_line); const pf = &pane.fs; const state = pane.terminal orelse return tree.failText(req.tag, E.INVAL, e_bad_line); const marks = &state.stream.handler; @@ -235,7 +235,7 @@ pub fn writeRun(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { } else if (pf.unmarked) { answer(p, slot, "error no prompt marks", .{}); } else if (waitingRun(p, pane) != null or (marks.prompts > 0 and (marks.phase != .input or - !pardes.panes.Terminal.promptInputEmpty(pane) or p.hostTtyTaken(id)))) + !pardes.panes.terminal.promptInputEmpty(pane) or p.hostTtyTaken(id)))) { // Something is running or someone has typed at the prompt: sending // now would type into the middle of it. The phase is pardes's own @@ -288,7 +288,7 @@ pub fn readRun(p: *Pardes, req: Req) Reply { /// for the prompt means whoever reads the answer can send the next command /// at once instead of finding the shell still drawing it. pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void { - if (comptime !pardes.panes.Terminal.enabled) return; + if (comptime !pardes.panes.terminal.enabled) return; const state = pane.terminal orelse return; const pf = &pane.fs; const slot = waitingRun(p, pane) orelse return; @@ -299,13 +299,13 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void { pf.run = null; return; } - if (marks.phase != .input or !pardes.panes.Terminal.promptInputEmpty(pane) or p.hostTtyTaken(id)) return; + if (marks.phase != .input or !pardes.panes.terminal.promptInputEmpty(pane) or p.hostTtyTaken(id)) return; slot.want = marks.started +% 1; slot.prompts = marks.prompts; slot.declined = marks.declined; p.emitWrite(id, slot.line); p.emitWrite(id, "\r"); - pardes.panes.Terminal.noteCommand(pane, slot.line); + pardes.panes.terminal.noteCommand(pane, slot.line); slot.phase = .sent; return; } @@ -319,7 +319,7 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void { // 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); + const empty = pardes.panes.terminal.promptInputEmpty(pane); switch (slot.cleared) { .no => { if (marks.continuation) { @@ -360,7 +360,7 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void { /// 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 + 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); // Keep the tail, cut at a line start so the first line kept is whole. @@ -400,7 +400,7 @@ 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); + pardes.panes.terminal.endOutputHere(pane); finish(p, slot, pane, code); }; var buf: [4]u8 = undefined; @@ -963,10 +963,10 @@ test "a run's answer says cut when its output's start is gone, and reads a bound // A resize between its start and end reflows the pins with the text. h = sh.send(p, node); sh.put(p, c ++ "one\r\ntwo\r\n"); - pardes.panes.Terminal.resizeGrid(pane, gpa, 50, 20); + pardes.panes.terminal.resizeGrid(pane, gpa, 50, 20); sh.put(p, d ++ prompt); try testing.expectEqualStrings("exit 0\none\ntwo\n", sh.answered(p, node, h, into)); - pardes.panes.Terminal.resizeGrid(pane, gpa, pane.cols, pane.rows); + pardes.panes.terminal.resizeGrid(pane, gpa, pane.cols, pane.rows); // A full-screen program leaves nothing on the primary screen, and a D // with no status is not a success. diff --git a/src/ninep/screen.zig b/src/ninep/screen.zig index f496da03..b991c3e4 100644 --- a/src/ninep/screen.zig +++ b/src/ninep/screen.zig @@ -47,7 +47,7 @@ pub fn readSnapshot(p: *Pardes, req: Req, pane: ?*Pane) Reply { const snapshot = &(tree.openOf(p, req) orelse return Reply.fail(req.tag, E.INVAL)).what.snapshot; if (snapshot.* == null) { const terminal = pane orelse return Reply.fail(req.tag, E.INVAL); - snapshot.* = panes.Terminal.screenTextAlloc(terminal, p.gpa) catch return Reply.fail(req.tag, E.NOMEM); + snapshot.* = panes.terminal.screenTextAlloc(terminal, p.gpa) catch return Reply.fail(req.tag, E.NOMEM); } const bytes = snapshot.*.?; const off = @min(req.off, bytes.len); @@ -129,7 +129,7 @@ test "terminal body handles keep one history snapshot across fragmented reads" { const node = Node.of(serialOf(p), .body); p.update(.{ .output = .{ .pane = 0, .bytes = "old caf\xc3\xa9\r\nold tail" } }); while (p.nextEffect()) |_| {} - const original = try panes.Terminal.screenTextAlloc(p.panes[0].?, gpa); + const original = try panes.terminal.screenTextAlloc(p.panes[0].?, gpa); defer gpa.free(original); const opened = call(p, .{ .tag = 1, .op = .open, .node = node }); try testing.expectEqual(Status.ok, opened.reply.status); diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 30bf073d..5f3ffc9c 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -456,9 +456,59 @@ pub fn stagedReply(p: *Pardes, req: Req) Reply { pub fn handle(p: *Pardes, req: Req) Reply { var reply = serve(p, req); if (reply.status == .err and reply.ename.len == 0) reply.ename = errWords(reply.errno); + if (reply.status == .err and reply.ename.len > cloud9.fs.errmax) + if (p.scratch.allocator().alloc(u8, cloud9.fs.errmax)) |room| { + reply.ename = fitErr(reply.ename, room); + } else |_| {}; return reply; } +/// A reason longer than an Rerror carries (Plan 9's ERRMAX, 128 bytes) +/// keeps its end, which says why: the longest path in it -- else the whole +/// text -- gives up its middle to `…`. +pub fn fitErr(text: []const u8, out: []u8) []const u8 { + const ell = "…"; + if (text.len <= out.len) return text; + const over = text.len - out.len + ell.len; + var from: usize = 0; + var to: usize = text.len; + var words = std.mem.tokenizeAny(u8, text, " \t"); + var longest: usize = 0; + while (words.next()) |w| if (std.mem.indexOfScalar(u8, w, '/') != null and w.len > longest and w.len > over + 2) { + longest = w.len; + from = @intFromPtr(w.ptr) - @intFromPtr(text.ptr); + to = from + w.len; + }; + var a = from + (to - from - over) / 2; + var b = a + over; + while (a > from and text[a] & 0xC0 == 0x80) a -= 1; + while (b < to and text[b] & 0xC0 == 0x80) b += 1; + return std.fmt.bufPrint(out, "{s}" ++ ell ++ "{s}", .{ text[0..a], text[b..] }) catch text[0..out.len]; +} + +test "a reason past 128 bytes keeps its end: the path in it gives up its middle" { + var out: [128]u8 = undefined; + const long = "Save /home/someone/projects/" ++ "deep/" ** 30 ++ "file.txt: no such directory"; + const fit = fitErr(long, &out); + try testing.expect(fit.len <= 128); + try testing.expect(std.mem.startsWith(u8, fit, "Save /home/someone/")); + try testing.expect(std.mem.endsWith(u8, fit, "file.txt: no such directory")); + try testing.expect(std.mem.indexOf(u8, fit, "…") != null); + try testing.expect(std.unicode.utf8ValidateSlice(fit)); + // No path: the whole text's middle goes, the end stays. + const words = "why " ** 40 ++ "the reason"; + try testing.expect(std.mem.endsWith(u8, fitErr(words, &out), "the reason")); + try testing.expectEqualStrings("short", fitErr("short", &out)); + // Through the tree: a look at a long ./ name not there says why at the end. + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + var line: [512]u8 = undefined; + const r = th.wr(p, Node.of(serialOf(p), .look), try std.fmt.bufPrint(&line, "./{s}x.txt\n", .{"no-such-dir/" ** 20})); + try testing.expectEqual(Status.err, r.reply.status); + try testing.expect(r.reply.ename.len <= 128); + try testing.expect(std.mem.endsWith(u8, r.reply.ename, "no such file") or std.mem.endsWith(u8, r.reply.ename, "no such directory")); +} + /// A refusal with no reason of its own said in Plan 9's words, not the C /// library's (`Operation not permitted`); each maps back to its errno in /// 9ns (enameToErrno), EPERM's to EACCES as Plan 9's does. @@ -1578,6 +1628,55 @@ test "editor paths resolve to the same nodes the wire serves" { try testing.expectEqual(before, p.next_serial); } +test "random placements, refused or not, leave no pane under its tag and two rows" { + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.writeFile(testing.io, .{ .sub_path = "f.txt", .data = "one\ntwo\n" }); + var dir_buf: [4096]u8 = undefined; + const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)]; + var look_line: [4200]u8 = undefined; + const looked = try std.fmt.bufPrint(&look_line, "{s}/f.txt\n", .{dir}); + var seed: u64 = 0; + while (seed < 300) : (seed += 1) { + var prng = std.Random.DefaultPrng.init(seed); + const r = prng.random(); + const rows = r.intRangeAtMost(u16, 6, 40); + const p = try pardes.Pardes.init(testing.allocator, .{ .tty_only = true, .cols = r.intRangeAtMost(u16, 60, 240), .rows = rows }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + _ = try p.setTestFile("x\n"); + while (p.nextEffect()) |_| {} + for (0..r.uintLessThan(usize, 3)) |_| _ = th.wr(p, Node.of(serialOf(p), .exec), "Newcol\n"); + if (r.boolean()) p.settings.placement = .pardes; + for (0..40) |step| { + var serials: [pardes.MAX_PANES]u32 = undefined; + var n: usize = 0; + for (p.panes) |slot| if (slot) |pn| { + serials[n] = pn.serial; + n += 1; + }; + if (n == 0) break; + const at = serials[r.uintLessThan(usize, n)]; + const action = r.uintLessThan(u8, 8); + switch (action) { + 0 => _ = th.wr(p, Node.of(at, .exec), "New\n"), + 1 => _ = call(p, .{ .tag = 1, .op = .open, .node = @intFromEnum(TopFile.new) }), + 2 => _ = th.wr(p, Node.of(at, .exec), "Newcol\n"), + 3 => _ = th.wr(p, Node.of(at, .exec), "Edit =\n"), + 4 => _ = th.wr(p, Node.of(at, .exec), "Tty\n"), + 5 => _ = th.wr(p, Node.of(at, .look), looked), + 6 => _ = th.wr(p, Node.of(at, .exec), "Delcol\n"), + else => _ = th.wr(p, Node.of(at, .ctl), "delete\n"), + } + for (0..p.ncol) |c| if (!layout.columnAtMinimums(p, c)) { + std.debug.print("seed {d} rows {d} step {d} action {d}: column {d} under its minimums\n", .{ seed, rows, step, action, c }); + for (p.col_panes[c][0..p.col_n[c]]) |id| std.debug.print(" pane {d} h {d} min {d}\n", .{ id, p.rects[id].h, layout.minRows(p, id) }); + return error.TestUnexpectedResult; + }; + } + } +} + test "at the pane cap, pane/new, look and New each say so, and look reads back empty" { const p = try th.withFile(testing.allocator, "x\n"); defer p.deinit(); |
