Skip to content

Commit 37b09b2

Browse files
Fix: Multitouch stability during rapid system changes (#113)
* Fix: Multitouch stability during rapid system changes * Update MiddleDrag/Managers/DeviceMonitor.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Karan Mohindroo <96403086+NullPointerDepressiveDisorder@users.noreply.github.com> * Update MiddleDrag/MiddleDragTests/MultitouchManagerTests.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Karan Mohindroo <96403086+NullPointerDepressiveDisorder@users.noreply.github.com> --------- Signed-off-by: Karan Mohindroo <96403086+NullPointerDepressiveDisorder@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 814fc8c commit 37b09b2

4 files changed

Lines changed: 26 additions & 27 deletions

File tree

MiddleDrag/Managers/DeviceMonitor.swift

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ import os
5454
}
5555
unsafe os_unfair_lock_unlock(&gCallbackLock)
5656

57+
// Additional safety check: Verify the monitor instance is still valid and running.
58+
unsafe monitor.stateLock.lock()
59+
let monitorIsRunning = unsafe monitor.isRunning
60+
unsafe monitor.stateLock.unlock()
61+
62+
guard monitorIsRunning else { return 0 }
63+
5764
#if DEBUG
5865
unsafe touchCount += 1
5966
// Log sparingly to avoid performance impact
@@ -90,11 +97,7 @@ class DeviceMonitor: TouchDeviceProviding {
9097
/// Delay between unregistering callbacks and stopping devices.
9198
/// This allows the MultitouchSupport framework's internal thread (mt_ThreadedMTEntry)
9299
/// to complete any in-flight callback processing before we stop devices.
93-
/// Value determined empirically: 500ms is sufficient to avoid CFRelease(NULL) crashes
94-
/// and EXC_BREAKPOINT exceptions during rapid connectivity changes (wifi ↔ none).
95-
/// Increased from 100ms to handle rapid connectivity toggling that triggers multiple
96-
/// restart cycles in quick succession.
97-
static let frameworkCleanupDelay: TimeInterval = 0.5
100+
static let frameworkCleanupDelay: TimeInterval = 1.0
98101

99102
// MARK: - Properties
100103

@@ -103,10 +106,10 @@ class DeviceMonitor: TouchDeviceProviding {
103106

104107
nonisolated(unsafe) private var device: MTDeviceRef?
105108
nonisolated(unsafe) private var registeredDevices: Set<UnsafeMutableRawPointer> = unsafe []
106-
private var isRunning = false
109+
fileprivate var isRunning = false
107110

108111
/// Lock to protect concurrent access to device state during stop/start operations
109-
private let stateLock = NSLock()
112+
fileprivate let stateLock = NSLock()
110113

111114
/// Tracks whether this instance owns the global callback reference
112115
private var ownsGlobalReference = false

MiddleDrag/Managers/MultitouchManager.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,14 @@ public final class MultitouchManager: @unchecked Sendable {
1111
/// Delay after stopping before restarting devices during wake-from-sleep.
1212
/// This allows the MultitouchSupport framework's internal thread (mt_ThreadedMTEntry)
1313
/// to fully complete cleanup before we start new devices.
14-
/// Value determined empirically: 500ms is sufficient to avoid CFRelease(NULL) crashes
15-
/// and EXC_BREAKPOINT exceptions during rapid connectivity changes (wifi ↔ none).
16-
/// Increased from 250ms to handle rapid connectivity toggling that triggers multiple
17-
/// restart cycles in quick succession.
18-
static let restartCleanupDelay: TimeInterval = 0.5
14+
static let restartCleanupDelay: TimeInterval = 1.0
1915

2016
/// Minimum delay between restart operations to prevent race conditions.
2117
/// When multiple restart triggers occur in rapid succession (e.g., rapid connectivity
2218
/// changes wifi ↔ none), we debounce them by waiting at least this long after the
2319
/// last restart completed. This prevents overlapping restart attempts that can expose
2420
/// race conditions in the MultitouchSupport framework's internal thread.
25-
static let minimumRestartInterval: TimeInterval = 0.6
21+
static let minimumRestartInterval: TimeInterval = 2.5
2622

2723
/// Initial interval between polling attempts when no multitouch device is found at launch.
2824
/// This handles Bluetooth trackpads that connect after login (common during boot).

MiddleDrag/MiddleDragTests/DeviceMonitorTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ import XCTest
267267
DeviceMonitor.frameworkCleanupDelay, 0,
268268
"frameworkCleanupDelay should be positive")
269269
unsafe XCTAssertLessThanOrEqual(
270-
DeviceMonitor.frameworkCleanupDelay, 0.5,
270+
DeviceMonitor.frameworkCleanupDelay, 1.0,
271271
"frameworkCleanupDelay should not be excessive")
272272
}
273273

MiddleDrag/MiddleDragTests/MultitouchManagerTests.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ final class MultitouchManagerTests: XCTestCase {
212212
) {
213213
expectation.fulfill()
214214
}
215-
wait(for: [expectation], timeout: 1.0)
215+
wait(for: [expectation], timeout: 2.0)
216216

217217
XCTAssertFalse(manager.isMonitoring)
218218
XCTAssertFalse(manager.isEnabled)
@@ -303,7 +303,7 @@ final class MultitouchManagerTests: XCTestCase {
303303
) {
304304
expectation.fulfill()
305305
}
306-
wait(for: [expectation], timeout: 1.0)
306+
wait(for: [expectation], timeout: 3.0)
307307

308308
// We expect:
309309
// 1 initial creation
@@ -339,7 +339,7 @@ final class MultitouchManagerTests: XCTestCase {
339339
) {
340340
expectation.fulfill()
341341
}
342-
wait(for: [expectation], timeout: 1.0)
342+
wait(for: [expectation], timeout: 3.0)
343343

344344
// Verify manager is still stopped
345345
XCTAssertFalse(manager.isMonitoring, "Manager should remain stopped")
@@ -1688,7 +1688,7 @@ final class MultitouchManagerTests: XCTestCase {
16881688
) {
16891689
expectation.fulfill()
16901690
}
1691-
wait(for: [expectation], timeout: 1.0)
1691+
wait(for: [expectation], timeout: 3.0)
16921692

16931693
// After restart, monitoring should still be active
16941694
XCTAssertTrue(manager.isMonitoring)
@@ -1722,7 +1722,7 @@ final class MultitouchManagerTests: XCTestCase {
17221722
) {
17231723
expectation.fulfill()
17241724
}
1725-
wait(for: [expectation], timeout: 1.0)
1725+
wait(for: [expectation], timeout: 3.0)
17261726

17271727
XCTAssertFalse(manager.isEnabled)
17281728
XCTAssertTrue(manager.isMonitoring)
@@ -1767,7 +1767,7 @@ final class MultitouchManagerTests: XCTestCase {
17671767
) {
17681768
restartExpectation.fulfill()
17691769
}
1770-
wait(for: [restartExpectation], timeout: 1.0)
1770+
wait(for: [restartExpectation], timeout: 3.0)
17711771

17721772
XCTAssertTrue(manager.isMonitoring)
17731773

@@ -1806,7 +1806,7 @@ final class MultitouchManagerTests: XCTestCase {
18061806
) {
18071807
expectation.fulfill()
18081808
}
1809-
wait(for: [expectation], timeout: 1.0)
1809+
wait(for: [expectation], timeout: 3.0)
18101810

18111811
unsafe XCTAssertEqual(mockDevice.startCallCount, 2) // Now restarted
18121812

@@ -1819,7 +1819,7 @@ final class MultitouchManagerTests: XCTestCase {
18191819
MultitouchManager.restartCleanupDelay, 0,
18201820
"restartCleanupDelay should be positive")
18211821
XCTAssertLessThanOrEqual(
1822-
MultitouchManager.restartCleanupDelay, 0.5,
1822+
MultitouchManager.restartCleanupDelay, 1.0,
18231823
"restartCleanupDelay should not be excessive")
18241824
}
18251825

