Skip to content

Take fails and strands the peripheral when the other Mac needs more than 2s to release it #159

Open

Description

@BunnyMan1

Summary

Taking a peripheral fails with "didn't release" whenever the Mac holding it needs more than about 2 seconds to finish releasing it. The holder is still mid-release at that point, so the taker gives up, and the release then completes with nobody picking the peripheral up. It ends up paired to neither Mac, and can stop answering until it is power-cycled.

Environment

  • Magic Switch v2.30.1 (release build) on both Macs
  • Mac A: MacBook Pro, Apple silicon, macOS 15.7.1
  • Mac B: Mac mini, Apple silicon, macOS 26.7.1
  • Magic Keyboard (product ID 0x0322), registered on both Macs through Sync
  • Connected over the LAN, with Bonjour working and Ping succeeding

Steps to reproduce

  1. Have the keyboard connected to Mac B.
  2. Click Take on Mac A.

It is intermittent because it depends on how long Mac B's release takes. Mac B took 1.1s, 3.5s and 11.7s across the releases I timed, and only the 1.1s one worked.

Expected

The take waits for the release to finish, then pairs the keyboard on Mac A.

Actual

Mac A shows the take-failed notification about 2 seconds after the release starts, and the keyboard is left on neither Mac.

Timeline from one failure (Mac A and Mac B logs, device names removed):

15:10:34.275 Mac B  app        Device information removed: <keyboard>
15:10:36.326 Mac A  usernoted  Presenting notification take-failed-<address>
15:10:36.377 Mac A  app        Auto-reconnect: <keyboard> held by <Mac B>; leaving it
15:10:37.754 Mac B  bluetoothd ACL disconnected: <address>, result 0, reason 10722
15:10:37.759 Mac B  bluetoothd Device <address> is unpaired and now fully disconnected, resetting it

The next take, about 35 seconds after the release finished, reached the keyboard but pairing failed. The Macs' retries then logged Pairing failed for <address>: 2 and later : 4, until the keyboard was power-cycled.

All four releases over 2 seconds that I could time failed about 2 seconds after they started:

Release started Failure notification Release finished Duration
15:09:59.037 15:10:01.177 not captured over 2s
15:10:34.275 15:10:36.326 15:10:37.754 3.5s
15:16:38.126 15:16:40.194 15:16:49.869 11.7s
15:16:44.122 (second Take during the release above) 15:16:46.181 15:16:49.869 same release

Releases that finished within the limit worked: Mac B in 1.1s, and Mac A in 0.8s and 1.6s.

Likely cause

Reading the code at 4cb56e9 (the same logic is in the v2.30.1 tag):

  1. The holder answers UNREGISTER_ONE through replyOnceDisconnected, which uses waitForLocalDisconnect. That polls 5 times at 0.5s intervals, so it reports failure after about 2 seconds, even though -remove is still completing in the Bluetooth daemon.
  2. The taker lands in the .failure(let err) arm of takePeripheralFromPeer, shows the notification and arms the HOLDS_ONE-gated watcher.
  3. The watcher's first HOLDS_ONE arrives while the holder is still connected, so it gets a yes, and reclaimIfPeerIsFree disarms itself. Nothing is left watching when the release completes moments later.

From reading the code (not observed), continueSendHandoff uses the same helper, so a slow release during Send should also report "didn't disconnect" and never send CONNECT_ONE, while the release still completes.

Why the release is sometimes slow

I don't have a confirmed cause. In the 11.7s release, bluetoothd on Mac B was running repeated HID report exchanges (getReportWithKeyhole, report ID 0x45) and logged Unplugging virtual cable only after they ended. In the 1.1s release those exchanges had already stopped. The 3.5s release fits less cleanly: it logged Unplugging virtual cable ... failed with status 118, then removed the link key 2 seconds later. This is a small sample, and I only saw releases over 2 seconds on the macOS 26 Mac.

Possible fix (untested)

  • Extend the wait in waitForLocalDisconnect to about 15 seconds. releaseBodyTimeout on the taker is already 20 seconds, so that should fit for Take. The longest release I saw was 11.7 seconds.
  • Don't let a HOLDS_ONE yes disarm the watcher straight after a failed release, for example by keeping it armed for a grace period while the peer is still mid-release.

Other things I noticed

  • Retries stopped: after a stranded keyboard's failures, the last retry I could see on Mac B was around 15:12:16, within the 10-minute window. I then power-cycled the keyboard and it did not reconnect until I clicked Take manually at 15:15:37. I don't know why, and it may be a separate problem.
  • README setup step: "each peripheral has to be paired to both Macs" caused trouble for me. Pairing the keyboard with the second Mac made the first show "This Mac can no longer connect to ... Forget this device and pair it again." Pairing with one Mac, registering it, then using Sync and Take worked, which suggests the step may be unnecessary.

I can provide fuller logs if they would help.

Activity

  1. MegaManSec commented on Nov 4, 2026

    @MegaManSec
    Owner

    Confirmed, thanks. #160 waits up to 15s for the release and keeps the taking Mac watching while it finishes; #162 fixes the README step.

    For "retries stopped", could you share both Macs' logs from 15:10–15:16?

    log show --predicate 'subsystem == "com.megamansec.magic-switch"' --info --start "<date> 15:10:00" --end "<date> 15:16:00"
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions