OpenTabletDriver / OpenTabletDriver/OpenTabletDriver

macOS: daemon crashes with "Collection was modified" when devices reconnect after sleep/wake

Open
#5,028 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

needs-triage
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

  1. Start OTD with the tablet connected through the monitor hub or a separate dock.
  2. Use the tablet normally, then put the Mac to sleep.
  3. Wake the Mac; some tests used approximately 10–20 seconds of sleep. USB devices disconnected and reappeared during testing.
  4. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.