diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/9p_io.zig | 3 | ||||
| -rw-r--r-- | src/fs.zig | 49 | ||||
| -rw-r--r-- | src/pardes.zig | 42 |
3 files changed, 90 insertions, 4 deletions
diff --git a/src/9p_io.zig b/src/9p_io.zig index 7f0134ed..08deae7a 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -1358,6 +1358,9 @@ pub fn start(io: std.Io, gpa: std.mem.Allocator, core: *pardes.Pardes) ?*Listene return null; }; core.fs.socket_path = listener.path(); + // So the editor can recognise its own tree by name and refuse to walk into + // it through the mount, which would deadlock the loop that serves it. + pardes.filesystem.noteOwnSocket(listener.path()); core.fs.tcp_address = listener.tcp_address; core.fs.quic_address = listener.quic_address; return listener; @@ -681,8 +681,56 @@ pub fn resolve(p: ?*pardes.Pardes, word: []const u8, cwd: []const u8, out: *[409 return null; } +/// The name this session is posted under, taken from its socket path. +var own_name_buf: [64]u8 = undefined; +var own_name_len: usize = 0; + +pub fn noteOwnSocket(socket_path: []const u8) void { + own_name_len = 0; + const base = std.fs.path.basename(socket_path); + const head = "pardes-9p-"; + const tail = ".sock"; + if (!std.mem.startsWith(u8, base, head) or !std.mem.endsWith(u8, base, tail)) return; + const name = base[head.len .. base.len - tail.len]; + if (name.len == 0 or name.len > own_name_buf.len) return; + @memcpy(own_name_buf[0..name.len], name); + own_name_len = name.len; +} + +/// Is this path inside this editor's OWN 9P tree, as a mount presents it? +/// +/// Touching one from inside the editor deadlocks the session outright: the +/// realpath, the stat and the read all leave through the mount and come back +/// as 9P requests that only the editor's loop can answer, while that loop is +/// blocked making them. The filesystem then stops answering anybody. So the +/// answer has to come from the NAME, before any syscall -- the syscall is the +/// thing that never returns. Look at `/n/self/...` instead, which the editor +/// serves from memory without leaving the process. +pub fn isOwnMount(path: []const u8) bool { + if (own_name_len == 0) return false; + var buf: [own_name_buf.len + 16]u8 = undefined; + const needle = std.fmt.bufPrint(&buf, "/pardes/{s}/", .{own_name_buf[0..own_name_len]}) catch return false; + return std.mem.indexOf(u8, path, needle) != null; +} + +test "a path inside this session's own mount is refused before any syscall" { + noteOwnSocket("/run/user/1000/pardes-9p-demo.sock"); + defer own_name_len = 0; + try std.testing.expect(isOwnMount("/mnt/9p/pardes/demo/index")); + try std.testing.expect(isOwnMount("/mnt/9p/pardes/demo/pane/new")); + try std.testing.expect(!isOwnMount("/mnt/9p/pardes/other/index")); + try std.testing.expect(!isOwnMount("/home/goblin/src/pardes/demo.zig")); + var out: [4096]u8 = undefined; + try std.testing.expect(resolveOs("/mnt/9p/pardes/demo/index", &out) == null); + + // A session with no listener has no name, so nothing is refused. + noteOwnSocket("/tmp/not-a-pardes-socket"); + try std.testing.expect(!isOwnMount("/mnt/9p/pardes/demo/index")); +} + pub fn resolveOs(path: []const u8, out: *[4096]u8) ?Resolved { if (comptime !platform_has_fs) return null; + if (isOwnMount(path)) return null; var z: [4096]u8 = undefined; const path_z = std.fmt.bufPrintSentinel(&z, "{s}", .{path}, 0) catch return null; const resolved = realpath(path_z, out) orelse return null; @@ -706,6 +754,7 @@ pub fn read(p: *pardes.Pardes, path: []const u8) ![]u8 { } pub fn readLimit(p: *pardes.Pardes, path: []const u8, max_bytes: usize) ![]u8 { + if (isOwnMount(path)) return error.OwnMount; const limit = @min(max_bytes, limits.max_file_bytes); if (std.mem.eql(u8, std.mem.trimEnd(u8, path, "/"), "/n")) { var out: std.Io.Writer.Allocating = .init(p.gpa); diff --git a/src/pardes.zig b/src/pardes.zig index 5c6d2422..ec743bc6 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -13387,7 +13387,7 @@ pub const Pardes = struct { s.previous_tag_layers = &.{}; s.cell_diffs = &.{}; const chrome = p.chromeTheme(); - const th = p.theme(); // the message row paints in the editor's colours + const th = p.theme(); // Separators have their own role; a scrollbar need not frame the page. s.fill(0, 0, s.cols, s.rows, .{ .bg = .{ .rgb = chrome.border } }); @@ -13407,9 +13407,14 @@ pub const Pardes = struct { const r = p.rects[id]; const tx = r.x + config.GUTTER; const tw = r.w - config.GUTTER; + // Painted the way the sticky context headers above them are: the + // tagline font and the tagline's own colours, so a notice reads as + // a band belonging to the pane rather than as a line of body text + // that happens to sit at the bottom of it. const msg_style: CellStyle = .{ - .fg = if (th.fg) |c| .{ .rgb = c } else .default, - .bg = if (th.bg) |c| .{ .rgb = c } else .default, + .fg = .{ .rgb = chrome.tag_fg }, + .bg = .{ .rgb = chrome.tag_bg }, + .font_role = .tagline, }; // The stack sits directly above the tagline, one line to a row, in // the order collectNotices chose. A single notice therefore lands @@ -13417,7 +13422,7 @@ pub const Pardes = struct { const bottom = if (p.settings.tag_bottom) r.y + r.h - 1 - BOX_H else r.y + r.h - 1; for (pane.notices.slice(), 0..) |kind, i| { const row = bottom - @as(u16, @intCast(pane.notices.len - 1 - i)); - s.fill(tx, row, tw, 1, .{ .bg = msg_style.bg }); + s.fill(tx, row, tw, 1, .{ .bg = msg_style.bg, .font_role = .tagline }); switch (kind) { .message => _ = s.print(tx, row, tw, pane.msg[0..pane.msg_len], msg_style), .leader => { @@ -14634,6 +14639,35 @@ test "Esc back into a tty leaves its view at the prompt" { try std.testing.expectEqual(live, sp.terminal.?.vt.screens.active.pages.scrollbar().offset); } +test "a builtin announces itself on the message row, and Verbose silences it" { + if (platform == .web) return; + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 }); + defer p.deinit(); + const pane = p.panes[p.active].?; + + p.sync(); + try std.testing.expect(p.settings.verbose); + // Wrap, not Collapse: a builtin that folds its own pane away leaves no row + // for its announcement, which is correct and not what this is testing. + try std.testing.expect(p.executeBuiltinLine(p.active, "Wrap")); + try std.testing.expectEqualStrings("Wrap", pane.msg[0..pane.msg_len]); + + // The notice list is what the paint pass reads, so the announcement has to + // reach it, not just the buffer. + p.collectNotices(pane, p.rects[p.active], p.active); + try std.testing.expect(pane.notices.len > 0); + try std.testing.expectEqual(Pane.Notices.Kind.message, pane.notices.kinds[0]); + + // Msg owns the row itself, so it does not announce over its own text. + try std.testing.expect(p.executeBuiltinLine(p.active, "Msg hello")); + try std.testing.expectEqualStrings("hello", pane.msg[0..pane.msg_len]); + + pane.msg_len = 0; + p.settings.verbose = false; + try std.testing.expect(p.executeBuiltinLine(p.active, "Wrap")); + try std.testing.expectEqual(@as(u16, 0), pane.msg_len); +} + test "closing the last pane in a column leaves an empty one in its place" { if (platform == .web) return; const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 24 }); |
