-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Fix Cloud display ownership and connection readiness #13196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
80 commits
Select commit
Hold shift + click to select a range
de3d294
test: cover Cloud display ownership and readiness gaps
austinywang eb3edd7
Enforce Cloud display provenance and independent guest displays
austinywang fb79df1
Merge origin/main into 13192-cloud-display-ownership
austinywang 2b23bbc
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang 329897a
Keep display creation compatible with baked Cloud images
austinywang d11a13b
Merge origin/main into 13192-cloud-display-ownership
austinywang 0f8e410
Fix guest display target wiring and session supervision
austinywang a7f0e3b
Harden embedded display helper and Dock restore ownership
austinywang 8c5a84b
Merge origin/main into 13192-cloud-display-ownership
austinywang 0a7fad0
Close Cloud display lifecycle gaps
austinywang 87abadc
Harden display discovery and helper restart recovery
austinywang 5db9dc3
Finish Cloud display build and readiness guards
austinywang 21478f4
Merge origin/main into 13192-cloud-display-ownership
austinywang 91d6bdb
Preserve display state and bind guest listeners privately
austinywang 657285f
Invalidate display catalogs when VM state changes
austinywang a440be5
Preserve Dock display duplication identity
austinywang 27a7fe8
Preserve Cloud display refresh and browser locations
austinywang c92eb8a
Run guest display service as the desktop user
austinywang 44e6a17
Align Ghostty submodule with current main
austinywang fe68113
Invalidate terminal Cloud navigation callbacks
austinywang 3aeafaa
Keep guest display ports out of forwarded resources
austinywang 89be278
Complete additional display recovery paths
austinywang 220c027
Synchronize guest profile and slow-route readiness
austinywang d7a3aaf
Finish guest display startup and restore routing
austinywang 49e5899
Keep unresolved display restores retryable
austinywang 26e71ed
Fence guest discovery to provider lifetime
austinywang a6c7dea
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang c0f7bcd
test: cover display transport recovery and duplication state
austinywang 81bd61b
fix: preserve Cloud displays during transport recovery
austinywang 7ebdd4f
test: preserve display identity across browser reconfiguration
austinywang 292fd36
fix: retain display identity across route reconfiguration
austinywang b1f53ae
test: cover route observation after display reconfiguration
austinywang 54fcdfb
fix: retain Cloud restore lifecycle state
austinywang 1c64ed8
test: drop Cloud provenance after external navigation
austinywang 7cda189
fix: clear Cloud provenance on external browser navigation
austinywang 1d18589
test: cover display catalog and readiness cancellation
austinywang fbef08c
fix: fence display catalog and readiness lifecycles
austinywang 3a5ea98
test: cover delayed display restore and scoped helpers
austinywang c3c5c8c
fix: complete display restore and supervisor isolation
austinywang 9928173
fix: require discovered guest display resources
austinywang 0cb8b98
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang a9a948a
test: cover guest component recovery
austinywang bed6163
fix: preserve Cloud provenance and supervise displays
austinywang 30e94d5
test: reject failed display catalog responses
austinywang 10c6557
fix: fence display discovery by response and auth
austinywang 46ce029
test: filter untrusted display restore targets
austinywang 6dba3ec
fix: fence display restore targets and VM kind
austinywang 0c0319f
test: fence browser Cloud service identity
austinywang f58c2c6
test: exercise recovered display supervision
austinywang fff1196
fix: recover display supervisors and port identity
austinywang f96bad4
test: recover scoped display process commands
austinywang e2fb4e3
fix: recover scoped display processes by command
austinywang c6f86de
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang c1cb5d9
test: cover Cloud restore and destination comment fixes
austinywang 178d35e
fix: address Cloud display review comments
austinywang 4795f0f
test: cover display port ownership and recovery
austinywang cbe4863
fix: harden Cloud display supervisor and route lifecycle
austinywang 3d27a16
test: stay within Swift file budget
austinywang f7d469d
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang 92ed22a
fix: sanitize display errors and readiness probes
austinywang 60c550c
fix: restore Cloud resources in Dock scopes
austinywang 3df7c56
fix: recover global Dock projections and daemon readiness
austinywang 4d657c1
fix: preserve Dock connections across Cloud route changes
austinywang 812b4ef
fix: bound display startup and preserve duplicate URLs
austinywang 75fdecb
fix: defer Cloud activation for hidden restores
austinywang 6db7593
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang 42f1ab4
fix: preserve ownership checks across latest main merge
austinywang eb39f9e
fix: remove duplicate projection query declarations
austinywang 23c807b
fix: restore provider display lifecycle after main merge
austinywang d8333af
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang 1a127d2
fix: use merged hostname route API
austinywang 210df4c
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang e18669f
fix: restore New Display hover button after main merge
austinywang 7856fa7
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang 1eda271
fix: keep SurfaceCatalog within its line budget
austinywang 2bc5ca3
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang 21a652e
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang 3d82498
Merge remote-tracking branch 'origin/main' into 13192-cloud-display-o…
austinywang 345e9ef
fix: route cloud desktop clicks through portal
austinywang 019fd05
chore: keep cloud fix within file budgets
austinywang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| import Foundation | ||
| import Observation | ||
|
|
||
| /// Explicit display discovery/creation through the existing VM-authorized exec | ||
| /// route. Routine terminal catalog refreshes never execute guest commands. | ||
| @MainActor | ||
| @Observable | ||
| final class CloudDisplayCoordinator { | ||
| private let execute: @MainActor (String, Int) async throws -> VMExecResult | ||
| private(set) var snapshot: CloudGuestDisplaySnapshot? | ||
| private(set) var lastValidatedSnapshot: CloudGuestDisplaySnapshot? | ||
| private(set) var isAvailable = false | ||
| private var generation: UInt64 = 0 | ||
| private var requestID: UUID? | ||
| private var refreshTask: Task<Void, Never>? | ||
| private var creation: Task<CloudGuestDisplaySnapshot, Error>? | ||
|
|
||
| init(execute: @escaping @MainActor (String, Int) async throws -> VMExecResult) { | ||
| self.execute = execute | ||
| } | ||
|
|
||
| var canCreate: Bool { isAvailable && (snapshot?.canCreate == true || requestID != nil) && creation == nil } | ||
| var displaySnapshot: CloudGuestDisplaySnapshot? { snapshot ?? lastValidatedSnapshot } | ||
|
|
||
| func refresh() async { | ||
| guard creation == nil else { return } | ||
| refreshTask?.cancel() | ||
| generation &+= 1 | ||
| let token = generation | ||
| let task = Task { [weak self, execute] in | ||
| do { | ||
| var response: VMExecResult? | ||
| var lastError: (any Error)? | ||
| for attempt in 0..<3 { | ||
| do { | ||
| let candidate = try await execute(CloudGuestDisplayScript.command(action: "list"), 10_000) | ||
| if candidate.exitCode == 0 { | ||
| response = candidate | ||
| break | ||
| } | ||
| lastError = SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) | ||
| } catch { | ||
| lastError = error | ||
| } | ||
| if attempt < 2 { try await Task.sleep(for: .milliseconds(100)) } | ||
|
austinywang marked this conversation as resolved.
|
||
| } | ||
| guard let response else { throw lastError ?? SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) } | ||
| guard response.exitCode == 0 else { | ||
| throw SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) | ||
| } | ||
| let snapshot = try CloudGuestDisplaySnapshot(data: Data(response.stdout.utf8)) | ||
| guard let self, token == self.generation, !Task.isCancelled else { return } | ||
| self.snapshot = snapshot | ||
| self.lastValidatedSnapshot = snapshot | ||
| self.isAvailable = true | ||
| } catch { | ||
| guard let self, token == self.generation else { return } | ||
| self.snapshot = nil | ||
| self.isAvailable = false | ||
| } | ||
| } | ||
| refreshTask = task | ||
| await task.value | ||
| if refreshTask != nil, token == generation { refreshTask = nil } | ||
| } | ||
|
|
||
| func create() async throws -> CloudGuestDisplaySnapshot { | ||
| if let creation { | ||
| return try await withTaskCancellationHandler { | ||
| try await creation.value | ||
| } onCancel: { | ||
| creation.cancel() | ||
| } | ||
| } | ||
| guard isAvailable, snapshot?.canCreate == true || requestID != nil else { | ||
| throw SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) | ||
| } | ||
| generation &+= 1 | ||
| let token = generation | ||
| let request = requestID ?? UUID() | ||
| requestID = request | ||
| // The UUID is generated here and retained after failures. A retry cannot | ||
| // create a second guest display when the first receipt was lost. | ||
| let task = Task { [weak self, execute] in | ||
| try Task.checkCancellation() | ||
| let response = try await execute(CloudGuestDisplayScript.command(action: "create", requestID: request), 65_000) | ||
| let snapshot = try CloudGuestDisplaySnapshot(data: Data(response.stdout.utf8)) | ||
| try Task.checkCancellation() | ||
| guard let self, self.generation == token else { throw CancellationError() } | ||
| self.snapshot = snapshot | ||
| self.lastValidatedSnapshot = snapshot | ||
| guard response.exitCode == 0, snapshot.error == nil, snapshot.created != nil else { | ||
| throw SurfaceCatalogError.unsupported(String(localized: "cloud.display.creationFailed", defaultValue: "The new display could not start. Refresh Displays, then retry. Existing displays are unchanged.")) | ||
| } | ||
| self.requestID = nil | ||
| return snapshot | ||
| } | ||
| creation = task | ||
| defer { if generation == token { creation = nil } } | ||
| return try await withTaskCancellationHandler { | ||
| try await task.value | ||
| } onCancel: { | ||
| task.cancel() | ||
| } | ||
| } | ||
|
|
||
| func stop() { | ||
| invalidate() | ||
| } | ||
|
|
||
| /// Drops guest state when the provider identity or account scope changes. | ||
| func invalidate() { | ||
| generation &+= 1 | ||
| refreshTask?.cancel() | ||
| refreshTask = nil | ||
| creation?.cancel() | ||
| creation = nil | ||
| snapshot = nil | ||
| lastValidatedSnapshot = nil | ||
| requestID = nil | ||
| isAvailable = false | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import Foundation | ||
|
|
||
| /// One guest-issued display resource with a stable slot and noVNC target. | ||
| struct CloudGuestDisplay: Decodable, Sendable { | ||
| let id: String | ||
| let number: Int | ||
| let port: Int | ||
| let state: SurfaceLifecycle | ||
|
|
||
| func resource(on machine: SurfaceMachineID, address: String?) -> SurfaceResource { | ||
| SurfaceResource( | ||
| id: SurfaceResourceID(machine: machine, kind: .display, key: id), | ||
| title: number == 1 | ||
| ? String(localized: "cloudTree.node.desktop", defaultValue: "Desktop") | ||
| : String(format: String(localized: "cloud.display.numberedTitle", defaultValue: "Desktop %d"), number), | ||
| detail: "noVNC", lifecycle: state, agent: nil, remoteWorkspace: nil, | ||
| port: port, url: address.map { CmuxTuiSurfaceProvider.privateDesktopURL(privateAddress: $0, port: port) } | ||
| ) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.