diff options
Diffstat (limited to 'src/ninep/pty.zig')
| -rw-r--r-- | src/ninep/pty.zig | 49 |
1 files changed, 39 insertions, 10 deletions
diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig index 393516ce..4009af49 100644 --- a/src/ninep/pty.zig +++ b/src/ninep/pty.zig @@ -25,7 +25,25 @@ fn dimension(word: []const u8) ?u16 { if (word.len == 0 or word.len > 5) return null; for (word) |c| if (c < '0' or c > '9') return null; const n = std.fmt.parseInt(u16, word, 10) catch return null; - return if (n == 0) null else n; + return if (n == 0 or n > winsize_max) null else n; +} + +/// The fewest rows a winsize write gives a terminal's pty. +pub const pty_rows_min = 2; + +/// As the root ctl's `size` (4096 a side). +const winsize_max = 4096; +const e_winsize_range = std.fmt.comptimePrint("invalid winsize: out of range, 1x1 to {d}x{d}", .{ winsize_max, winsize_max }); + +/// A winsize whose numbers are numbers but not sizes: said as such. +fn outOfRange(line: []const u8) bool { + var words = std.mem.tokenizeAny(u8, line, " \t"); + if (!std.mem.eql(u8, words.next() orelse return false, "winsize")) return false; + var n: u8 = 0; + while (words.next()) |w| : (n += 1) { + _ = std.fmt.parseInt(u32, w, 10) catch return false; + } + return n == 2; } fn signalNamed(word: []const u8) ?pardes.PtySignal { @@ -43,7 +61,7 @@ pub fn writeCtl(p: *Pardes, req: Req, id: usize) Reply { while (it.next()) |raw| { const line = std.mem.trim(u8, raw, " \t\r"); if (line.len == 0) continue; - if (!verb(p, id, line, apply)) return tree.failText(req.tag, E.INVAL, e_bad_pty_ctl); + if (!verb(p, id, line, apply)) return tree.failText(req.tag, E.INVAL, if (outOfRange(line)) e_winsize_range else e_bad_pty_ctl); } } return .{ .tag = req.tag, .written = @intCast(req.data.len) }; @@ -56,7 +74,9 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool { switch (v) { .winsize => { const cols = dimension(words.next() orelse return false) orelse return false; - const rows = dimension(words.next() orelse return false) orelse return false; + // Two rows at least, as a pane is laid out: a one-row pty + // loses its prompt's input mark and reads busy for ever. + const rows = @max(pty_rows_min, dimension(words.next() orelse return false) orelse return false); if (words.next() != null) return false; if (!apply) return true; p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = cols, .rows = rows } }); @@ -72,7 +92,7 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool { if (words.next() != null) return false; if (pane.cwdSlice().len > pardes.effect_path_cap) return false; if (!apply) return true; - p.emit(.{ .spawn = .{ .pane = @intCast(id), .serial = pane.serial, .cwd = .from(pane.cwdSlice()) } }); + p.emitSpawn(id, pane.serial, pane.cwdSlice()); }, } return true; @@ -189,7 +209,12 @@ pub fn writeRun(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { // 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(p, slot, "busy", .{}); + // Naming what runs, where the host can tell (Linux). + var name: [64]u8 = undefined; + if (p.hostForegroundName(id, &name)) |program| + answer(p, slot, "busy: {s} is running", .{program}) + else + answer(p, slot, "busy", .{}); } else { // Sent now if the shell is at its prompt, or at its first prompt // when it has not drawn one yet (noteMarks). @@ -433,7 +458,7 @@ test "a pane that is not a terminal has no pty/ at all" { var buf: [32]th.Dirent = undefined; const files = th.dirents(rdir(p, dir, 0).bytes, &buf); - try testing.expectEqual(@as(usize, 17), files.len); + try testing.expectEqual(@as(usize, 18), files.len); try testing.expect(th.nameAt(files, "pty") == null); try testing.expectEqual(E.NOENT, look_up(p, dir, "pty_ctl").errno()); @@ -454,7 +479,7 @@ test "a terminal pane's pty/ holds exactly ctl, status and data" { var buf: [32]th.Dirent = undefined; const files = th.dirents(rdir(p, dir, 0).bytes, &buf); - try testing.expectEqual(@as(usize, 18), files.len); + try testing.expectEqual(@as(usize, 19), files.len); try testing.expect(th.nameAt(files, "pty").?.dir); const inside = th.dirents(rdir(p, Node.of(serial, .pty), 0).bytes, &buf); @@ -528,10 +553,7 @@ test "every pty/ctl verb, and every refusal" { "winsize", // no arguments "winsize 80", // one argument "winsize 80 24 extra", // three - "winsize 0 24", // zero is "unknown", never a width - "winsize 80 0", "winsize -1 24", // not a decimal - "winsize 999999 24", // wider than a u16 "sig", // no name "sig INT TERM", // two "sig SIGINT", @@ -553,6 +575,13 @@ test "every pty/ctl verb, and every refusal" { try testing.expect(a.signal == null); try testing.expect(!a.spawned); } + // Numbers, but no size: said as out of range, the bound size's. + for ([_][]const u8{ "winsize 0 24", "winsize 80 0", "winsize 0 0", "winsize 4097 24", "winsize 999999 24" }) |bad| { + const a = wr(p, ctl, bad); + try testing.expectEqual(E.INVAL, a.errno()); + try testing.expectEqualStrings(e_winsize_range, a.reply.ename); + try testing.expect(a.winsize == null); + } const spaced = wr(p, ctl, "\n winsize 90 20 \n\n"); try testing.expectEqual(Status.ok, spaced.reply.status); |
