mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 05:02:50 +02:00
Remote desktop: say which way the Frame didn't answer
Second review: only a refused port 3389 means xrdp is off. A name that doesn't resolve, a timeout or no route now say so, rather than telling the person to turn on Developer Mode. All are Unreachable (a 400, no error diagnostic). The .rdp file name is a digest of the address, since fe80::1%2 and fe80::1:2 sanitised to the same name. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d9cd40c035
commit
e8db571a2c
3 files changed
+36
-17
No files matched your search
+16
-3
@@ -71,10 +71,22 @@ class OpenRdp(unittest.TestCase):
|
|||||||
def test_nothing_listening_says_why_and_opens_nothing(self):
|
def test_nothing_listening_says_why_and_opens_nothing(self):
|
||||||
self.xrdp.close()
|
self.xrdp.close()
|
||||||
for name in ("windows", "mac", "linux"):
|
for name in ("windows", "mac", "linux"):
|
||||||
with self.subTest(name), platform(name), self.assertRaises(frame_host.NotListening) as cm:
|
with self.subTest(name), platform(name), self.assertRaises(frame_host.Unreachable) as cm:
|
||||||
frame_host.open_rdp("frame", "127.0.0.1")
|
frame_host.open_rdp("frame", "127.0.0.1")
|
||||||
self.assertIn("Developer Mode", str(cm.exception))
|
self.assertIn("Developer Mode", str(cm.exception))
|
||||||
self.assertIn(f"port {frame_host.RDP_PORT}", str(cm.exception))
|
self.assertIn(f"port {frame_host.RDP_PORT} refused", str(cm.exception))
|
||||||
|
self.assertEqual(self.spawned, [])
|
||||||
|
|
||||||
|
def test_says_which_way_it_failed(self):
|
||||||
|
# Only a refused port says xrdp is off; a wrong address or a silent network say so instead.
|
||||||
|
for error, says in ((socket.gaierror(8, "nodename nor servname provided"), "Devices tab"),
|
||||||
|
(socket.timeout("timed out"), "didn't answer"),
|
||||||
|
(OSError(65, "No route to host"), "didn't answer")):
|
||||||
|
with self.subTest(says), mock.patch.object(frame_host.socket, "create_connection", side_effect=error), \
|
||||||
|
platform("windows"), self.assertRaises(frame_host.Unreachable) as cm:
|
||||||
|
frame_host.open_rdp("frame", "frame.local")
|
||||||
|
self.assertIn(says, str(cm.exception))
|
||||||
|
self.assertNotIn("refused", str(cm.exception))
|
||||||
self.assertEqual(self.spawned, [])
|
self.assertEqual(self.spawned, [])
|
||||||
|
|
||||||
def test_server_says_it_as_the_persons_to_fix(self):
|
def test_server_says_it_as_the_persons_to_fix(self):
|
||||||
@@ -89,7 +101,8 @@ class OpenRdp(unittest.TestCase):
|
|||||||
def test_one_file_per_address(self):
|
def test_one_file_per_address(self):
|
||||||
with platform("windows"):
|
with platform("windows"):
|
||||||
a, b = frame_host.rdp_file("192.168.1.5"), frame_host.rdp_file("fe80::1%eth0")
|
a, b = frame_host.rdp_file("192.168.1.5"), frame_host.rdp_file("fe80::1%eth0")
|
||||||
self.assertNotEqual(a, b)
|
c, d = frame_host.rdp_file("fe80::1%2"), frame_host.rdp_file("fe80::1:2")
|
||||||
|
self.assertEqual(len({a, b, c, d}), 4)
|
||||||
self.assertIn(b"full address:s:192.168.1.5\r\n", a.read_bytes())
|
self.assertIn(b"full address:s:192.168.1.5\r\n", a.read_bytes())
|
||||||
self.assertIn(b"full address:s:fe80::1%eth0\r\n", b.read_bytes())
|
self.assertIn(b"full address:s:fe80::1%eth0\r\n", b.read_bytes())
|
||||||
|
|
||||||
|
|||||||
+19
-13
@@ -5,8 +5,8 @@ Everything here runs on your computer, not the Frame. Python stdlib only.
|
|||||||
CLI (used by the Electron app, so terminal handling lives in one place):
|
CLI (used by the Electron app, so terminal handling lives in one place):
|
||||||
python3 ui/frame_host.py terminal -- CMD [ARG...] # open CMD in a terminal window
|
python3 ui/frame_host.py terminal -- CMD [ARG...] # open CMD in a terminal window
|
||||||
"""
|
"""
|
||||||
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
@@ -34,8 +34,8 @@ class HostError(RuntimeError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NotListening(HostError):
|
class Unreachable(HostError):
|
||||||
"""The Frame answers, but a service on it doesn't: something the person can turn on."""
|
"""The Frame, or a service on it, didn't answer: the person's to sort out, not a fault here."""
|
||||||
|
|
||||||
|
|
||||||
def data_dir(*parts):
|
def data_dir(*parts):
|
||||||
@@ -277,13 +277,22 @@ RDP_LOGIN = (f"accept the warning about the Frame's certificate, then sign in as
|
|||||||
"with your Developer Mode password")
|
"with your Developer Mode password")
|
||||||
|
|
||||||
|
|
||||||
def rdp_reachable(host, timeout=3):
|
def check_rdp(host, timeout=3):
|
||||||
"""Whether anything answers on the Frame's RDP port."""
|
"""Raise Unreachable, saying why, unless the Frame's RDP port takes a connection."""
|
||||||
try:
|
try:
|
||||||
with socket.create_connection((host, RDP_PORT), timeout=timeout):
|
with socket.create_connection((host, RDP_PORT), timeout=timeout):
|
||||||
return True
|
return
|
||||||
except OSError:
|
except ConnectionRefusedError:
|
||||||
return False
|
raise Unreachable(f"The Frame at {host} is on but isn't accepting remote desktop (port {RDP_PORT} "
|
||||||
|
"refused). Turn on Developer Mode in Steam Settings > System on the headset, "
|
||||||
|
"then restart it and try again.") from None
|
||||||
|
except socket.gaierror:
|
||||||
|
raise Unreachable(f"Can't find {host} on the network for remote desktop. Check the headset's "
|
||||||
|
"address on the Devices tab.") from None
|
||||||
|
except OSError as e:
|
||||||
|
raise Unreachable(f"The Frame didn't answer remote desktop at {host} ({e}). It may be asleep, "
|
||||||
|
"switched off or on another network; if it's on, check Developer Mode is on "
|
||||||
|
"in Steam Settings > System.") from None
|
||||||
|
|
||||||
|
|
||||||
def rdp_file(host):
|
def rdp_file(host):
|
||||||
@@ -292,7 +301,7 @@ def rdp_file(host):
|
|||||||
if any(c in host for c in "\r\n"):
|
if any(c in host for c in "\r\n"):
|
||||||
raise HostError("That headset address can't be used for remote desktop")
|
raise HostError("That headset address can't be used for remote desktop")
|
||||||
# One file per address, so two launches close together can't swap headsets.
|
# One file per address, so two launches close together can't swap headsets.
|
||||||
path = cache_dir(f"frame-{re.sub(r'[^A-Za-z0-9.-]', '_', host)}.rdp")
|
path = cache_dir(f"frame-{hashlib.sha256(host.encode()).hexdigest()[:16]}.rdp")
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(path, "w", encoding="utf-8", newline="\r\n") as f: # Path.write_text(newline=) is 3.10+
|
with open(path, "w", encoding="utf-8", newline="\r\n") as f: # Path.write_text(newline=) is 3.10+
|
||||||
f.write(f"full address:s:{host}\nusername:s:{RDP_USER}\n")
|
f.write(f"full address:s:{host}\nusername:s:{RDP_USER}\n")
|
||||||
@@ -303,10 +312,7 @@ def open_rdp(alias, host=None):
|
|||||||
"""Remote desktop to the Frame's xrdp (user steamos), at `host` or where the alias points."""
|
"""Remote desktop to the Frame's xrdp (user steamos), at `host` or where the alias points."""
|
||||||
host = host or ssh_hostname(alias)
|
host = host or ssh_hostname(alias)
|
||||||
# The client would open either way and then fail on its own, with nothing said here.
|
# The client would open either way and then fail on its own, with nothing said here.
|
||||||
if not rdp_reachable(host):
|
check_rdp(host)
|
||||||
raise NotListening(f"The Frame isn't accepting remote desktop at {host} (nothing answered on port "
|
|
||||||
f"{RDP_PORT}). Turn on Developer Mode in Steam Settings > System on the headset, "
|
|
||||||
"then restart it and try again.")
|
|
||||||
if MAC:
|
if MAC:
|
||||||
if subprocess.run(["open", "-a", "Windows App"], capture_output=True).returncode == 0:
|
if subprocess.run(["open", "-a", "Windows App"], capture_output=True).returncode == 0:
|
||||||
return f"Opened Windows App: connect to {host} and {RDP_LOGIN}"
|
return f"Opened Windows App: connect to {host} and {RDP_LOGIN}"
|
||||||
|
|||||||
+1
-1
@@ -1192,7 +1192,7 @@ def open_thing(body):
|
|||||||
SHOTS_DIR.mkdir(parents=True, exist_ok=True)
|
SHOTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
frame_host.open_path(SHOTS_DIR)
|
frame_host.open_path(SHOTS_DIR)
|
||||||
return {"message": f"Opened {SHOTS_DIR} in {frame_host.FILE_MANAGER}"}
|
return {"message": f"Opened {SHOTS_DIR} in {frame_host.FILE_MANAGER}"}
|
||||||
except frame_host.NotListening as e:
|
except frame_host.Unreachable as e:
|
||||||
raise Failure(str(e), 400) # theirs to turn on; nothing failed here
|
raise Failure(str(e), 400) # theirs to turn on; nothing failed here
|
||||||
except frame_host.HostError as e:
|
except frame_host.HostError as e:
|
||||||
raise Failure(str(e), 500)
|
raise Failure(str(e), 500)
|
||||||
|
|||||||
Reference in new issue
Block a user