@@ -1829,7 +1829,7 @@ final class MultitouchManagerTests: XCTestCase {
18291829
MultitouchManager.minimumRestartInterval, 0,
18301830
"minimumRestartInterval should be positive")
18311831
XCTAssertLessThanOrEqual(
1832-
MultitouchManager.minimumRestartInterval, 1.0,
1832+
MultitouchManager.minimumRestartInterval, 2.5,
18331833
"minimumRestartInterval should not be excessive")
18341834
}
18351835

@@ -1863,7 +1863,7 @@ final class MultitouchManagerTests: XCTestCase {
18631863
) {
18641864
expectation.fulfill()
18651865
}
1866-
wait(for: [expectation], timeout: 2.0)
1866+
wait(for: [expectation], timeout: 5.0)
18671867

18681868
// We expect far fewer than 10 device creations due to:
18691869
// 1. The first restart being in progress blocks subsequent ones
@@ -1903,7 +1903,7 @@ final class MultitouchManagerTests: XCTestCase {
19031903
) {
19041904
expectation.fulfill()
19051905
}
1906-
wait(for: [expectation], timeout: 1.0)
1906+
wait(for: [expectation], timeout: 3.0)
19071907

19081908
// Only 2 device creations: initial start + one restart
19091909
// The duplicate restart calls while in progress should have been skipped
@@ -1933,7 +1933,7 @@ final class MultitouchManagerTests: XCTestCase {
19331933
) {
19341934
expectation.fulfill()
19351935
}
1936-
wait(for: [expectation], timeout: 3.0)
1936+
wait(for: [expectation], timeout: 5.0)
19371937

19381938
// Should still be monitoring after all restarts
19391939
XCTAssertTrue(manager.isMonitoring)
@@ -1980,7 +1980,7 @@ final class MultitouchManagerTests: XCTestCase {
19801980
) {
19811981
restartExpectation.fulfill()
19821982
}
1983-
wait(for: [restartExpectation], timeout: 1.0)
1983+
wait(for: [restartExpectation], timeout: 3.0)
19841984

19851985
XCTAssertTrue(manager.isMonitoring)
19861986

0 commit comments

Comments
 (0)