Skip to content

Commit a0a8c23

Browse files
authored
refactor: consolidate ControlMaster side-channel invocation (#128)
1 parent 78a3af3 commit a0a8c23

1 file changed

Lines changed: 66 additions & 83 deletions

File tree

server.py

Lines changed: 66 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -2230,14 +2230,55 @@ def terminate_remote_tmux(self):
22302230
# died, peer FIN, etc.) — best-effort, fall through.
22312231
pass
22322232

2233+
# ── ControlMaster side-channel plumbing ─────────────────────────
2234+
#
2235+
# Every side-channel operation (tmux capture/options, file transfer,
2236+
# ls) runs a one-shot command over the existing ControlMaster socket
2237+
# with the same ssh argv. Build it in exactly one place so a future
2238+
# option change can't silently miss one of the seven call sites.
2239+
# terminate_remote_tmux is deliberately NOT on this helper: it
2240+
# re-dials with -p/-l/ConnectTimeout because its socket may belong
2241+
# to a master that is still authenticating.
2242+
2243+
def _mux_ready(self):
2244+
"""True when the ControlMaster socket exists and can be dialed."""
2245+
return bool(self._control_path) and os.path.exists(self._control_path)
2246+
2247+
def _mux_argv(self, remote_cmd):
2248+
"""argv for a one-shot remote command over the ControlMaster."""
2249+
return [
2250+
"ssh", "-T",
2251+
"-o", "BatchMode=yes",
2252+
"-o", "ControlPath=" + self._control_path,
2253+
"--", self._host, remote_cmd,
2254+
]
2255+
2256+
def _mux_run(self, remote_cmd, timeout, timeout_msg,
2257+
err_prefix="ssh error: "):
2258+
"""subprocess.run a remote command over the ControlMaster with
2259+
the shared timeout/exception handling. Returns (CompletedProcess,
2260+
None) on spawn success — the caller still interprets returncode/
2261+
stdout/stderr, which genuinely differ per operation — or
2262+
(None, error_string)."""
2263+
try:
2264+
proc = subprocess.run(self._mux_argv(remote_cmd),
2265+
capture_output=True, timeout=timeout)
2266+
except subprocess.TimeoutExpired:
2267+
return None, timeout_msg
2268+
except Exception as e:
2269+
# `or repr(e)` keeps the error truthy even for the rare
2270+
# exception with an empty str() — callers branch on `if err:`.
2271+
return None, err_prefix + (str(e) or repr(e))
2272+
return proc, None
2273+
22332274
def tmux_capture(self):
22342275
"""Capture the full tmux pane buffer (scrollback + visible) over
22352276
the ControlMaster channel. Only meaningful for persistent
22362277
sessions — xterm.js can't see tmux's own scrollback. Returns
22372278
(bytes, error)."""
22382279
if not self.persistent or not self.slot_id:
22392280
return None, "not a persistent session"
2240-
if not self._control_path or not os.path.exists(self._control_path):
2281+
if not self._mux_ready():
22412282
return None, "control socket not ready"
22422283
# `-S -<N>` reads the most recent N lines of history (was `-S -`,
22432284
# the whole history, which on a 10M-line scrollback buffers
@@ -2248,19 +2289,9 @@ def tmux_capture(self):
22482289
tname = "websh-" + self.slot_id
22492290
remote_cmd = (self.tmux_cmd + " capture-pane -p -J -S -" +
22502291
str(MAX_TMUX_CAPTURE_LINES) + " -t " + tname)
2251-
ssh_cmd = [
2252-
"ssh", "-T",
2253-
"-o", "BatchMode=yes",
2254-
"-o", "ControlPath=" + self._control_path,
2255-
"--", self._host, remote_cmd,
2256-
]
2257-
try:
2258-
proc = subprocess.run(
2259-
ssh_cmd, capture_output=True, timeout=30)
2260-
except subprocess.TimeoutExpired:
2261-
return None, "tmux capture timeout"
2262-
except Exception as e:
2263-
return None, "ssh error: " + str(e)
2292+
proc, err = self._mux_run(remote_cmd, 30, "tmux capture timeout")
2293+
if err:
2294+
return None, err
22642295
if proc.returncode != 0:
22652296
err = proc.stderr.decode("utf-8", "replace").strip()[:300]
22662297
return None, "tmux exit %d: %s" % (proc.returncode, err)
@@ -2283,7 +2314,7 @@ def push_tmux_options(self, options):
22832314
used at connect time. Returns (ok, error)."""
22842315
if not self.persistent or not self.slot_id:
22852316
return False, "not a persistent session"
2286-
if not self._control_path or not os.path.exists(self._control_path):
2317+
if not self._mux_ready():
22872318
return False, "control socket not ready"
22882319
if not options:
22892320
return True, ""
@@ -2294,19 +2325,9 @@ def push_tmux_options(self, options):
22942325
# connect time.
22952326
parts = ["set -g " + opt + " " + val for opt, val in options]
22962327
remote_cmd = self.tmux_cmd + " " + " \\; ".join(parts)
2297-
ssh_cmd = [
2298-
"ssh", "-T",
2299-
"-o", "BatchMode=yes",
2300-
"-o", "ControlPath=" + self._control_path,
2301-
"--", self._host, remote_cmd,
2302-
]
2303-
try:
2304-
proc = subprocess.run(
2305-
ssh_cmd, capture_output=True, timeout=10)
2306-
except subprocess.TimeoutExpired:
2307-
return False, "tmux set timeout"
2308-
except Exception as e:
2309-
return False, "ssh error: " + str(e)
2328+
proc, err = self._mux_run(remote_cmd, 10, "tmux set timeout")
2329+
if err:
2330+
return False, err
23102331
if proc.returncode != 0:
23112332
err = proc.stderr.decode("utf-8", "replace").strip()[:300]
23122333
return False, "tmux exit %d: %s" % (proc.returncode, err)
@@ -2321,7 +2342,7 @@ def upload_file(self, rel_path, body_stream, length,
23212342
re-auth and no PTY overhead. Returns (ok, error)."""
23222343
if not self.alive:
23232344
return False, "session is dead"
2324-
if not self._control_path or not os.path.exists(self._control_path):
2345+
if not self._mux_ready():
23252346
return False, "control socket not ready"
23262347

23272348
# rel_path is base64-encoded and decoded inside the remote shell so
@@ -2334,14 +2355,8 @@ def upload_file(self, rel_path, body_stream, length,
23342355
'cat > "$HOME/$n"'
23352356
)
23362357

2337-
ssh_cmd = [
2338-
"ssh", "-T",
2339-
"-o", "BatchMode=yes",
2340-
"-o", "ControlPath=" + self._control_path,
2341-
"--", self._host, remote_cmd,
2342-
]
23432358
proc = subprocess.Popen(
2344-
ssh_cmd,
2359+
self._mux_argv(remote_cmd),
23452360
stdin=subprocess.PIPE,
23462361
# `cat >` produces no stdout; discard it. stderr is kept (for the
23472362
# "ssh exit N: <msg>" error below) but MUST be drained while we
@@ -2452,7 +2467,7 @@ def finalize_upload(self, tmp_name, final_name):
24522467
guard for vim/less/htop)."""
24532468
if not self.persistent or not self.slot_id:
24542469
return False, "non-persistent"
2455-
if not self._control_path or not os.path.exists(self._control_path):
2470+
if not self._mux_ready():
24562471
return False, "control socket not ready"
24572472

24582473
# Both names are base64-encoded in case the user picked a file
@@ -2493,18 +2508,9 @@ def finalize_upload(self, tmp_name, final_name):
24932508
'fi; '
24942509
'mv -- "$HOME/$t" "./$f" && printf %s "$cwd/$f"'
24952510
)
2496-
ssh_cmd = [
2497-
"ssh", "-T",
2498-
"-o", "BatchMode=yes",
2499-
"-o", "ControlPath=" + self._control_path,
2500-
"--", self._host, remote_cmd,
2501-
]
2502-
try:
2503-
proc = subprocess.run(ssh_cmd, capture_output=True, timeout=15)
2504-
except subprocess.TimeoutExpired:
2505-
return False, "finalize timeout"
2506-
except Exception as e:
2507-
return False, "ssh error: " + str(e)
2511+
proc, err = self._mux_run(remote_cmd, 15, "finalize timeout")
2512+
if err:
2513+
return False, err
25082514
if proc.returncode != 0:
25092515
err = proc.stderr.decode("utf-8", "replace").strip()[:300]
25102516
return False, "finalize exit %d: %s" % (proc.returncode, err)
@@ -2516,7 +2522,7 @@ def remove_remote_tmp(self, rel_path):
25162522
the user cancelled — keystroke-free, so no risk of poking a
25172523
running editor in the foreground PTY. Idempotent. rel_path
25182524
must come from the caller's path validator."""
2519-
if not self._control_path or not os.path.exists(self._control_path):
2525+
if not self._mux_ready():
25202526
return False, "control socket not ready"
25212527
b = base64.b64encode(rel_path.encode("utf-8")).decode("ascii")
25222528
# The `--` after rm protects against an attacker-supplied path
@@ -2526,18 +2532,9 @@ def remove_remote_tmp(self, rel_path):
25262532
'n=$(printf %s ' + b + ' | base64 -d) && '
25272533
'rm -f -- "$HOME/$n"'
25282534
)
2529-
ssh_cmd = [
2530-
"ssh", "-T",
2531-
"-o", "BatchMode=yes",
2532-
"-o", "ControlPath=" + self._control_path,
2533-
"--", self._host, remote_cmd,
2534-
]
2535-
try:
2536-
proc = subprocess.run(ssh_cmd, capture_output=True, timeout=10)
2537-
except subprocess.TimeoutExpired:
2538-
return False, "rm timeout"
2539-
except Exception as e:
2540-
return False, "ssh error: " + str(e)
2535+
proc, err = self._mux_run(remote_cmd, 10, "rm timeout")
2536+
if err:
2537+
return False, err
25412538
if proc.returncode != 0:
25422539
return False, "rm exit %d" % proc.returncode
25432540
return True, ""
@@ -2546,7 +2543,7 @@ def list_dir(self, remote_path):
25462543
"""List a directory via the ControlMaster side-channel.
25472544
remote_path may be absolute, ~, ~/sub, or relative-to-$HOME.
25482545
Returns (entries, abs_path, error_string)."""
2549-
if not self._control_path or not os.path.exists(self._control_path):
2546+
if not self._mux_ready():
25502547
return None, None, "control socket not ready"
25512548

