summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.zig.zon4
-rw-r--r--src/fs.zig8
-rw-r--r--src/ninep/events.zig12
-rw-r--r--src/ninep/tree.zig12
-rw-r--r--test/selfmount.py10
5 files changed, 30 insertions, 16 deletions
diff --git a/build.zig.zon b/build.zig.zon
index 4df012bc..106ef085 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -9,8 +9,8 @@
// read-only HTTPS URL is what a manifest can carry. Re-pin with
// `zig fetch --save=cloud9 git+https://git.sr.ht/~gbrls/cloud9#<commit>`.
.cloud9 = .{
- .url = "git+https://git.sr.ht/~gbrls/cloud9#705be665742fa0332585a4321640b5e2245610cb",
- .hash = "cloud9-0.1.0-yt86qt8NHgDuS2WNp0TgW03h-eVIgWmkznn0t0nMQKbr",
+ .url = "git+https://git.sr.ht/~gbrls/cloud9#a03d1649108510cda5951e918e561d7cae43d6b0",
+ .hash = "cloud9-0.1.0-yt86qiISHgBgvO5D4UOz0pZnWrLmL337v38Z8ROAeLG0",
},
// Pinned to the exact libvaxis commit ghostty is built against, so it
// is compatible with ghostty's uucode configuration (the one we share
diff --git a/src/fs.zig b/src/fs.zig
index d13e36bd..91edd429 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -1360,7 +1360,13 @@ fn readFileLimit(gpa: std.mem.Allocator, path: []const u8, limit: usize) ![]u8 {
defer _ = libc.close(fd);
const end = libc.lseek(fd, 0, libc.SEEK.END);
- if (end < 0 and libc.errno(end) == .SPIPE) return error.NotAFile;
+ // Not seekable: a pipe or a FIFO is no file; a regular file that is a
+ // stream (a 9ns mount's log or screen, opened nonseekable) is read to
+ // its end, as one with no length is.
+ if (end < 0 and libc.errno(end) == .SPIPE) {
+ const facts = ninep_io.statNoFollow(path_z) orelse return error.NotAFile;
+ if (facts.mode & 0o170000 != 0o100000) return error.NotAFile;
+ }
const size: usize = if (end < 0) 0 else @intCast(end);
if (end >= 0 and libc.lseek(fd, 0, libc.SEEK.SET) < 0) return error.ReadFailed;
if (size == 0) {
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index bacf3ad6..9752e287 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -494,18 +494,6 @@ pub fn writeLog(p: *Pardes, req: Req) Reply {
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
}
-/// The length a stat reports: the text an open would freeze now.
-pub fn logLen(q: *const Queue) u64 {
- var n: u64 = 0;
- var at = q.head;
- while (at + 4 <= q.buf.items.len) {
- const len = std.mem.readInt(u32, q.buf.items[at..][0..4], .little);
- n += len;
- at += 4 + len;
- }
- return n;
-}
-
// ---- per-pane event records ----
pub const max_record_text = 256;
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index be03e8f0..8c536c51 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -646,7 +646,9 @@ fn topSize(p: *Pardes, f: TopFile) u64 {
.README => fs.help.len,
.status => ctl.statusLen(p),
.look, .exec, .tagexec => ctl.resultsLen(p),
- .log => events.logLen(&p.fs.log),
+ // A stream: a length would read as its end to a kernel that caches
+ // one (a 9ns mount's splice), and a follower's reads go past it.
+ .log => 0,
.ctl => ctl.rootLen(p),
.commands => ctl.commandsLen(),
.focus => focus: {
@@ -1335,6 +1337,14 @@ test "filesystem inspection preserves pending and displayed Look hover" {
try testing.expect(!p.raw_hover_intent);
}
+test "log stats 0 however much it holds, a stream as acme's generated files are" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ _ = th.wr(p, Node.of(th.serialOf(p), .exec), "Msg a record for the log\n");
+ try testing.expect(th.logHas(p, "a record for the log"));
+ try testing.expectEqual(@as(u64, 0), call(p, .{ .tag = 1, .op = .getattr, .node = @intFromEnum(TopFile.log) }).reply.attr.size);
+}
+
test "filesystem pane creation and truncation cancel Look hover" {
const requests = [_]Req{
// Opening `new` is what makes a pane now; walking to it makes none,
diff --git a/test/selfmount.py b/test/selfmount.py
index 86c32fa8..dd59083c 100644
--- a/test/selfmount.py
+++ b/test/selfmount.py
@@ -176,6 +176,16 @@ def run(binary):
held = run(f"for i in $(seq 40); do cat {mount}/pane/{first}/event > /dev/null & done; "
f"sleep 1; timeout 10 cat {mount}/layout > /dev/null; st=$?; kill $(jobs -p); exit $st\n".encode())
check(held == '0', f'cat layout answers beside 40 held event reads ({held!r})')
+ # fs.md's follow recipe, as written, with plain cat, its output
+ # a pipe (where coreutils cat splices): it hears a record made
+ # after it started.
+ recipe = Path(f'/tmp/{name}.follow')
+ ran = run(f"exec 3<>{mount}/log; echo follow >&3; "
+ f"(sleep 1; echo 'Msg recipe-heard' > {mount}/exec) & "
+ f"timeout 5 cat <&3 | cat > {recipe}; exit 0\n".encode())
+ followed = recipe.read_text() if recipe.exists() else ''
+ recipe.unlink(missing_ok=True)
+ check(ran == '0' and 'recipe-heard' in followed, f'the log follow recipe with cat hears a new record ({followed[-120:]!r})')
client.write('/exec', b'Msg still here\n')
text = ''.join(cell[0] for cell in client.screen()['cells'])
check('still here' in text, 'the editor answers after all of that')