Summary
OpenTabletDriver intermittently crashes around USB device reconnection after macOS sleep/wake. It has been fairly easy to encounter in my setup, but I do not yet have a deterministic reproduction procedure. I captured an unhandled exception in the daemon's device-change callback, followed later by an unhandled RPC connection-loss exception in the UI.
AI disclosure: All source-code analysis below and the local experimental patch/tests were produced using OpenAI Codex. I am the affected user and a developer, but I do not have relevant expertise in tablet drivers, macOS HID, or this codebase. The diagnosis is a hypothesis requiring review; the stack traces are actual output captured from the installed application.
Environment
- macOS 26.5.2 on Apple Silicon
- OpenTabletDriver 0.6.7 macOS x86_64 build, running under Rosetta 2
- Initial connection: Mac → USB-C cable → monitor's built-in USB hub → tablet, keyboard, and headset
- Moving the tablet to a separate dock did not prevent the problem.
Tablet Device
Wacom CTL-471
Steps associated with the issue
- Start OTD with the tablet connected through the monitor hub or a separate dock.
- Use the tablet normally, then put the Mac to sleep.
- Wake the Mac; some tests used approximately 10–20 seconds of sleep. USB devices disconnected and reappeared during testing.
- Continue using the system and observe OTD and its captured stderr.
These are the circumstances in which I encountered the problem, not a guaranteed reproducer.
Expected Behavior
OTD handles device removal/reconnection without terminating.
Observed Behavior
The daemon exits with the exception below. The UI subsequently also reports an unhandled RPC connection-loss exception.
Diagnostic Information
The following exceptions were captured from the installed application's stderr, before any local source modifications were deployed.
Daemon
Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
at System.Collections.Generic.List`1.Enumerator.MoveNext()
at System.Linq.Enumerable.ExceptIterator[TSource](IEnumerable`1 first, IEnumerable`1 second, IEqualityComparer`1 comparer)+MoveNext()
at System.Linq.Enumerable.Any[TSource](IEnumerable`1 source, Func`2 predicate)
at OpenTabletDriver.Daemon.DriverDaemon.<.ctor>b__1_2(Object sender, DevicesChangedEventArgs args) in /home/runner/work/OpenTabletDriver/OpenTabletDriver/OpenTabletDriver.Daemon/DriverDaemon.cs:line 56
at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__128_1(Object state)
at System.Threading.ThreadPoolWorkQueue.DispatchItemWithAutoreleasePool(Object workItem, Thread currentThread)
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
at System.Threading.Thread.StartCallback()
Subsequently captured UI exception
Unhandled exception. StreamJsonRpc.ConnectionLostException: The JSON-RPC connection with the remote party was lost before the request could complete.
at StreamJsonRpc.JsonRpc.InvokeCoreAsync(JsonRpcRequest request, Type expectedResultType, CancellationToken cancellationToken)
at StreamJsonRpc.JsonRpc.InvokeCoreAsync[TResult](RequestId id, String targetName, IReadOnlyList`1 arguments, IReadOnlyList`1 positionalArgumentDeclaredTypes, IReadOnlyDictionary`2 namedArgumentDeclaredTypes, CancellationToken cancellationToken, Boolean isParameterObject)
at OpenTabletDriver.UX.MainForm.<HandleDaemonConnected>b__21_0() in /home/runner/work/OpenTabletDriver/OpenTabletDriver/OpenTabletDriver.UX/MainForm.cs:line 412
at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__128_0(Object state)
at MonoMac.AppKit.AppKitSynchronizationContext.<>c__DisplayClass1_0.<Post>b__0()
at MonoMac.Foundation.NSAsyncActionDispatcher.Apply()
Preliminary analysis
A possible race involves mutable device collections: DevicesChangedEventArgs retains references to the previous/current collections, while Additions uses deferred Except enumeration. RootHub may modify the underlying list while the daemon enumerates args.Additions in its device-change callback. The UI exception may be a secondary failure after losing its connection to the daemon.
This seems consistent with the captured exception, but I cannot independently validate the diagnosis or whether the proposed solution fits the project's intended design.
A similar exception was reported in #3283, but in Driver.MatchDevice, so I am not assuming this is a duplicate.
Local experiment and possible follow-up
To explore this hypothesis, I have a local experimental patch based on 0.6.x that snapshots device collections and adjusts notification synchronization, plus a separate attempt to handle connection loss during UI initialization.
Four added tests failed on the unmodified baseline and passed with these changes; the macOS solution built successfully. The full suite had 12 failures also present on the unmodified baseline in this macOS environment. These results only exercise the proposed model of the problem; they do not establish that it explains or fixes the real-device crash.
The patch has not yet been validated through everyday use. I plan to test it during normal use and report the results. If you are comfortable reviewing an AI-assisted contribution, I would be happy to follow up with a PR after testing, splitting independent fixes as requested by the contribution guidelines.
Acknowledgements
Summary
OpenTabletDriver intermittently crashes around USB device reconnection after macOS sleep/wake. It has been fairly easy to encounter in my setup, but I do not yet have a deterministic reproduction procedure. I captured an unhandled exception in the daemon's device-change callback, followed later by an unhandled RPC connection-loss exception in the UI.
AI disclosure: All source-code analysis below and the local experimental patch/tests were produced using OpenAI Codex. I am the affected user and a developer, but I do not have relevant expertise in tablet drivers, macOS HID, or this codebase. The diagnosis is a hypothesis requiring review; the stack traces are actual output captured from the installed application.
Environment
Tablet Device
Wacom CTL-471
Steps associated with the issue
These are the circumstances in which I encountered the problem, not a guaranteed reproducer.
Expected Behavior
OTD handles device removal/reconnection without terminating.
Observed Behavior
The daemon exits with the exception below. The UI subsequently also reports an unhandled RPC connection-loss exception.
Diagnostic Information
The following exceptions were captured from the installed application's stderr, before any local source modifications were deployed.
Daemon
Subsequently captured UI exception
Preliminary analysis
A possible race involves mutable device collections:
DevicesChangedEventArgsretains references to the previous/current collections, whileAdditionsuses deferredExceptenumeration.RootHubmay modify the underlying list while the daemon enumeratesargs.Additionsin its device-change callback. The UI exception may be a secondary failure after losing its connection to the daemon.This seems consistent with the captured exception, but I cannot independently validate the diagnosis or whether the proposed solution fits the project's intended design.
A similar exception was reported in #3283, but in
Driver.MatchDevice, so I am not assuming this is a duplicate.Local experiment and possible follow-up
To explore this hypothesis, I have a local experimental patch based on
0.6.xthat snapshots device collections and adjusts notification synchronization, plus a separate attempt to handle connection loss during UI initialization.Four added tests failed on the unmodified baseline and passed with these changes; the macOS solution built successfully. The full suite had 12 failures also present on the unmodified baseline in this macOS environment. These results only exercise the proposed model of the problem; they do not establish that it explains or fixes the real-device crash.
The patch has not yet been validated through everyday use. I plan to test it during normal use and report the results. If you are comfortable reviewing an AI-assisted contribution, I would be happy to follow up with a PR after testing, splitting independent fixes as requested by the contribution guidelines.
Acknowledgements