25522549
b64 = base64.b64encode(remote_path.encode("utf-8")).decode("ascii")
@@ -2581,18 +2578,10 @@ def list_dir(self, remote_path):
25812578
'printf "%s\\t%s\\t%s\\t%s\\0" "$t" "$s" "$m" "$f"; '
25822579
'done'
25832580
)
2584-
ssh_cmd = [
2585-
"ssh", "-T",
2586-
"-o", "BatchMode=yes",
2587-
"-o", "ControlPath=" + self._control_path,
2588-
"--", self._host, remote_cmd,
2589-
]
2590-
try:
2591-
result = subprocess.run(ssh_cmd, capture_output=True, timeout=10)
2592-
except subprocess.TimeoutExpired:
2593-
return None, None, "timeout"
2594-
except Exception as e:
2595-
return None, None, str(e)
2581+
# err_prefix="" keeps this method's historical bare str(e) message.
2582+
result, err = self._mux_run(remote_cmd, 10, "timeout", err_prefix="")
2583+
if err:
2584+
return None, None, err
25962585
if result.returncode != 0:
25972586
return None, None, "directory not found"
25982587

@@ -2628,7 +2617,7 @@ def download_file(self, remote_path):
26282617
Subprocess stdout starts with a header "OK\\t<size>\\n" or
26292618
"ERR\\t<msg>\\n" so the caller can detect failure before
26302619
sending HTTP response headers."""
2631-
if not self._control_path or not os.path.exists(self._control_path):
2620+
if not self._mux_ready():
26322621
return None, "control socket not ready"
26332622

26342623
b64 = base64.b64encode(remote_path.encode("utf-8")).decode("ascii")
@@ -2646,20 +2635,14 @@ def download_file(self, remote_path):
26462635
'cat -- "$F"; '
26472636
'else printf "ERR\\tFile not found\\n"; fi'
26482637
)
2649-
ssh_cmd = [
2650-
"ssh", "-T",
2651-
"-o", "BatchMode=yes",
2652-
"-o", "ControlPath=" + self._control_path,
2653-
"--", self._host, remote_cmd,
2654-
]
26552638
try:
26562639
# stderr→DEVNULL: the protocol header (OK/ERR on stdout) already
26572640
# signals failure to the caller, and a PIPE that nobody drains
26582641
# would deadlock the child once ssh writes >~64 KB of warnings
26592642
# (host-key prompts, banners, debug). Same pattern as
26602643
# terminate_remote_tmux.
26612644
proc = subprocess.Popen(
2662-
ssh_cmd,
2645+
self._mux_argv(remote_cmd),
26632646
stdin=subprocess.DEVNULL,
26642647
stdout=subprocess.PIPE,
26652648
stderr=subprocess.DEVNULL,

0 commit comments

Comments
 (0)