diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 10:58:58 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 6add2ee79879712b39f6f76165a24a641127dd3b (patch) | |
| tree | fe8ea592e8f6e6642a81cbfbe70b3b2527b5f3f0 | |
| parent | 2a71bff5f06ba3e785a5eb96a8377e2cab8e2cdd (diff) | |
| download | pardes-6add2ee79879712b39f6f76165a24a641127dd3b.tar.gz pardes-6add2ee79879712b39f6f76165a24a641127dd3b.zip | |
Kill written to /ctl is answered before the editor quits
The write got EPIPE: Kill set quit, the request waited for the editor to
settle its effects, and the editor quit and cut the connections first.
A write that quits the editor is now answered at once, and the listener
lets pending answers out (up to 200 ms) before it stops the runner.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | src/9p_io.zig | 19 | ||||
| -rw-r--r-- | test/fs.py | 20 |
2 files changed, 39 insertions, 0 deletions
diff --git a/src/9p_io.zig b/src/9p_io.zig index 02b9efb5..d6eef9d4 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -262,6 +262,10 @@ pub const Listener = struct { // buffer for it, which a request run while this one waited would // write over. if (reply.status == .err) return conn.reply(&reply, ""); + // A write that quits the editor (Kill) is answered now: the editor + // is on its way out and will settle nothing this could wait for, + // and `deinit` lets the answer out before it cuts the connections. + if (core.quit) return conn.reply(&reply, ""); const restoring = core.restore_req != null; if (core.effects_len != 0) pardes.turn.awaitSettled(epoch); // Once a wait returns `core` may be gone: a Restore meanwhile put a @@ -698,6 +702,21 @@ pub const Listener = struct { // A connection task may be waiting for the turn; it answers // nothing more once stopping, but the wait itself must end. pardes.turn.rest(); + // An answer given as the editor quit (a Kill written to /ctl) + // may still be on its way out: let it go before the cut. + const deadline = nowMs() +| 200; + while (nowMs() < deadline) { + const pending = for (&l.runner.conns) |*conn| { + if (!conn.live()) continue; + conn.lock(); + const n = conn.engine.output().len; + conn.unlock(); + if (n != 0) break true; + } else false; + if (!pending) break; + Client.nap(1); + } + Client.nap(1); // the last bytes leave the stream's own buffer l.runner.stop(); pardes.turn.wake(); } @@ -405,9 +405,29 @@ def run_file(binary): assert closing == [b'error shell gone\n'], closing +def kill_answers(binary): + """Kill written to /ctl is answered before the editor quits.""" + with tempfile.TemporaryDirectory(prefix='pardes-kill-') as directory: + seen = {} + with session(binary, Path(directory), 'kill', terminal=seen) as (client, _): + client.write('/ctl', b'Kill\n') + deadline = time.monotonic() + 5 + while time.monotonic() < deadline: + try: + # a zombie has quit; the session reaps it + if Path(f'/proc/{seen["pid"]}/stat').read_text().split(') ')[1][0] == 'Z': + break + except FileNotFoundError: + break + time.sleep(.01) + else: + raise AssertionError('Kill did not quit the editor') + + def test(binary, quic=False): started = time.monotonic() run_file(binary) + kill_answers(binary) for options, message in [ (['--fs'], b'no such option'), (['--fs9'], b'no such option'), |
