summaryrefslogtreecommitdiff
path: root/src/ninep/pty.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep/pty.zig')
-rw-r--r--src/ninep/pty.zig49
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);