diff options
Diffstat (limited to 'src/ninep/pty.zig')
| -rw-r--r-- | src/ninep/pty.zig | 101 |
1 files changed, 92 insertions, 9 deletions
diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig index 3599b612..393516ce 100644 --- a/src/ninep/pty.zig +++ b/src/ninep/pty.zig @@ -60,6 +60,7 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool { if (words.next() != null) return false; if (!apply) return true; p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = cols, .rows = rows } }); + pane.fs.winsize = .{ cols, rows }; }, .sig => { const which = signalNamed(words.next() orelse return false) orelse return false; @@ -79,13 +80,20 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool { /// ctl reads back its state in the words it takes, as a Plan 9 ctl does. pub fn readCtl(p: *Pardes, req: Req, pane: *Pane) Reply { - p.fs.stage(p.gpa).print(p.gpa, "winsize {d} {d}\n", .{ pane.cols, pane.rows }) catch + const size = winsize(pane); + p.fs.stage(p.gpa).print(p.gpa, "winsize {d} {d}\n", .{ size[0], size[1] }) catch return Reply.fail(req.tag, E.NOMEM); return tree.stagedReply(p, req); } -/// Three right-aligned fields: cols, rows and busy: whether a run written now -/// would be told `busy` (a command runs, or text is typed at the prompt). +/// The pty's size: what ctl's winsize set, else the pane's grid. +fn winsize(pane: *const Pane) [2]u16 { + return pane.fs.winsize orelse .{ pane.cols, pane.rows }; +} + +/// Three right-aligned fields: the pty's cols and rows, and busy: whether a +/// run written now would be told `busy` (a command runs, or text is typed at +/// the prompt). A newline ends the line, as a Plan 9 status file's does. pub const status_len: u64 = 36; pub fn readStatus(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { @@ -97,7 +105,8 @@ pub fn readStatus(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { 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))); const out = p.fs.stage(p.gpa); - out.print(p.gpa, "{d:>11} {d:>11} {d:>11} ", .{ pane.cols, pane.rows, @intFromBool(busy) }) catch {}; + const size = winsize(pane); + out.print(p.gpa, "{d:>11} {d:>11} {d:>11}\n", .{ size[0], size[1], @intFromBool(busy) }) catch {}; return tree.stagedReply(p, req); } @@ -287,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); @@ -305,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. @@ -317,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. @@ -466,6 +497,12 @@ test "every pty/ctl verb, and every refusal" { try testing.expectEqual(@as(u16, 44), ws.winsize.?.rows); try testing.expectEqual(cols, pane.cols); try testing.expectEqual(rows, pane.rows); + // ctl and status read back the pty's size, until the pane resizes. + try testing.expectEqualStrings("winsize 132 44\n", rd(p, ctl, 0, 64).bytes); + try testing.expect(std.mem.startsWith(u8, rd(p, Node.of(serialOf(p), .pty_status), 0, 64).bytes, " 132 44 ")); + p.update(.{ .resize = .{ .cols = 76, .rows = 24 } }); + try testing.expect(pane.cols != 132); + try testing.expectEqualStrings(try std.fmt.bufPrint(&said, "winsize {d} {d}\n", .{ pane.cols, pane.rows }), rd(p, ctl, 0, 64).bytes); for ([_]struct { line: []const u8, want: pardes.PtySignal }{ .{ .line = "sig INT", .want = .int }, @@ -527,6 +564,8 @@ test "owned cwd pty exec rejects long paths before applying its batch" { const p = try withTerm(testing.allocator); defer p.deinit(); const pane = p.panes[0].?; + // A one-row tag: the long path would wrap it onto rows and resize the pty. + pane.tag_expanded = false; const ctl = Node.of(pane.serial, .pty_ctl); var path: [1025]u8 = @splat('d'); path[0] = '/'; @@ -554,7 +593,7 @@ test "pty/status reports the grid and who holds the tty" { const a = rd(p, status, 0, 256); try testing.expectEqual(Status.ok, a.reply.status); var want: [64]u8 = undefined; - const whole = try std.fmt.bufPrint(&want, "{d:>11} {d:>11} {d:>11} ", .{ pane.cols, pane.rows, 0 }); + const whole = try std.fmt.bufPrint(&want, "{d:>11} {d:>11} {d:>11}\n", .{ pane.cols, pane.rows, 0 }); try testing.expectEqualStrings(whole, a.bytes); try testing.expectEqual(@as(usize, 3 * 12), a.bytes.len); try testing.expectEqualStrings(whole[12..], rd(p, status, 12, 256).bytes); @@ -563,7 +602,7 @@ test "pty/status reports the grid and who holds the tty" { p.host = .{ .ctx = &probe, .vtable = &th.FakeTty.vtable }; const held = rd(p, status, 0, 256); try testing.expectEqualStrings( - try std.fmt.bufPrint(&want, "{d:>11} {d:>11} {d:>11} ", .{ pane.cols, pane.rows, 1 }), + try std.fmt.bufPrint(&want, "{d:>11} {d:>11} {d:>11}\n", .{ pane.cols, pane.rows, 1 }), held.bytes, ); } @@ -645,12 +684,12 @@ test "run answers how its command ended once the shell is back at a prompt" { // Before its first prompt the shell cannot take a line yet: the line // waits for it, and pty/status does not call the shell busy meanwhile. const status = Node.of(serialOf(p), .pty_status); - try testing.expect(std.mem.endsWith(u8, rd(p, status, 0, 64).bytes, " 0 ")); + try testing.expect(std.mem.endsWith(u8, rd(p, status, 0, 64).bytes, " 0\n")); const early = call(p, .{ .tag = 1, .op = .open, .node = node }); const waiting = call(p, .{ .tag = 2, .op = .write, .node = node, .handle = early.reply.handle, .data = "true\n" }); try testing.expectEqualStrings("", waiting.pty()); try testing.expectEqual(Status.again, call(p, .{ .tag = 3, .op = .read, .node = node, .handle = early.reply.handle, .size = 64 }).reply.status); - try testing.expect(std.mem.endsWith(u8, rd(p, status, 0, 64).bytes, " 1 ")); + try testing.expect(std.mem.endsWith(u8, rd(p, status, 0, 64).bytes, " 1\n")); // A respawn before that prompt keeps it waiting for the new shell's. p.acknowledgeShell(0, "/bin/sh", true); try testing.expectEqual(Status.again, call(p, .{ .tag = 3, .op = .read, .node = node, .handle = early.reply.handle, .size = 64 }).reply.status); @@ -746,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" { |
