summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/9p_io.zig3
-rw-r--r--src/fs.zig49
-rw-r--r--src/pardes.zig42
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;
diff --git a/src/fs.zig b/src/fs.zig
index 7cb08980..5c75eb72 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -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 });