Fix the follow-up review's findings

- Pairing saves nothing if it was cancelled while adding the key.
- The ssh stand-in runs under bash: dash refuses job control without a
  terminal, which cost stdin and the process group.
- A server that stops while the tunnel opens fails the attempt (and retries)
  instead of leaving it half-connected.
- The health probe answers within its deadline even when a dead link keeps
  the probe itself waiting.
- A cached version is deleted only if no server runs from it (servers now run
  by absolute path) and it's two weeks unused.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-27 11:40:17 +10:00
1 parent a910f83ac1
commit 14790ac768
4 files changed
+31 -18

No files matched your search

+7 -3
View File
@@ -47,6 +47,7 @@ final class AppModel: ObservableObject {
invalidate()
let mine = attempt
await teardown()
guard mine == attempt else { return }
phase = .connecting("Signing in to \(target.host)")
let pin = PinnedHostKey(expected: nil)
do {
@@ -62,6 +63,7 @@ final class AppModel: ObservableObject {
+ "[ -s \(file) ] && [ -n \"$(tail -c 1 \(file))\" ] && printf '\\n' >> \(file); "
+ "printf '%s\\n' \(shellQuote(line)) >> \(file); }; }",
"Couldn't add the key on the Frame")
guard mine == attempt else { return } // cancelled meanwhile: save nothing
guard let seen = pin.seen else { throw FrameFailure("The Frame didn't show a host key") }
UserDefaults.standard.set(seen, forKey: Self.hostKeyKey)
UserDefaults.standard.set(try JSONEncoder().encode(target), forKey: Self.settingsKey)
@@ -168,17 +170,19 @@ final class AppModel: ObservableObject {
let f = try await PortForwarder.start(over: l, to: server.port)
forwarder = f
guard current() else { throw CancellationError() }
if let tail = server.exited { // stopped while the tunnel was opening
throw FrameFailure("Frame Control on the headset stopped. \(tail.suffix(200))")
}
// Only now does this attempt's connection become the app's.
self.link = l
self.server = server
self.forwarder = f
readySince = Date()
// Runs at once if the server already stopped while the tunnel was opening.
phase = .ready(URL(string: "http://127.0.0.1:\(f.localPort)/?key=\(key)")!)
// Runs at once if it stopped in the moment since the check above.
server.whenExited { [weak self] tail in
Task { @MainActor in self?.lost(mine, "Frame Control on the headset stopped. \(tail.suffix(200))") }
}
guard server.exited == nil else { return }
phase = .ready(URL(string: "http://127.0.0.1:\(f.localPort)/?key=\(key)")!)
watchHealth(mine)
} catch {
forwarder?.stop()
+14 -7
View File
@@ -80,15 +80,15 @@ final class FrameLink: @unchecked Sendable {
var isConnected: Bool { client.isConnected }
/// Whether the Frame answers a trivial command within a few seconds.
/// Whether the Frame answers a trivial command within a few seconds. The probe
/// runs unstructured: a dead link can keep it waiting well past the deadline,
/// and the answer mustn't wait for it.
func answers(within seconds: Double = 6) async -> Bool {
guard client.isConnected else { return false }
return await withTaskGroup(of: Bool.self) { group in
group.addTask { (try? await self.run("true").status) == 0 }
group.addTask { try? await Task.sleep(nanoseconds: UInt64(seconds * 1e9)); return false }
let first = await group.next() ?? false
group.cancelAll()
return first
let once = Once()
return await withCheckedContinuation { (c: CheckedContinuation<Bool, Never>) in
Task { let ok = (try? await self.run("true").status) == 0; if once.claim() { c.resume(returning: ok) } }
Task { try? await Task.sleep(nanoseconds: UInt64(seconds * 1e9)); if once.claim() { c.resume(returning: false) } }
}
}
@@ -133,6 +133,13 @@ final class FrameLink: @unchecked Sendable {
}
}
/// True for the first caller only.
final class Once: @unchecked Sendable {
private let lock = NSLock()
private var done = false
func claim() -> Bool { lock.withLock { defer { done = true }; return !done } }
}
func shellQuote(_ s: String) -> String {
"'" + s.replacingOccurrences(of: "'", with: "'\\''") + "'"
}
+8 -6
View File
@@ -69,18 +69,20 @@ final class HeadsetServer: @unchecked Sendable {
try await link.check("rm -rf \(dir).tmp && mkdir \(dir).tmp && tar xzf \(archive) -C \(dir).tmp && rm -f \(archive) "
+ "&& rm -rf \(dir) && mv \(dir).tmp \(dir)", "Couldn't unpack Frame Control on the headset")
}
// Another phone or iPad may be running a different version right now, so only
// versions (and interrupted unpacks) untouched for two weeks go. This one is
// marked as used.
_ = try? await link.run("touch \(dir) && find \(cacheDir) -mindepth 1 -maxdepth 1 -type d ! -name \(bundle.version) "
+ "-mtime +14 -exec rm -rf {} +")
// Another phone or iPad may be running a different version right now: a version
// goes only when no server runs from it and it hasn't been used for two weeks
// (this one is marked as used). Servers run by absolute path, so pgrep sees it.
_ = try? await link.run("touch \(dir) && cd \(cacheDir) && for d in */; do d=${d%/}; "
+ "[ \"$d\" = \(bundle.version) ] && continue; "
+ "[ -n \"$(find \"$d\" -maxdepth 0 -mtime +14)\" ] || continue; "
+ "pgrep -f \"$PWD/$d/\" >/dev/null && continue; rm -rf -- \"$d\"; done")
return dir
}
/// Starts the server in dir and waits for it to say which port it took.
static func start(in dir: String, over link: FrameLink, key: String, device: String) async throws -> HeadsetServer {
let command = "cd \(dir) && FRAME_LOCAL=1 FRAME_UI_KEY=\(key) FRAME_DEVICE=\(shellQuote(device)) "
+ "exec python3 -I -u -B ui/server.py --port 0 --exit-on-eof 2>&1"
+ "exec python3 -I -u -B \"$PWD/ui/server.py\" --port 0 --exit-on-eof 2>&1"
let stream = try await link.client.executeCommandStream(command)
let box = PortWaiter()
let reader = Task { () -> Void in
+2 -2
View File
@@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/bash
# Stand-in for ssh when Frame Control's server runs on the Frame itself
# (FRAME_LOCAL=1, started by the iPhone app). The server and its helpers call
# `ssh [options] frame COMMAND`, and rsync calls it as its transport; here that
@@ -16,7 +16,7 @@ cd "$HOME" || exit 255
# Stopping ssh ends everything the command started (sshd hangs up the session).
# To do the same, run COMMAND as its own process group and pass on a TERM, HUP
# or INT to all of it, so e.g. a live-video ffmpeg can't outlive its stream.
set -m # job control: the background job gets its own process group, and keeps stdin
set -m # job control (bash allows it without a terminal): own process group, and stdin kept
"${SHELL:-/bin/sh}" -c "$*" &
child=$!
trap 'kill -TERM -- "-$child" 2>/dev/null' TERM HUP INT