Skip to content

Commit d24a544

Browse files
beriberikixclaude
andcommitted
fix: resolve SystemExtensionManager deadlock and state persistence issues
- Fix health check timer deadlock by running timer on global queue instead of same queue as performHealthCheck() - Switch SystemExtension DeviceClaimer from systemDomainMask to userDomainMask for proper permissions - Implement state persistence in CLI SystemExtensionManager stub to track claimed devices between commands - Add proper error logging for state directory creation failures Fixes issue where unbind command reported "Device was not claimed by System Extension" due to missing state persistence. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 54fa905 commit d24a544

3 files changed

Lines changed: 54 additions & 10 deletions

File tree

Sources/SystemExtension/IOKit/DeviceClaimer.swift

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public class IOKitDeviceClaimer: DeviceClaimer {
8383
category: "device-claimer"
8484
)
8585

86-
let stateDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .systemDomainMask)
86+
let stateDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
8787
.first?.appendingPathComponent("usbipd-mac")
8888
let statePath = stateDir?.appendingPathComponent("claimed-devices.json").path ?? "/tmp/usbipd-claimed-devices.json"
8989

@@ -111,11 +111,19 @@ public class IOKitDeviceClaimer: DeviceClaimer {
111111

112112
// Create state directory if needed
113113
let stateDir = (stateFilePath as NSString).deletingLastPathComponent
114-
try? FileManager.default.createDirectory(
115-
atPath: stateDir,
116-
withIntermediateDirectories: true,
117-
attributes: nil
118-
)
114+
do {
115+
try FileManager.default.createDirectory(
116+
atPath: stateDir,
117+
withIntermediateDirectories: true,
118+
attributes: nil
119+
)
120+
logger.debug("Created state directory", context: ["stateDir": stateDir])
121+
} catch {
122+
logger.warning("Failed to create state directory", context: [
123+
"stateDir": stateDir,
124+
"error": error.localizedDescription
125+
])
126+
}
119127

120128
logger.info("IOKitDeviceClaimer initialized", context: [
121129
"stateFilePath": stateFilePath,

Sources/USBIPDCore/SystemExtension/SystemExtensionManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ public class SystemExtensionManager {
401401
private func startHealthCheckTimer() {
402402
guard config.healthCheckInterval > 0 else { return }
403403

404-
healthCheckTimer = DispatchSource.makeTimerSource(queue: queue)
404+
healthCheckTimer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
405405
healthCheckTimer?.schedule(
406406
deadline: .now() + .seconds(Int(config.healthCheckInterval)),
407407
repeating: .seconds(Int(config.healthCheckInterval))

Sources/USBIPDCore/SystemExtension/SystemExtensionStubs.swift

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,25 @@ public protocol DeviceClaimer {
1919
/// Stub implementation of IOKit-based device claiming
2020
public class IOKitDeviceClaimer: DeviceClaimer {
2121
private var claimedDevices: [ClaimedDevice] = []
22+
private let stateFilePath: String
2223

23-
public init() {}
24+
public init() {
25+
// Use user Application Support directory for state persistence
26+
let stateDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
27+
.first?.appendingPathComponent("usbipd-mac")
28+
self.stateFilePath = stateDir?.appendingPathComponent("cli-claimed-devices.json").path ?? "/tmp/usbipd-cli-claimed-devices.json"
29+
30+
// Create state directory if needed
31+
let stateDirPath = (stateFilePath as NSString).deletingLastPathComponent
32+
try? FileManager.default.createDirectory(
33+
atPath: stateDirPath,
34+
withIntermediateDirectories: true,
35+
attributes: nil
36+
)
37+
38+
// Restore state on initialization
39+
try? restoreClaimedDevices()
40+
}
2441

2542
public func claimDevice(device: USBDevice) throws -> ClaimedDevice {
2643
let deviceID = "\(device.busID)-\(device.deviceID)"
@@ -43,12 +60,14 @@ public class IOKitDeviceClaimer: DeviceClaimer {
4360
)
4461

4562
claimedDevices.append(claimedDevice)
63+
try saveClaimState()
4664
return claimedDevice
4765
}
4866

4967
public func releaseDevice(device: USBDevice) throws {
5068
let deviceID = "\(device.busID)-\(device.deviceID)"
5169
claimedDevices.removeAll { $0.deviceID == deviceID }
70+
try saveClaimState()
5271
}
5372

5473
public func getAllClaimedDevices() -> [ClaimedDevice] {
@@ -60,11 +79,28 @@ public class IOKitDeviceClaimer: DeviceClaimer {
6079
}
6180

6281
public func saveClaimState() throws {
63-
// Stub implementation - would save to persistent storage
82+
do {
83+
let data = try JSONEncoder().encode(claimedDevices)
84+
try data.write(to: URL(fileURLWithPath: stateFilePath))
85+
} catch {
86+
// Don't fail the operation, just log the error
87+
print("Warning: Failed to save device claim state: \(error.localizedDescription)")
88+
}
6489
}
6590

6691
public func restoreClaimedDevices() throws {
67-
// Stub implementation - would restore from persistent storage
92+
guard FileManager.default.fileExists(atPath: stateFilePath) else {
93+
return // No state file, start with empty list
94+
}
95+
96+
do {
97+
let data = try Data(contentsOf: URL(fileURLWithPath: stateFilePath))
98+
claimedDevices = try JSONDecoder().decode([ClaimedDevice].self, from: data)
99+
} catch {
100+
// Don't fail the operation, just log the error
101+
print("Warning: Failed to restore device claim state: \(error.localizedDescription)")
102+
claimedDevices = []
103+
}
68104
}
69105
}
70106

0 commit comments

Comments
 (0)