Stop cancelled remote queries without blocking the selector

RemoteQueryPool.cancel() terminated the SSH process and then waited up
to 250 ms for it, on the curses thread. Ctrl+R re-enqueues every host,
so a refresh of six hosts could freeze the popup for over a second.

Only send SIGTERM when cancelling; reap the process on later polls,
escalating to SIGKILL after 250 ms and closing its pipes once it has
exited. close() still waits, since the popup is exiting anyway.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 03:04:27 +00:00
co-authored by Claude Fable 5.1
parent 435ba91d1e
commit 72d594033e
2 changed files with 70 additions and 11 deletions
+27
View File
@@ -282,6 +282,33 @@ class RemoteSshTests(unittest.TestCase):
self.assertEqual(decode_remote_output("错误".encode("gbk")), "错误")
def test_cancel_does_not_block_and_reaps_the_process_later(self) -> None:
with tempfile.TemporaryDirectory() as directory:
slow_ssh = Path(directory) / "ssh"
# Ignore SIGTERM so the pool has to escalate to SIGKILL.
slow_ssh.write_text("#!/bin/sh\ntrap '' TERM\nsleep 5 & wait\n", encoding="utf-8")
slow_ssh.chmod(0o755)
pool = RemoteQueryPool(str(slow_ssh), query_timeout=5)
self.addCleanup(pool.close)
pool.enqueue(RemoteEntry("slow"))
time.sleep(0.05)
process = pool._active["slow"].process # type: ignore[attr-defined]
started = time.monotonic()
pool.cancel("slow")
self.assertLess(time.monotonic() - started, 0.1)
self.assertFalse(pool.is_loading("slow"))
deadline = time.monotonic() + 3
while process.poll() is None and time.monotonic() < deadline:
pool.poll()
time.sleep(0.02)
self.assertIsNotNone(process.poll())
pool.poll()
self.assertEqual(pool._stopping, []) # type: ignore[attr-defined]
assert process.stdout is not None
self.assertTrue(process.stdout.closed)
def test_query_pool_times_out_without_blocking_other_results(self) -> None:
with tempfile.TemporaryDirectory() as directory:
slow_ssh = Path(directory) / "ssh"