mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 02:00:19 +02:00
Fix Windows hangs found against a real Frame
- Child processes never inherit the server's stdin. Under the app it's the pipe held open for --exit-on-eof, and Windows' ssh.exe waited on it forever, so captures, the screenshot list and Android apps timed out. - frame_connect's key check accepts a first-seen host key (as the copy step does), so an already-authorized key doesn't trigger a password prompt. Verified on Windows 11, Ubuntu and macOS against a Steam Frame: status, headset and desktop captures, live video, library, Android apps, screenshots and upload. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
37153f69ae
commit
03729ce950
6 files changed
+78
-16
No files matched your search
+5
-3
@@ -29,7 +29,9 @@ class FrameError(RuntimeError):
|
||||
|
||||
def ssh(cmd, input=None, timeout=120):
|
||||
try:
|
||||
p = subprocess.run(['ssh', *SSH_OPTS, FRAME, cmd], input=input, capture_output=True,
|
||||
# No inherited stdin (see server.ssh): Windows' ssh.exe would wait on it.
|
||||
feed = {'input': input} if input is not None else {'stdin': subprocess.DEVNULL}
|
||||
p = subprocess.run(['ssh', *SSH_OPTS, FRAME, cmd], capture_output=True, **feed,
|
||||
timeout=timeout, text=isinstance(input, str) or input is None)
|
||||
except subprocess.TimeoutExpired:
|
||||
raise FrameError(f'timed out talking to {FRAME}')
|
||||
@@ -65,7 +67,7 @@ def apk_info(path):
|
||||
tool = aapt2()
|
||||
if not tool:
|
||||
raise FrameError(f"aapt2 not found: {frame_host.install_hint('aapt2')}")
|
||||
out = subprocess.run([tool, 'dump', 'badging', path], capture_output=True, text=True).stdout
|
||||
out = subprocess.run([tool, 'dump', 'badging', path], capture_output=True, stdin=subprocess.DEVNULL, text=True).stdout
|
||||
m = re.search(r"package: name='([^']+)'.*?versionName='([^']*)'", out)
|
||||
if not m:
|
||||
raise FrameError(f'not a readable APK: {os.path.basename(path)}')
|
||||
@@ -115,7 +117,7 @@ def _copy(src, dest, executable=False, timeout=600):
|
||||
else:
|
||||
cmd = ['scp', *SSH_OPTS, src, f'{FRAME}:{dest}']
|
||||
try:
|
||||
subprocess.run(cmd, check=True, capture_output=True, text=True, timeout=timeout)
|
||||
subprocess.run(cmd, check=True, capture_output=True, stdin=subprocess.DEVNULL, text=True, timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
raise FrameError(f'copying {name} to the Frame timed out')
|
||||
except subprocess.CalledProcessError as e:
|
||||
|
||||
+3
-1
@@ -119,7 +119,9 @@ def write_config(host, port=22):
|
||||
|
||||
|
||||
def key_login_works():
|
||||
return subprocess.run(["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", FRAME_ALIAS, "true"],
|
||||
# accept-new: trust a first-seen host key (as the copy step does); a changed one still fails.
|
||||
return subprocess.run(["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5",
|
||||
"-o", "StrictHostKeyChecking=accept-new", FRAME_ALIAS, "true"],
|
||||
capture_output=True).returncode == 0
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -185,7 +185,7 @@ def clipboard_text():
|
||||
for cmd in cmds:
|
||||
if not shutil.which(cmd[0]):
|
||||
continue
|
||||
r = subprocess.run(cmd, capture_output=True, timeout=10)
|
||||
r = subprocess.run(cmd, capture_output=True, stdin=subprocess.DEVNULL, timeout=10)
|
||||
if r.returncode == 0:
|
||||
text = r.stdout.decode("utf-8", errors="replace")
|
||||
return text[:-2] if WINDOWS and text.endswith("\r\n") else text
|
||||
@@ -197,7 +197,7 @@ def clipboard_text():
|
||||
def ssh_hostname(alias):
|
||||
"""The real host name an ssh alias points at (`ssh -G`), for non-SSH clients like RDP."""
|
||||
try:
|
||||
out = subprocess.run(["ssh", "-G", alias], capture_output=True, text=True, timeout=10).stdout
|
||||
out = subprocess.run(["ssh", "-G", alias], capture_output=True, stdin=subprocess.DEVNULL, text=True, timeout=10).stdout
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return alias
|
||||
for line in out.splitlines():
|
||||
|
||||
+9
-6
@@ -95,7 +95,7 @@ def ensure_master():
|
||||
|
||||
def up():
|
||||
try:
|
||||
return subprocess.run([*MUX, "-O", "check", FRAME], capture_output=True,
|
||||
return subprocess.run([*MUX, "-O", "check", FRAME], capture_output=True, stdin=subprocess.DEVNULL,
|
||||
timeout=5).returncode == 0
|
||||
except subprocess.TimeoutExpired:
|
||||
return False
|
||||
@@ -118,7 +118,10 @@ def ensure_master():
|
||||
def ssh(remote, *, stdin=None, timeout=30, text=True):
|
||||
try:
|
||||
ensure_master()
|
||||
r = subprocess.run([*SSH, FRAME, remote], input=stdin, capture_output=True,
|
||||
# Never let ssh inherit our stdin: under the app it's the pipe held open for
|
||||
# --exit-on-eof, and Windows' ssh.exe waits on it forever.
|
||||
feed = {"input": stdin} if stdin is not None else {"stdin": subprocess.DEVNULL}
|
||||
r = subprocess.run([*SSH, FRAME, remote], capture_output=True, **feed,
|
||||
text=text, errors="replace" if text else None, timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
raise Failure(f"Timed out talking to {FRAME}")
|
||||
@@ -494,7 +497,7 @@ def adb_path():
|
||||
|
||||
def adb(adb_bin, *args, timeout=20):
|
||||
try:
|
||||
r = subprocess.run([adb_bin, *args], capture_output=True, text=True,
|
||||
r = subprocess.run([adb_bin, *args], capture_output=True, stdin=subprocess.DEVNULL, text=True,
|
||||
errors="replace", timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
raise Failure(f"adb {' '.join(args[-2:])} timed out")
|
||||
@@ -609,7 +612,7 @@ class AdbTunnel:
|
||||
self._stop_ssh()
|
||||
for p in self.local:
|
||||
try:
|
||||
subprocess.run([self.adb, "disconnect", self.serial(p)], capture_output=True, timeout=10)
|
||||
subprocess.run([self.adb, "disconnect", self.serial(p)], capture_output=True, stdin=subprocess.DEVNULL, timeout=10)
|
||||
except (subprocess.TimeoutExpired, OSError):
|
||||
pass
|
||||
finally:
|
||||
@@ -799,7 +802,7 @@ def push_file(path, dest="Downloads/"):
|
||||
else:
|
||||
# Modern scp uses SFTP, so the remote path isn't parsed by a shell.
|
||||
cmd = ["scp", *SSH[1:], "-r", str(path), f"{FRAME}:{dest}"]
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, errors="replace", timeout=3600)
|
||||
r = subprocess.run(cmd, capture_output=True, stdin=subprocess.DEVNULL, text=True, errors="replace", timeout=3600)
|
||||
except subprocess.TimeoutExpired:
|
||||
raise Failure(f"Copying {name} timed out")
|
||||
if r.returncode != 0:
|
||||
@@ -1046,7 +1049,7 @@ def main():
|
||||
finally:
|
||||
# The master was started with -N, so it stays up until told to exit.
|
||||
if CONTROL:
|
||||
subprocess.run([*MUX, "-O", "exit", FRAME], capture_output=True)
|
||||
subprocess.run([*MUX, "-O", "exit", FRAME], capture_output=True, stdin=subprocess.DEVNULL)
|
||||
if _master and _master.poll() is None:
|
||||
_master.terminate()
|
||||
for proc in list(_live_tunnels): # ADB forwards and video streams cut off mid-way
|
||||
|
||||
Reference in new issue
Block a user