Repository navigation
Fix Cloud VM creation, snapshot refresh, and desktop restore #12268
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
26 commits
Select commit
Hold shift + click to select a range
a2272c2
fix: announce private VM addresses before Cloud connections
austinywang 830324e
test: remove obsolete IPv4-only desktop listener assertion
austinywang dc66b80
test: preserve VM readiness when one private family is pending
austinywang db67a30
test: exercise unavailable IPv4 and IPv6 announcement sockets
austinywang 2a64eaf
fix: keep Cloud VMs usable when one private family is unavailable
austinywang 5c20070
refactor: isolate Cloud provider refresh entrypoints
austinywang 4d1fe36
test: reproduce competing Cloud snapshot refreshes and early catalog …
austinywang ec0502e
fix: serialize Cloud snapshots and reconnect restored desktop tabs
austinywang c8f4ebc
refactor: isolate same-revision Cloud snapshot comparison
austinywang 9af510d
test: distinguish connection telemetry from Cloud graph conflicts
austinywang bd208c8
fix: keep volatile client diagnostics out of Cloud revision checks
austinywang 44dc5c2
test: reproduce stale session revision after a Cloud delta
austinywang 23f9ff3
test: evaluate cursor mutations before assertion macros
austinywang 2adf75d
fix: advance the mirrored session revision with Cloud deltas
austinywang 192789c
test: preserve Cloud snapshot validity across live terminal resizes
austinywang 35cfc28
fix: separate live terminal geometry from Cloud revision checks
austinywang b5baacb
test: distinguish live PTY titles from revisioned terminal identity
austinywang ecdaed2
fix: treat PTY titles as live Cloud snapshot observations
austinywang 1dd7ee9
test: reject Cloud readiness without any usable address
austinywang 00358ad
test: pass empty address arrays as complete fixtures
austinywang d79f584
test: cover create and restore rollback without private addresses
austinywang 345ffd0
test: assert network diagnostics at the readiness boundary
austinywang dfff157
fix: reject unusable Cloud networks and clear restored browser routes
austinywang 8a67adb
test: cover a Cloud wake whose payload omits its private address
austinywang 2d212f7
fix: report a Cloud wake's network setup without failing the wake
austinywang a5e0dc5
Merge remote-tracking branch 'origin/main' into fix-cloud-network-ann…
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
There are no files selected for viewing
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,55 @@ | ||
| import Foundation | ||
|
|
||
| /// Serializes graph publication for one provider. A forced reader waits for a | ||
| /// pass started after its request; ordinary readers share the active pass. | ||
| @MainActor | ||
| final class CloudProviderRefreshCoordinator { | ||
| private struct Entry { | ||
| let request: UInt64 | ||
| let forced: Bool | ||
| let task: Task<Bool, Never> | ||
| } | ||
|
|
||
| private var inFlight: Entry? | ||
| private var latestRequest: UInt64 = 0 | ||
| private var invalidation: UInt64 = 0 | ||
| private var lifetime: UInt64 = 0 | ||
|
|
||
| func refresh(force: Bool, operation: @escaping @MainActor (Bool) async -> Bool) async -> Bool { | ||
| latestRequest &+= 1 | ||
| let request = latestRequest | ||
| let epoch = lifetime | ||
| while !Task.isCancelled, epoch == lifetime { | ||
| if let entry = inFlight { | ||
| let result = await entry.task.value | ||
| if inFlight?.task == entry.task { inFlight = nil } | ||
| guard !Task.isCancelled, epoch == lifetime else { return false } | ||
| if !force || (entry.forced && entry.request >= request) { return result } | ||
| continue | ||
| } | ||
| let task = Task { @MainActor [weak self] in | ||
| while let self, !Task.isCancelled, epoch == self.lifetime { | ||
| let revision = self.invalidation | ||
| let result = await operation(force) | ||
| guard !Task.isCancelled, epoch == self.lifetime else { return false } | ||
| // Metadata superseded this pass. Readers stay attached to | ||
| // the owner until a pass over the current metadata finishes. | ||
| if revision == self.invalidation { return result } | ||
| } | ||
| return false | ||
| } | ||
| // Covers all forced readers already waiting, so a burst shares | ||
| // one trailing pass instead of issuing one snapshot per waiter. | ||
| inFlight = Entry(request: latestRequest, forced: force, task: task) | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func invalidate() { invalidation &+= 1 } | ||
|
|
||
| func cancel() { | ||
| lifetime &+= 1 | ||
| inFlight?.task.cancel() | ||
| inFlight = nil | ||
| } | ||
| } |
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,62 @@ | ||
| import Foundation | ||
|
|
||
| extension CloudVMState { | ||
| static func == (lhs: CloudVMState, rhs: CloudVMState) -> Bool { | ||
| lhs.hasSameModeledContent(as: rhs) && lhs.document == rhs.document | ||
| } | ||
|
|
||
| /// Clients and live terminal titles and dimensions are observations, not | ||
| /// revisioned resources (client.list and public_terminal_snapshot). Keep | ||
| /// them in exports without treating inspection or resize as a conflict. | ||
| func hasSameRevisionedContent(as other: CloudVMState) -> Bool { | ||
| hasSameModeledContent(as: other, includingLiveTerminalMetadata: false) | ||
| && document.values.filter { $0.key != "clients" } == other.document.values.filter { $0.key != "clients" } | ||
| && document.collections.filter { $0.key != "clients" && $0.key != "terminals" } | ||
| == other.document.collections.filter { $0.key != "clients" && $0.key != "terminals" } | ||
| && hasSameTerminalDocument(as: other) | ||
| } | ||
|
|
||
| private var revisionedTerminals: [CloudVMTerminalState] { | ||
| terminals.map { | ||
| var terminal = $0 | ||
| terminal.title = "" | ||
| terminal.cols = nil | ||
| terminal.rows = nil | ||
| return terminal | ||
| } | ||
| } | ||
|
|
||
| private func hasSameModeledContent(as other: CloudVMState, includingLiveTerminalMetadata: Bool = true) -> Bool { | ||
| let left = includingLiveTerminalMetadata ? terminals : revisionedTerminals | ||
| let right = includingLiveTerminalMetadata ? other.terminals : other.revisionedTerminals | ||
| return machine == other.machine | ||
| && cursor == other.cursor | ||
| && workspaces == other.workspaces | ||
| && screens == other.screens | ||
| && panes == other.panes | ||
| && tabs == other.tabs | ||
| && left == right | ||
| && browsers == other.browsers | ||
| && agents == other.agents | ||
| } | ||
|
|
||
| /// Identity, launch fields, and unknown fields remain strict. Only the PTY | ||
| /// title and dimensions are live; unchanged rows use their byte cache. | ||
| private func hasSameTerminalDocument(as other: CloudVMState) -> Bool { | ||
| guard let left = document.collections["terminals"] else { | ||
| return other.document.collections["terminals"] == nil | ||
| } | ||
| guard let right = other.document.collections["terminals"], left.order == right.order else { return false } | ||
| for id in left.order { | ||
| guard let a = left.rows[id], let b = right.rows[id] else { return false } | ||
| if a == b { continue } | ||
| guard var lhs = try? JSONSerialization.jsonObject(with: a) as? [String: Any], | ||
| var rhs = try? JSONSerialization.jsonObject(with: b) as? [String: Any] else { return false } | ||
| for key in ["title", "cols", "rows"] { lhs[key] = nil; rhs[key] = nil } | ||
| guard let lhsData = try? JSONSerialization.data(withJSONObject: lhs, options: [.sortedKeys]), | ||
| let rhsData = try? JSONSerialization.data(withJSONObject: rhs, options: [.sortedKeys]), | ||
| lhsData == rhsData else { return false } | ||
| } | ||
| return true | ||
| } | ||
| } |
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,23 @@ | ||
| import Foundation | ||
|
|
||
| extension CmuxTuiSurfaceProvider { | ||
| func refresh() async { | ||
| await refreshCurrentGraph(force: false) | ||
| } | ||
|
|
||
| // Matches the protocol's Void return type so existential catalog reads | ||
| // preserve force instead of falling through to its legacy default. | ||
| func refresh(force: Bool) async { | ||
| await refreshCurrentGraph(force: force) | ||
| } | ||
|
|
||
| /// Re-syncs the graph and reports whether the result is authoritative enough | ||
| /// for mutations. Concurrent reads share the provider's refresh owner. | ||
| @discardableResult | ||
| func refreshCurrentGraph(force: Bool) async -> Bool { | ||
| await refreshCoordinator.refresh(force: force) { [weak self] force in | ||
| guard let self else { return false } | ||
| return await self.performRefresh(force: force) | ||
| } | ||
| } | ||
| } |
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.