Skip to content

Commit 1822765

Browse files
shchekleinclaude
andauthored
tests: fix command-queue race in mock-ssh-server exec handling (#72)
Handler.run() creates the per-channel command queue with a check-then-assign while paramiko's transport thread creates it with setdefault() and puts the command into it from check_channel_exec_request(). When the exec request lands inside the window, run() replaces the queue that already holds the command, handle_client() blocks on Queue.get() forever and the client never receives an exit status. This is the intermittent CI hang: every _execute()-based operation (cp_file, checksum, the move fallback) rolls these dice, and test_concurrency_for_raw_commands rolls them 16 at a time. Confirmed by the faulthandler dump from the first run with #71 merged (handle_client threads parked on Queue.get with the client waiting), and reproduced deterministically by widening the window with a 5ms sleep: upstream logic deadlocks on the first round, the same logic with an atomic setdefault() survives, with or without the delay. mock-ssh-server is unmaintained, so the method is patched in conftest instead of upstream. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 1dcfc1a commit 1822765

1 file changed

Lines changed: 28 additions & 0 deletions

File tree

tests/conftest.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import threading
2+
from queue import Queue
3+
4+
import mockssh.server
5+
6+
7+
def _handler_run(self):
8+
# Identical to mockssh.server.Handler.run except that the command
9+
# queue is created atomically. Upstream checks `chanid not in
10+
# command_queues` and then assigns a fresh Queue, while paramiko's
11+
# transport thread may concurrently create the queue and put the
12+
# command into it via check_channel_exec_request(); the assignment
13+
# then replaces that queue, the command is lost, and handle_client
14+
# blocks on Queue.get() forever -- the client never receives an
15+
# exit status. mock-ssh-server is unmaintained, so it is patched
16+
# here instead of upstream.
17+
self.transport.start_server(server=self)
18+
while True:
19+
channel = self.transport.accept()
20+
if channel is None:
21+
break
22+
self.command_queues.setdefault(channel.chanid, Queue())
23+
thread = threading.Thread(target=self.handle_client, args=(channel,))
24+
thread.daemon = True
25+
thread.start()
26+
27+
28+
mockssh.server.Handler.run = _handler_run

0 commit comments

Comments
 (0)