OpenTabletDriver / OpenTabletDriver/OpenTabletDriver
macOS: daemon crashes with "Collection was modified" when devices reconnect after sleep/wake
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 4.1k
- Forks
- 534
- Avg merge
- 4d 20h
- Merged PRs (30d)
- 12
Description
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
- I have reviewed the macOS FAQ and General FAQ. I did not find an entry addressing the captured collection-modification exception. This does not mean I have performed every troubleshooting step in the Wiki.
- I have searched existing issues and have not found a confirmed duplicate of this stack trace.
- I have written a concise and meaningful title.
- I am using the latest release, 0.6.7.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at OpenTabletDriver.Daemon/DriverDaemon.cs line 56 and the device-change callback, then compare the captured stack trace with the local experimental snapshot and synchronization tests. Check MainForm.cs line 412 for the subsequent connection-loss path. Done means the daemon survives device reconnection after sleep/wake and the relevant tests pass without introducing the reported UI failure.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, macos
- Domain
- desktop, operating-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100