From 0cb573694cd7cdf0273c925e4e6149b411d6edc3 Mon Sep 17 00:00:00 2001 From: NullPointerDepressiveDisorder <96403086+NullPointerDepressiveDisorder@users.noreply.github.com> Date: Fri, 20 Feb 2026 23:50:45 -0800 Subject: [PATCH 1/3] Fix: Multitouch stability during rapid system changes --- MiddleDrag/Managers/DeviceMonitor.swift | 17 +++++++----- MiddleDrag/Managers/MultitouchManager.swift | 8 ++---- .../MiddleDragTests/DeviceMonitorTests.swift | 2 +- .../MultitouchManagerTests.swift | 26 +++++++++---------- 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/MiddleDrag/Managers/DeviceMonitor.swift b/MiddleDrag/Managers/DeviceMonitor.swift index 9d15c4b..ebc25de 100644 --- a/MiddleDrag/Managers/DeviceMonitor.swift +++ b/MiddleDrag/Managers/DeviceMonitor.swift @@ -54,6 +54,13 @@ import os } unsafe os_unfair_lock_unlock(&gCallbackLock) + // Aditional safety check: Verify the monitor instace is still valid and running. + unsafe monitor.stateLock.lock() + let monitorIsRunning = unsafe monitor.isRunning + unsafe monitor.stateLock.unlock() + + guard monitorIsRunning else { return 0 } + #if DEBUG unsafe touchCount += 1 // Log sparingly to avoid performance impact @@ -90,11 +97,7 @@ class DeviceMonitor: TouchDeviceProviding { /// Delay between unregistering callbacks and stopping devices. /// This allows the MultitouchSupport framework's internal thread (mt_ThreadedMTEntry) /// to complete any in-flight callback processing before we stop devices. - /// Value determined empirically: 500ms is sufficient to avoid CFRelease(NULL) crashes - /// and EXC_BREAKPOINT exceptions during rapid connectivity changes (wifi ↔ none). - /// Increased from 100ms to handle rapid connectivity toggling that triggers multiple - /// restart cycles in quick succession. - static let frameworkCleanupDelay: TimeInterval = 0.5 + static let frameworkCleanupDelay: TimeInterval = 1.0 // MARK: - Properties @@ -103,10 +106,10 @@ class DeviceMonitor: TouchDeviceProviding { nonisolated(unsafe) private var device: MTDeviceRef? nonisolated(unsafe) private var registeredDevices: Set = unsafe [] - private var isRunning = false + fileprivate var isRunning = false /// Lock to protect concurrent access to device state during stop/start operations - private let stateLock = NSLock() + fileprivate let stateLock = NSLock() /// Tracks whether this instance owns the global callback reference private var ownsGlobalReference = false diff --git a/MiddleDrag/Managers/MultitouchManager.swift b/MiddleDrag/Managers/MultitouchManager.swift index fbade63..594103e 100644 --- a/MiddleDrag/Managers/MultitouchManager.swift +++ b/MiddleDrag/Managers/MultitouchManager.swift @@ -11,18 +11,14 @@ public final class MultitouchManager: @unchecked Sendable { /// Delay after stopping before restarting devices during wake-from-sleep. /// This allows the MultitouchSupport framework's internal thread (mt_ThreadedMTEntry) /// to fully complete cleanup before we start new devices. - /// Value determined empirically: 500ms is sufficient to avoid CFRelease(NULL) crashes - /// and EXC_BREAKPOINT exceptions during rapid connectivity changes (wifi ↔ none). - /// Increased from 250ms to handle rapid connectivity toggling that triggers multiple - /// restart cycles in quick succession. - static let restartCleanupDelay: TimeInterval = 0.5 + static let restartCleanupDelay: TimeInterval = 1.0 /// Minimum delay between restart operations to prevent race conditions. /// When multiple restart triggers occur in rapid succession (e.g., rapid connectivity /// changes wifi ↔ none), we debounce them by waiting at least this long after the /// last restart completed. This prevents overlapping restart attempts that can expose /// race conditions in the MultitouchSupport framework's internal thread. - static let minimumRestartInterval: TimeInterval = 0.6 + static let minimumRestartInterval: TimeInterval = 2.5 /// Initial interval between polling attempts when no multitouch device is found at launch. /// This handles Bluetooth trackpads that connect after login (common during boot). diff --git a/MiddleDrag/MiddleDragTests/DeviceMonitorTests.swift b/MiddleDrag/MiddleDragTests/DeviceMonitorTests.swift index 096aac2..68af990 100644 --- a/MiddleDrag/MiddleDragTests/DeviceMonitorTests.swift +++ b/MiddleDrag/MiddleDragTests/DeviceMonitorTests.swift @@ -267,7 +267,7 @@ import XCTest DeviceMonitor.frameworkCleanupDelay, 0, "frameworkCleanupDelay should be positive") unsafe XCTAssertLessThanOrEqual( - DeviceMonitor.frameworkCleanupDelay, 0.5, + DeviceMonitor.frameworkCleanupDelay, 1.0, "frameworkCleanupDelay should not be excessive") } diff --git a/MiddleDrag/MiddleDragTests/MultitouchManagerTests.swift b/MiddleDrag/MiddleDragTests/MultitouchManagerTests.swift index cf718e4..132d544 100644 --- a/MiddleDrag/MiddleDragTests/MultitouchManagerTests.swift +++ b/MiddleDrag/MiddleDragTests/MultitouchManagerTests.swift @@ -212,7 +212,7 @@ final class MultitouchManagerTests: XCTestCase { ) { expectation.fulfill() } - wait(for: [expectation], timeout: 1.0) + wait(for: [expectation], timeout: 3.0) XCTAssertFalse(manager.isMonitoring) XCTAssertFalse(manager.isEnabled) @@ -303,7 +303,7 @@ final class MultitouchManagerTests: XCTestCase { ) { expectation.fulfill() } - wait(for: [expectation], timeout: 1.0) + wait(for: [expectation], timeout: 3.0) // We expect: // 1 initial creation @@ -339,7 +339,7 @@ final class MultitouchManagerTests: XCTestCase { ) { expectation.fulfill() } - wait(for: [expectation], timeout: 1.0) + wait(for: [expectation], timeout: 3.0) // Verify manager is still stopped XCTAssertFalse(manager.isMonitoring, "Manager should remain stopped") @@ -1688,7 +1688,7 @@ final class MultitouchManagerTests: XCTestCase { ) { expectation.fulfill() } - wait(for: [expectation], timeout: 1.0) + wait(for: [expectation], timeout: 3.0) // After restart, monitoring should still be active XCTAssertTrue(manager.isMonitoring) @@ -1722,7 +1722,7 @@ final class MultitouchManagerTests: XCTestCase { ) { expectation.fulfill() } - wait(for: [expectation], timeout: 1.0) + wait(for: [expectation], timeout: 3.0) XCTAssertFalse(manager.isEnabled) XCTAssertTrue(manager.isMonitoring) @@ -1767,7 +1767,7 @@ final class MultitouchManagerTests: XCTestCase { ) { restartExpectation.fulfill() } - wait(for: [restartExpectation], timeout: 1.0) + wait(for: [restartExpectation], timeout: 3.0) XCTAssertTrue(manager.isMonitoring) @@ -1806,7 +1806,7 @@ final class MultitouchManagerTests: XCTestCase { ) { expectation.fulfill() } - wait(for: [expectation], timeout: 1.0) + wait(for: [expectation], timeout: 3.0) unsafe XCTAssertEqual(mockDevice.startCallCount, 2) // Now restarted @@ -1819,7 +1819,7 @@ final class MultitouchManagerTests: XCTestCase { MultitouchManager.restartCleanupDelay, 0, "restartCleanupDelay should be positive") XCTAssertLessThanOrEqual( - MultitouchManager.restartCleanupDelay, 0.5, + MultitouchManager.restartCleanupDelay, 1.0, "restartCleanupDelay should not be excessive") } @@ -1829,7 +1829,7 @@ final class MultitouchManagerTests: XCTestCase { MultitouchManager.minimumRestartInterval, 0, "minimumRestartInterval should be positive") XCTAssertLessThanOrEqual( - MultitouchManager.minimumRestartInterval, 1.0, + MultitouchManager.minimumRestartInterval, 2.5, "minimumRestartInterval should not be excessive") } @@ -1863,7 +1863,7 @@ final class MultitouchManagerTests: XCTestCase { ) { expectation.fulfill() } - wait(for: [expectation], timeout: 2.0) + wait(for: [expectation], timeout: 5.0) // We expect far fewer than 10 device creations due to: // 1. The first restart being in progress blocks subsequent ones @@ -1903,7 +1903,7 @@ final class MultitouchManagerTests: XCTestCase { ) { expectation.fulfill() } - wait(for: [expectation], timeout: 1.0) + wait(for: [expectation], timeout: 3.0) // Only 2 device creations: initial start + one restart // The duplicate restart calls while in progress should have been skipped @@ -1933,7 +1933,7 @@ final class MultitouchManagerTests: XCTestCase { ) { expectation.fulfill() } - wait(for: [expectation], timeout: 3.0) + wait(for: [expectation], timeout: 5.0) // Should still be monitoring after all restarts XCTAssertTrue(manager.isMonitoring) @@ -1980,7 +1980,7 @@ final class MultitouchManagerTests: XCTestCase { ) { restartExpectation.fulfill() } - wait(for: [restartExpectation], timeout: 1.0) + wait(for: [restartExpectation], timeout: 3.0) XCTAssertTrue(manager.isMonitoring) From 22c315123ea1c90084678a79b5422cbc39de2568 Mon Sep 17 00:00:00 2001 From: Karan Mohindroo <96403086+NullPointerDepressiveDisorder@users.noreply.github.com> Date: Fri, 20 Feb 2026 23:56:53 -0800 Subject: [PATCH 2/3] 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> --- MiddleDrag/Managers/DeviceMonitor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiddleDrag/Managers/DeviceMonitor.swift b/MiddleDrag/Managers/DeviceMonitor.swift index ebc25de..d71ae3d 100644 --- a/MiddleDrag/Managers/DeviceMonitor.swift +++ b/MiddleDrag/Managers/DeviceMonitor.swift @@ -54,7 +54,7 @@ import os } unsafe os_unfair_lock_unlock(&gCallbackLock) - // Aditional safety check: Verify the monitor instace is still valid and running. + // Additional safety check: Verify the monitor instance is still valid and running. unsafe monitor.stateLock.lock() let monitorIsRunning = unsafe monitor.isRunning unsafe monitor.stateLock.unlock() From 16a97e25975265d023094c12bd1717c7ebe27c7b Mon Sep 17 00:00:00 2001 From: Karan Mohindroo <96403086+NullPointerDepressiveDisorder@users.noreply.github.com> Date: Fri, 20 Feb 2026 23:57:51 -0800 Subject: [PATCH 3/3] 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> --- MiddleDrag/MiddleDragTests/MultitouchManagerTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiddleDrag/MiddleDragTests/MultitouchManagerTests.swift b/MiddleDrag/MiddleDragTests/MultitouchManagerTests.swift index 132d544..80b34b6 100644 --- a/MiddleDrag/MiddleDragTests/MultitouchManagerTests.swift +++ b/MiddleDrag/MiddleDragTests/MultitouchManagerTests.swift @@ -212,7 +212,7 @@ final class MultitouchManagerTests: XCTestCase { ) { expectation.fulfill() } - wait(for: [expectation], timeout: 3.0) + wait(for: [expectation], timeout: 2.0) XCTAssertFalse(manager.isMonitoring) XCTAssertFalse(manager.isEnabled)