mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 08:00:32 +02:00
iPhone app: test pairing and power against a Holo Core stand-in
Valve publishes no Steam Frame OS image, so tests/frame-container builds the Frame's SSH surface on Valve and Collabora's Holo Core aarch64 base: a steamos user with a password and sudo, OpenSSH with keys and passwords, Python, and a systemctl that only records requests. Against it from the Simulator: password pairing, the host-key pin, the power password check. Found and fixed: a changed host key or a refused login said "Can't reach the Frame" and retried forever; they now say "Pair with the Frame again" and offer that. The server's key rejection now says the header may be wrong, not only missing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
db75d1ca71
commit
aafd2dbda8
8 files changed
+78
-14
No files matched your search
@@ -70,7 +70,9 @@ final class AppModel: ObservableObject {
|
||||
settings = target
|
||||
} catch {
|
||||
guard mine == attempt else { return }
|
||||
fail((error as? FrameFailure)?.message ?? FrameLink.describe(error, host: target.host), retry: false)
|
||||
let failure = error as? FrameFailure
|
||||
fail(failure?.message ?? FrameLink.describe(error, host: target.host), retry: false,
|
||||
needsPairing: failure?.needsPairing ?? false)
|
||||
return
|
||||
}
|
||||
await connect()
|
||||
@@ -197,11 +199,17 @@ final class AppModel: ObservableObject {
|
||||
forwarder?.stop()
|
||||
if let link { await link.close() } // ends its server too
|
||||
guard current(), !(error is CancellationError) else { return }
|
||||
fail((error as? FrameFailure)?.message ?? FrameLink.describe(error, host: settings.host), retry: true)
|
||||
let failure = error as? FrameFailure
|
||||
fail(failure?.message ?? FrameLink.describe(error, host: settings.host), retry: !(failure?.needsPairing ?? false),
|
||||
needsPairing: failure?.needsPairing ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
private func fail(_ message: String, retry: Bool) {
|
||||
/// Whether the last failure needs the user to pair again rather than wait.
|
||||
@Published private(set) var needsPairing = false
|
||||
|
||||
private func fail(_ message: String, retry: Bool, needsPairing: Bool = false) {
|
||||
self.needsPairing = needsPairing
|
||||
phase = .failed(message)
|
||||
retrying = retry && settings != nil
|
||||
guard retrying else { return }
|
||||
|
||||
@@ -14,6 +14,11 @@ struct FrameControlApp: App {
|
||||
// and connect to FRAME_TEST_HOST with it (`simctl launch` passes
|
||||
// SIMCTL_CHILD_FRAME_TEST_HOST through as FRAME_TEST_HOST).
|
||||
print("FRAME_CONTROL_KEY: \(model.authorizedKeysLine)")
|
||||
// FRAME_TEST_PAIR="host|user|password" runs the real password pairing.
|
||||
if model.settings == nil, let pair = ProcessInfo.processInfo.environment["FRAME_TEST_PAIR"] {
|
||||
let f = pair.components(separatedBy: "|")
|
||||
if f.count == 3 { await model.pair(host: f[0], user: f[1], password: f[2]); return }
|
||||
}
|
||||
if model.settings == nil, let host = ProcessInfo.processInfo.environment["FRAME_TEST_HOST"] {
|
||||
await model.useKey(host: host, user: "steamos")
|
||||
return
|
||||
|
||||
@@ -13,7 +13,12 @@ struct FrameSettings: Codable, Equatable {
|
||||
|
||||
struct FrameFailure: LocalizedError {
|
||||
let message: String
|
||||
init(_ message: String) { self.message = message }
|
||||
/// Retrying can't help: the Frame's identity changed, or it refused this phone's login.
|
||||
var needsPairing = false
|
||||
init(_ message: String, needsPairing: Bool = false) {
|
||||
self.message = message
|
||||
self.needsPairing = needsPairing
|
||||
}
|
||||
var errorDescription: String? { message }
|
||||
}
|
||||
|
||||
@@ -52,7 +57,9 @@ final class FrameLink: @unchecked Sendable {
|
||||
hostKeyValidator: .custom(hostKey), reconnect: .never, connectTimeout: .seconds(8))
|
||||
return FrameLink(client: client)
|
||||
} catch {
|
||||
throw FrameFailure(describe(error, host: settings.host))
|
||||
let text = String(describing: error)
|
||||
throw FrameFailure(describe(error, host: settings.host),
|
||||
needsPairing: error is PinnedHostKey.Changed || text.contains("allAuthenticationOptionsFailed"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ struct RootView: View {
|
||||
case .connecting(let step):
|
||||
ConnectingView(step: step, host: model.settings?.host) { model.showSetup() }
|
||||
case .failed(let message):
|
||||
FailedView(message: message, canRetry: model.settings != nil, retrying: model.retrying,
|
||||
FailedView(message: message, canRetry: model.settings != nil, retrying: model.retrying, needsPairing: model.needsPairing,
|
||||
retry: { Task { await model.connect() } }, change: { model.showSetup() })
|
||||
case .ready(let url):
|
||||
WebShell(url: url, model: model).ignoresSafeArea()
|
||||
@@ -51,19 +51,23 @@ struct FailedView: View {
|
||||
let message: String
|
||||
let canRetry: Bool
|
||||
let retrying: Bool
|
||||
let needsPairing: Bool
|
||||
let retry: () -> Void
|
||||
let change: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "wifi.exclamationmark").font(.system(size: 44)).foregroundStyle(.orange)
|
||||
Text("Can't reach the Frame").font(.title3.bold())
|
||||
Image(systemName: needsPairing ? "lock.trianglebadge.exclamationmark" : "wifi.exclamationmark")
|
||||
.font(.system(size: 44)).foregroundStyle(.orange)
|
||||
Text(needsPairing ? "Pair with the Frame again" : "Can't reach the Frame").font(.title3.bold())
|
||||
Text(message).multilineTextAlignment(.center).foregroundStyle(Color.frameMuted)
|
||||
if retrying { Text("Trying again every few seconds.").font(.footnote).foregroundStyle(Color.frameMuted) }
|
||||
if canRetry {
|
||||
if needsPairing {
|
||||
Button("Pair again", action: change).buttonStyle(.borderedProminent).controlSize(.large)
|
||||
} else if canRetry {
|
||||
Button("Try again", action: retry).buttonStyle(.borderedProminent).controlSize(.large)
|
||||
}
|
||||
Button(canRetry ? "Change headset" : "Back", action: change)
|
||||
if !needsPairing { Button(canRetry ? "Change headset" : "Back", action: change) }
|
||||
}
|
||||
.padding(32)
|
||||
.frame(maxWidth: 480)
|
||||
|
||||
Reference in new issue
Block a user