diff --git a/app/main.js b/app/main.js index b159ac2..5d308f5 100644 --- a/app/main.js +++ b/app/main.js @@ -258,6 +258,29 @@ ipcMain.on("devices:changed", (e, list) => { buildMenu(); }); const activeAlias = () => (devices.find(d => d.active) || {}).alias || FRAME; + +// SSH through the server, so it goes to the headset and address the app is using +// (with its own pinned identity), and refuses when there's none. +function openSsh() { + if (!url) return dialog.showErrorBox("Couldn't open SSH", "Frame Control's server isn't running."); + const body = JSON.stringify({ what: "terminal" }); + const req = http.request(new URL("/api/open", url), { + method: "POST", timeout: 15000, + headers: { "Content-Type": "application/json", "X-Frame-UI": "1", "Content-Length": Buffer.byteLength(body) }, + }, (res) => { + let data = ""; + res.on("data", (c) => { data += c; }); + res.on("end", () => { + if (res.statusCode === 200) return; + let why = `HTTP ${res.statusCode}`; + try { why = JSON.parse(data).error || why; } catch {} + dialog.showErrorBox("Couldn't open SSH", why); + }); + }); + req.on("error", (e) => dialog.showErrorBox("Couldn't open SSH", e.message)); + req.on("timeout", () => req.destroy(new Error("the server didn't answer"))); + req.end(body); +} function showDevices() { if (win && url) win.webContents.executeJavaScript('location.hash = "devices"').catch(() => {}); } @@ -362,7 +385,7 @@ function buildMenu() { label: "Frame", submenu: [ { label: "Set Up Connection…", click: () => setUpConnection() }, - { label: IS_MAC ? "Open SSH in Terminal" : "Open SSH in a Terminal", click: () => runInTerminal(["ssh", activeAlias()]) }, + { label: IS_MAC ? "Open SSH in Terminal" : "Open SSH in a Terminal", click: openSsh }, { type: "separator" }, ...(devices.length > 1 ? [{ label: "Headset", diff --git a/tests/test_link.py b/tests/test_link.py index 090b18b..bcb34aa 100644 --- a/tests/test_link.py +++ b/tests/test_link.py @@ -278,6 +278,8 @@ class Connecting(unittest.TestCase): self.link.config_mtime = None self.link.watch_config() self.assertEqual(len(self.routes), routes) # an install is running: not yet + self.link.connect(["dropped"]) # a reconnect meanwhile keeps the login it started with + self.assertIn("User=steamos", self.routes[-1][1]) running[0] = 0 self.link.watch_config() self.assertIn("User=deck", self.routes[-1][1]) diff --git a/ui/frame_link.py b/ui/frame_link.py index 8ab5350..efb17af 100644 --- a/ui/frame_link.py +++ b/ui/frame_link.py @@ -413,6 +413,7 @@ class Link: if before.get("transient") and not before.get("none") and self.routed is not None: # A bare alias is in use: a headset set up now doesn't take over by itself. self.override = self.override or before["alias"] + self.session_alias = self.session_alias or before["alias"] # and stays on the list if self.reg.sync_from_config(): self.devices_changed() after = self.active_device() @@ -435,9 +436,15 @@ class Link: why = self.describe(reasons) self.last_attempt = now() self.close_master() - with self.route_lock: + with self.work_lock, self.route_lock: gen = self.attempt_gen = self.gen device = self.active_device() + if self.routed and self.routed[0] == device["id"] and self.route_key(device) != self.routed \ + and self.work(): + # Its login changed in ~/.ssh/config while an install runs: reconnect with the + # one it started with; the change applies once it's done (see watch_config). + device = dict(device, user=self.routed[1], port=self.routed[2]) + self.deferred = True if self.route_key(device) != self.routed: # Another headset, or a new user or port: nothing may go on using the old # route, even if this attempt fails.