-
Notifications
You must be signed in to change notification settings - Fork 1
fix(usb): suppress reattach prompt after intentional detach #22
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
Changes from all commits
b496730
5d8ed3a
16dc69e
353ba90
49f35aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,15 @@ import Foundation | |
|
|
||
| private enum USBPassthroughPolicy { | ||
| static let attachFailureSuppressionInterval: TimeInterval = 10 | ||
| static let intentionalDisconnectExpectationInterval: TimeInterval = 10 | ||
| static let intentionalReenumerationInterval: TimeInterval = 3 | ||
| } | ||
|
|
||
| private struct ExpectedAccessoryReenumeration { | ||
| let registryID: UInt64 | ||
| let deviceDescriptorHash: String | ||
| let disconnectDeadline: Date | ||
| var reconnectDeadline: Date? | ||
| } | ||
|
|
||
| @MainActor | ||
|
|
@@ -27,9 +36,9 @@ final class USBAccessoryCoordinator { | |
| private var pendingAttachAccessoryID: UInt64? | ||
| private var pendingAttachToken: UUID? | ||
| private var lastAccessoryEventByDescriptor: [String: (kind: String, date: Date)] = [:] | ||
| private var lastAttachAttemptByDescriptor: [String: Date] = [:] | ||
| private var attachSuppressedUntilByDescriptor: [String: Date] = [:] | ||
| private var reconnectDescriptorKey: String? | ||
| private var expectedAccessoryReenumeration: ExpectedAccessoryReenumeration? | ||
| private var announcedAccessoryIDs: Set<UInt64> = [] | ||
| private var isIntentionalVMStopInProgress = false | ||
| private var isRegistrationPending = false | ||
|
|
@@ -204,6 +213,7 @@ final class USBAccessoryCoordinator { | |
| accessories.removeAll() | ||
| selectedAccessoryID = nil | ||
| reconnectDescriptorKey = nil | ||
| expectedAccessoryReenumeration = nil | ||
| announcedAccessoryIDs.removeAll() | ||
| notifyStateChanged() | ||
|
|
||
|
|
@@ -269,7 +279,22 @@ final class USBAccessoryCoordinator { | |
| } | ||
| } | ||
|
|
||
| func prepareForIntentionalVMStop() { | ||
| func prepareForIntentionalVMStop( | ||
| suppressReenumerationPrompt: Bool = true | ||
| ) { | ||
| if suppressReenumerationPrompt, | ||
| let attachedAccessoryID, | ||
| let record = accessories.first(where: { $0.id == attachedAccessoryID }) { | ||
| expectedAccessoryReenumeration = ExpectedAccessoryReenumeration( | ||
| registryID: attachedAccessoryID, | ||
| deviceDescriptorHash: record.deviceDescriptorHash, | ||
| disconnectDeadline: Date().addingTimeInterval( | ||
| USBPassthroughPolicy.intentionalDisconnectExpectationInterval | ||
| ) | ||
| ) | ||
|
Afcoo marked this conversation as resolved.
|
||
| } else if !suppressReenumerationPrompt { | ||
| expectedAccessoryReenumeration = nil | ||
| } | ||
| isIntentionalVMStopInProgress = true | ||
| reportEventLog( | ||
| "Marked USB passthrough teardown as an intentional VM stop.", | ||
|
|
@@ -282,7 +307,6 @@ final class USBAccessoryCoordinator { | |
| attachedDevice = nil | ||
| pendingAttachAccessoryID = nil | ||
| pendingAttachToken = nil | ||
| lastAttachAttemptByDescriptor.removeAll() | ||
| attachSuppressedUntilByDescriptor.removeAll() | ||
| reconnectDescriptorKey = nil | ||
| vmSessionAccessoryID = nil | ||
|
|
@@ -440,7 +464,6 @@ final class USBAccessoryCoordinator { | |
| completion: ((Bool) -> Void)? | ||
| ) { | ||
| let registryID = accessory.registryID | ||
| let descriptorKey = record.descriptorIdentityKey | ||
|
|
||
| if let vmSessionAccessoryID { | ||
| onStatusMessage?(String(localized: "Detach the current USB accessory before attaching another USB accessory.")) | ||
|
|
@@ -480,7 +503,6 @@ final class USBAccessoryCoordinator { | |
| let attachToken = UUID() | ||
| pendingAttachAccessoryID = registryID | ||
| pendingAttachToken = attachToken | ||
| lastAttachAttemptByDescriptor[descriptorKey] = Date() | ||
| notifyStateChanged() | ||
| reportEventLog( | ||
| "USB attach details: \(record.descriptorDiagnosticText), registry " + | ||
|
|
@@ -610,6 +632,16 @@ final class USBAccessoryCoordinator { | |
| level: .debug | ||
| ) | ||
|
|
||
| if consumeExpectedReenumerationIfMatching(record) { | ||
| _ = announcedAccessoryIDs.insert(record.id) | ||
| reportEventLog( | ||
| "USB accessory returned after intentional passthrough release; " + | ||
| "automatic attach prompt suppressed for registry " + | ||
| "\(record.registryIDText).", | ||
| level: .debug | ||
| ) | ||
| } | ||
|
|
||
| let becameReady = previousRecord?.hasConfigurationDescriptor != true && record.hasConfigurationDescriptor | ||
| let shouldAnnounce = becameReady | ||
| && attachedAccessoryID != record.id | ||
|
|
@@ -621,11 +653,24 @@ final class USBAccessoryCoordinator { | |
| } | ||
|
|
||
| private func removeAccessory(_ accessory: AAUSBAccessory) { | ||
| let record = USBAccessoryRecord(accessory: accessory) | ||
| let record = accessories.first { $0.id == accessory.registryID } | ||
| ?? USBAccessoryRecord(accessory: accessory) | ||
| let wasSelected = selectedAccessoryID == accessory.registryID | ||
| let wasAttached = attachedAccessoryID == accessory.registryID | ||
| let wasPendingAttach = pendingAttachAccessoryID == accessory.registryID | ||
|
|
||
| if let expectedAccessoryReenumeration, | ||
| expectedAccessoryReenumeration.registryID == accessory.registryID { | ||
| let now = Date() | ||
| if expectedAccessoryReenumeration.disconnectDeadline > now { | ||
| self.expectedAccessoryReenumeration?.reconnectDeadline = now.addingTimeInterval( | ||
| USBPassthroughPolicy.intentionalReenumerationInterval | ||
| ) | ||
| } else { | ||
| self.expectedAccessoryReenumeration = nil | ||
| } | ||
| } | ||
|
|
||
| accessoryObjects[accessory.registryID] = nil | ||
| accessories.removeAll { $0.id == accessory.registryID } | ||
| announcedAccessoryIDs.remove(accessory.registryID) | ||
|
|
@@ -688,6 +733,35 @@ final class USBAccessoryCoordinator { | |
| } | ||
| } | ||
|
|
||
| private func consumeExpectedReenumerationIfMatching( | ||
| _ record: USBAccessoryRecord | ||
| ) -> Bool { | ||
| guard let expectedAccessoryReenumeration else { | ||
| return false | ||
| } | ||
|
|
||
| let now = Date() | ||
| guard let reconnectDeadline = expectedAccessoryReenumeration.reconnectDeadline else { | ||
| if expectedAccessoryReenumeration.disconnectDeadline <= now { | ||
| self.expectedAccessoryReenumeration = nil | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| guard reconnectDeadline > now else { | ||
| self.expectedAccessoryReenumeration = nil | ||
| return false | ||
| } | ||
|
|
||
| guard expectedAccessoryReenumeration.deviceDescriptorHash | ||
| == record.deviceDescriptorHash else { | ||
|
Comment on lines
+756
to
+757
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When another unit of the same USB model connects during the three-second re-enumeration window, its device descriptor is typically byte-identical because the descriptor contains only the serial-string index, not the unit's serial value. This comparison therefore consumes the old device's expectation and inserts the new registry ID into AGENTS.md reference: AGENTS.md:L203-L208 Useful? React with 👍 / 👎. |
||
| return false | ||
| } | ||
|
|
||
| self.expectedAccessoryReenumeration = nil | ||
| return true | ||
| } | ||
|
|
||
| private func attachSuppressionRemaining(for record: USBAccessoryRecord) -> TimeInterval? { | ||
| guard let suppressedUntil = attachSuppressedUntilByDescriptor[record.descriptorIdentityKey] else { | ||
| return nil | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.