mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 09:00:35 +02:00
Devices: several headsets, several addresses each, and live connection status
Frame Control can now manage more than one Steam Frame, and reach each at any of several addresses (LAN IPs per network, its .local name, Tailscale). A connector in the server tries them all at once, picks the best one that answers, follows ssh -v through each stage (network, finding, SSH, identity, login) and streams that to the page. The header shows it live; a new Devices tab (key 5) manages headsets, addresses and network names. - ui/frame_devices.py: registry in devices.json, imported from the managed ~/.ssh/config blocks; per-headset host key pinning; config block updates. - ui/frame_network.py: gateway IP+MAC fingerprint, Wi-Fi name, Tailscale. - ui/frame_link.py: the connector, Test now, Tailscale/mDNS discovery, API. - server.py: ensure_master delegates to the connector; /api/connection, /api/connection/events (SSE), /api/devices. - Electron: headset switcher and Devices item in the Frame menu. - frame_connect.py --alias; FRAME_CONTROL_DATA_DIR / FRAME_CONTROL_SSH_DIR keep tests off real data. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
dcf9689f64
commit
a954fc83c9
24 files changed
+3597
-61
No files matched your search
@@ -0,0 +1,147 @@
|
||||
"""frame_network's parsers, with what macOS, Linux and Windows print.
|
||||
|
||||
Run: python3 -m unittest discover -s tests
|
||||
"""
|
||||
import sandbox # noqa: F401 (first: keeps tests off real data and services)
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "ui"))
|
||||
|
||||
import frame_network as fn # noqa: E402
|
||||
|
||||
MAC_ROUTE = """ route to: default
|
||||
destination: default
|
||||
mask: default
|
||||
gateway: 192.168.1.1
|
||||
interface: en0
|
||||
flags: <UP,GATEWAY,DONE,STATIC,PRCLONING,GLOBAL>
|
||||
"""
|
||||
MAC_ARP = "? (192.168.1.1) at b4:fb:e4:1:87:3f on en0 ifscope [ethernet]\n"
|
||||
MAC_ARP_INCOMPLETE = "? (192.168.1.1) at (incomplete) on en0 ifscope [ethernet]\n"
|
||||
MAC_SUMMARY = """<dictionary> {
|
||||
BSSID : <redacted>
|
||||
ConnectionID : 1
|
||||
InterfaceType : WiFi
|
||||
LinkStatusActive : TRUE
|
||||
NetworkID : <redacted>
|
||||
SSID : <redacted>
|
||||
Security : WPA2_PSK
|
||||
}"""
|
||||
MAC_SUMMARY_NAMED = MAC_SUMMARY.replace("SSID : <redacted>\n Security", "SSID : Home Net\n Security")
|
||||
MAC_SUMMARY_WIRED = "<dictionary> {\n InterfaceType : Ethernet\n LinkStatusActive : TRUE\n}"
|
||||
|
||||
LINUX_ROUTE = """default via 10.0.0.1 dev wlp2s0 proto dhcp src 10.0.0.23 metric 600
|
||||
default via 192.168.50.1 dev enp3s0 proto dhcp src 192.168.50.9 metric 100
|
||||
"""
|
||||
LINUX_NEIGH = "192.168.50.1 dev enp3s0 lladdr 00:11:22:aa:bb:cc REACHABLE\n"
|
||||
NMCLI = "no:Neighbour\nyes:Cafe\\: upstairs\nno:\n"
|
||||
|
||||
WIN_ROUTE = """===========================================================================
|
||||
Interface List
|
||||
12...00 15 5d 01 02 03 ......Intel(R) Wi-Fi 6 AX201 160MHz
|
||||
===========================================================================
|
||||
|
||||
IPv4 Route Table
|
||||
===========================================================================
|
||||
Active Routes:
|
||||
Network Destination Netmask Gateway Interface Metric
|
||||
0.0.0.0 0.0.0.0 192.168.0.254 192.168.0.40 50
|
||||
0.0.0.0 0.0.0.0 192.168.1.1 192.168.1.50 35
|
||||
===========================================================================
|
||||
Persistent Routes:
|
||||
None
|
||||
"""
|
||||
WIN_ARP = """
|
||||
Interface: 192.168.1.50 --- 0xc
|
||||
Internet Address Physical Address Type
|
||||
192.168.1.1 b4-fb-e4-b5-67-55 dynamic
|
||||
"""
|
||||
NETSH = """
|
||||
There is 1 interface on the system:
|
||||
|
||||
Name : Wi-Fi
|
||||
Description : Intel(R) Wi-Fi 6 AX201 160MHz
|
||||
State : connected
|
||||
SSID : Office 5G
|
||||
BSSID : 12:34:56:78:9a:bc
|
||||
Network type : Infrastructure
|
||||
"""
|
||||
NETSH_OFF = NETSH.replace("State : connected", "State : disconnected")
|
||||
|
||||
TAILSCALE = json.dumps({
|
||||
"BackendState": "Running",
|
||||
"CurrentTailnet": {"Name": "example.github"},
|
||||
"Self": {"HostName": "laptop", "DNSName": "laptop.tail1234.ts.net.", "TailscaleIPs": ["fd7a:115c:a1e0::1", "100.101.102.103"]},
|
||||
"Peer": {"nodekey:1": {"HostName": "frame", "DNSName": "frame.tail1234.ts.net.", "OS": "linux", "Online": True,
|
||||
"TailscaleIPs": ["100.113.174.84", "fd7a:115c:a1e0::5928:ae55"]},
|
||||
"nodekey:2": {"HostName": "phone", "DNSName": "phone.tail1234.ts.net.", "OS": "iOS", "Online": False,
|
||||
"TailscaleIPs": ["100.77.1.2"]}},
|
||||
})
|
||||
|
||||
|
||||
class Parsers(unittest.TestCase):
|
||||
def test_macos(self):
|
||||
self.assertEqual(fn.parse_route_macos(MAC_ROUTE), ("192.168.1.1", "en0"))
|
||||
self.assertEqual(fn.parse_route_macos("route: writing to routing socket: not in table\n"), (None, None))
|
||||
self.assertEqual(fn.parse_arp_macos(MAC_ARP, "192.168.1.1"), "b4:fb:e4:01:87:3f") # padded
|
||||
self.assertIsNone(fn.parse_arp_macos(MAC_ARP_INCOMPLETE, "192.168.1.1"))
|
||||
self.assertIsNone(fn.parse_arp_macos(MAC_ARP, "192.168.1.10"))
|
||||
self.assertEqual(fn.parse_summary_macos(MAC_SUMMARY), (None, True)) # no Location permission
|
||||
self.assertEqual(fn.parse_summary_macos(MAC_SUMMARY_NAMED), ("Home Net", True))
|
||||
self.assertEqual(fn.parse_summary_macos(MAC_SUMMARY_WIRED), (None, False))
|
||||
|
||||
def test_linux(self):
|
||||
self.assertEqual(fn.parse_route_linux(LINUX_ROUTE), ("192.168.50.1", "enp3s0")) # lowest metric
|
||||
self.assertEqual(fn.parse_route_linux(""), (None, None))
|
||||
self.assertEqual(fn.parse_neigh_linux(LINUX_NEIGH, "192.168.50.1"), "00:11:22:aa:bb:cc")
|
||||
self.assertIsNone(fn.parse_neigh_linux("192.168.50.1 dev enp3s0 FAILED\n", "192.168.50.1"))
|
||||
self.assertEqual(fn.parse_nmcli(NMCLI), "Cafe: upstairs")
|
||||
self.assertIsNone(fn.parse_nmcli("no:Neighbour\n"))
|
||||
|
||||
def test_windows(self):
|
||||
self.assertEqual(fn.parse_route_windows(WIN_ROUTE), ("192.168.1.1", "192.168.1.50"))
|
||||
self.assertEqual(fn.parse_arp_windows(WIN_ARP, "192.168.1.1"), "b4:fb:e4:b5:67:55")
|
||||
self.assertEqual(fn.parse_netsh(NETSH), "Office 5G") # not the BSSID
|
||||
self.assertIsNone(fn.parse_netsh(NETSH_OFF))
|
||||
|
||||
def test_mac_addresses(self):
|
||||
self.assertEqual(fn.norm_mac("B4-FB-E4-B5-67-55"), "b4:fb:e4:b5:67:55")
|
||||
for bad in ("", "(incomplete)", "ff:ff:ff:ff:ff:ff", "00:00:00:00:00:00", "b4:fb:e4:b5:67", "zz:fb:e4:b5:67:55"):
|
||||
self.assertIsNone(fn.norm_mac(bad), bad)
|
||||
|
||||
def test_network_id_is_stable_and_needs_both_parts(self):
|
||||
a = fn.network_id("192.168.1.1", "b4:fb:e4:b5:67:55")
|
||||
self.assertEqual(a, fn.network_id("192.168.1.1", "b4:fb:e4:b5:67:55"))
|
||||
self.assertTrue(a.startswith("n-"))
|
||||
self.assertNotEqual(a, fn.network_id("192.168.1.1", "b4:fb:e4:b5:67:56")) # same IP, another router
|
||||
self.assertIsNone(fn.network_id("192.168.1.1", None))
|
||||
self.assertIsNone(fn.network_id(None, "b4:fb:e4:b5:67:55"))
|
||||
|
||||
def test_tailscale(self):
|
||||
ts = fn.parse_tailscale(TAILSCALE)
|
||||
self.assertTrue(ts["up"])
|
||||
self.assertEqual(ts["ip"], "100.101.102.103")
|
||||
self.assertEqual(ts["name"], "laptop.tail1234.ts.net")
|
||||
self.assertEqual(ts["tailnet"], "example.github")
|
||||
frame = ts["peers"][0]
|
||||
self.assertEqual((frame["name"], frame["dns"], frame["os"], frame["online"]),
|
||||
("frame", "frame.tail1234.ts.net", "linux", True))
|
||||
self.assertFalse(fn.parse_tailscale(json.dumps({"BackendState": "Stopped", "Self": {}}))["up"])
|
||||
self.assertEqual(fn.parse_tailscale("not json"), {"up": False, "peers": []})
|
||||
self.assertEqual(fn.parse_tailscale("[]"), {"up": False, "peers": []})
|
||||
|
||||
def test_address_kinds(self):
|
||||
cases = {"frame.local": "mdns", "frame.local.": "mdns", "frame.tail1234.ts.net": "tailscale",
|
||||
"100.113.174.84": "tailscale", "fd7a:115c:a1e0::5928:ae55": "tailscale",
|
||||
"192.168.1.40": "lan", "10.0.0.5": "lan", "fe80::1%en0": "lan",
|
||||
"frame.example.com": "manual", "8.8.8.8": "manual"}
|
||||
for host, kind in cases.items():
|
||||
self.assertEqual(fn.guess_kind(host), kind, host)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user