summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 15:14:00 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit864407b240b812743d6f57a56daf9706b36fc027 (patch)
treef2934c7c82602b456cdbacbe446b7e58b4c2d96e
parent1d0f8cd158bb47941c0187d32422787c92e8feeb (diff)
downloadpardes-864407b240b812743d6f57a56daf9706b36fc027.tar.gz
pardes-864407b240b812743d6f57a56daf9706b36fc027.zip
pardes --wait for a new name waits on the pane it made, by serial, and a pane closed before it looks is done, not a failure
fs.py's `pardes --wait 'wait new.txt'` failed two runs in three on the 0.0.8 merge. The stress case repeats the test's steps: 4 of 50 failed under load, each with "pardes: --wait: no pane shows that file". A new name's launch makes its pane, names it, then found it again by path in /index. The script had already seen the name, saved and removed the pane by then, so the lookup missed and --wait exited 1. Now the launch hands --wait the serial pane/new gave it. After a look, finding no pane on the file means that pane is already closed, which is the end --wait waits for: exit 0. The follow with its /index recheck covers a close in between, as before. Under the same load the stress case went 0 of 100. The same load showed a test race too, at fs.py's second Restore. The exec write is answered before the hang-up, but the client's clunk after it can meet the connection already cut. The test now requires the answer and tolerates the reset. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--src/main.zig21
-rw-r--r--test/fs.py13
2 files changed, 25 insertions, 9 deletions
diff --git a/src/main.zig b/src/main.zig
index 576efc8e..859e6fb3 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -102,13 +102,18 @@ const help_text =
/// pane `path` landed in (the one it already had, if open), then /log
/// followed on one connection until that pane's `del` -- exit 0 -- or the
/// connection ends with the session -- exit 1.
-fn waitForDel(io: std.Io, gpa: std.mem.Allocator, dial: []const u8, path: []const u8) noreturn {
- const first = ninep_io.Client.read(gpa, dial, "/index", "/index") catch std.process.exit(1);
- const serial = paneShowing(first, path) orelse {
- std.Io.File.stderr().writeStreamingAll(io, "pardes: --wait: no pane shows that file\n") catch {};
- std.process.exit(1);
+fn waitForDel(io: std.Io, gpa: std.mem.Allocator, dial: []const u8, path: []const u8, known: ?u32) noreturn {
+ _ = io;
+ // The pane this launch made (a name not there yet) is known by its
+ // serial; one a look opened or went to is found by its file. Found by
+ // none after the look went there, it was closed already: that is the
+ // end being waited for, not a failure (a pane closed between the look
+ // and this read made `--wait` exit 1 before).
+ const serial = known orelse found: {
+ const first = ninep_io.Client.read(gpa, dial, "/index", "/index") catch std.process.exit(1);
+ defer gpa.free(first);
+ break :found paneShowing(first, path) orelse std.process.exit(0);
};
- gpa.free(first);
const Wait = struct {
gpa: std.mem.Allocator,
dial: []const u8,
@@ -436,6 +441,7 @@ fn nativeMain(init: std.process.Init) !void {
};
break :named std.fmt.bufPrint(&newbuf, "{s}/{s}", .{ std.mem.trimEnd(u8, dir.path, "/"), base }) catch |err| Refuse.with(init.io, word, err);
};
+ var made_serial: ?u32 = null;
if (found_path != null) {
var command_buf: [8192]u8 = undefined;
const command = std.fmt.bufPrint(&command_buf, "{s}{s}\n", .{ path, word[target.path.len..] }) catch |err| Refuse.with(init.io, word, err);
@@ -443,6 +449,7 @@ fn nativeMain(init: std.process.Init) !void {
} else {
const made = ninep_io.Client.read(arena, parent.dial, "/pane/new", "/pane/new") catch |err| Refuse.with(init.io, word, err);
const serial = std.fmt.parseInt(u32, std.mem.trim(u8, made, " \n"), 10) catch |err| Refuse.with(init.io, word, err);
+ made_serial = serial;
var name_buf: [64]u8 = undefined;
const name = try std.fmt.bufPrint(&name_buf, "/pane/{d}/name", .{serial});
var line_buf: [4200]u8 = undefined;
@@ -466,7 +473,7 @@ fn nativeMain(init: std.process.Init) !void {
Refuse.with(init.io, word, err);
};
}
- if (wait) waitForDel(init.io, arena, parent.dial, path);
+ if (wait) waitForDel(init.io, arena, parent.dial, path, made_serial);
return;
}
if (positional) |a| {
diff --git a/test/fs.py b/test/fs.py
index b02b9e45..3f11269a 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -1117,7 +1117,15 @@ def test(binary, quic=False):
except OSError as refused:
assert 'Modified' in str(refused), refused
assert old.read('/pane/1/body') == b'changed after dump\n'
- execute(old, control, 'Restore ' + str(saved))
+ # Answered before the hang-up: the write's reply comes; the
+ # clunk after it may meet the connection already cut.
+ restore = old.open(f'/pane/{control}/exec', 1)
+ line = ('Restore ' + str(saved) + '\n').encode()
+ old.rpc(118, struct.pack('<IQI', restore, 0, len(line)) + line)
+ try:
+ old.close(restore)
+ except (EOFError, OSError):
+ pass
# Refused while the connections are cut, or cut already.
try:
old.read_fid(held_body)
@@ -1315,7 +1323,8 @@ def test(binary, quic=False):
client.write(f'/pane/{named}/body', b'new\n')
execute(client, named, 'Save')
client.remove(f'/pane/{named}')
- assert made.wait(timeout=5) == 0
+ code = made.wait(timeout=5)
+ assert code == 0, (code, made.stderr.read())
assert fresh.read_bytes() == b'new\n'
# ...in a directory not there either: forwarded all the same, and
# Save makes the directories.