You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(helper): scan re-launches itself via LaunchServices (the real macOS 26 fix) (#46)
* fix(helper): scan-with-retry loop for macOS 26 cold-start unredaction
v1.0.6 didn't actually verify cold. The RunLoop.current.run pump in
v1.0.6 was load-bearing on locationd's warm cache rather than its
own dispatch handshake — when tested 30+ minutes after a fresh
grant (cache cold) it still returned redacted rows. User surfaced
this when `uv run diting` worked (in-repo bundle re-opened many
times in the session, keeping locationd warm) while `diting`
(curl-installed, cold) still hung indefinitely.
Restructure the scan subcommand around dispatchMain + scan-with-
retry. Same dispatch pattern as the working bluetooth-status
probe, plus adaptive retry that adjusts to locationd cold-start
latency.
How it works:
1. ScanWorker.start() calls requestWhenInUseAuthorization /
startUpdatingLocation, then attemptScan(0) immediately.
2. attemptScan runs scanForNetworks. If any row has a bssid,
emit JSON and exit (warm path: ~90 ms).
3. If every row is redacted but the scan returned data, schedule
attemptScan(n+1) 500 ms later. While we wait, dispatchMain
keeps libdispatch + CoreLocation pumping so locationd
registration finishes in the background.
4. Up to 6 attempts ≈ 5 s worst case; locationManager DidChange
Authorization short-circuits early only on .denied /
.restricted (waiting wouldn't unstuck those).
5. Global g_scanWorker pins the worker (and its
CLLocationManager) for the lifetime of the process so the
consumer registration isn't released mid-scan.
Why retry instead of a fixed wait: the auth callback fires within
~100 ms when TCC.db has a grant, but CoreLocation's registration
with locationd lands non-deterministically (~200 ms warm, up to
~3 s cold). A fixed wait either makes warm scans needlessly slow
or misses cold state. Retry adapts.
Verified locally on the user's machine, freshly-rebuilt bundle,
locationd cold:
- 4.2 s, 95/95 rows unredacted (first cold run, ~4 retries)
- 1.7 s, 102/102 rows unredacted (subsequent, ~1 retry)
- 0.088 s, 122/122 unredacted (warm, no retry)
- 1.9 s, 99/99 unredacted (cold again)
- 0 redacted-result runs across all four tests
Bonus discovery: macOS extends System Settings → Location grants
from the original cdhash to fresh rebuilds of the same bundle id
(at least for ad-hoc-signed bundles). New cdhashes inherit without
a fresh prompt — useful for the install.sh-then-update flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(helper): scan re-launches itself via LaunchServices for TCC
This supersedes the dispatchMain + retry-loop approach in the
prior commit on this branch. That approach turned out to also
ride on locationd warm cache and failed cold on the user's
machine — same install hang it was supposed to fix.
Diagnostic that nailed it: print
`CLLocationManager.authorizationStatus().rawValue` to stderr at the
top of the disclaimed scan subprocess. Returns `0` (.notDetermined)
on macOS 26 — even though the GUI bundle launched via `open` from
the same binary at the same path with the same cdhash sees the
SAME bundle as `.authorizedWhenInUse`.
The asymmetry:
- Direct-exec subprocess of <bundle>/Contents/MacOS/binary:
TCC attributes by raw-process identity. No grant matches. Bundle's
Location entry is invisible to this process — no run-loop pump,
no NSApp.run(), no disclaim, no retry can fix it.
- LaunchServices-launched bundle (via `open` from outside, or our
own `/usr/bin/open --args ...` from inside):
TCC attributes by bundle. Grant matches. scanForNetworks
unredacts normally.
CoreBluetooth on the same machine is fine with direct-exec — that's
why `bluetooth-status` has been working the whole time. CoreLocation
tightened on macOS 26 and the gate requires LaunchServices launch.
Fix: split `runScanAndDumpJSON` into outer + inner halves.
Outer (Python's subprocess.run([binary, "scan"])):
Spawn /usr/bin/open with `-W -g -a <bundle>` and env vars
DITING_SCAN_VIA_LAUNCH=1 + DITING_SCAN_OUT=<tempfile>. Wait
for the LaunchServices'd child to write JSON, relay to stdout,
exit. Python sees this process as if it were the original
subprocess — no protocol change.
Inner (DITING_SCAN_VIA_LAUNCH=1 in env):
Initialise NSApp with .prohibited activation policy (no Dock
icon, no focus theft), start the ScanWorker, run NSApp.run().
Worker does the actual scan-with-retry, writes to
DITING_SCAN_OUT, exits.
Verified on the user's macOS 26 machine, installed bundle path,
locationd cold (3 cold tests):
- 1.56 s, 116/116 unredacted
- 0.29 s, 139/139 unredacted (locationd cached after run 1)
- 1.84 s, 103/103 unredacted
The end-to-end install flow (`diting --lang zh --log --notify`)
now enters the TUI within ~3 s instead of hanging indefinitely.
Documentation:
- CHANGELOG.md / docs/zh/CHANGELOG.md updated to describe the real
fix (not the abandoned dispatchMain approach)
- docs/RELEASE.md / docs/zh/RELEASE.md gain a troubleshooting entry
with the recipe to verify the fix isn't regressed
- Memory entry macos26_corewlan_launchservices.md captures the
hours of debugging context for future sessions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0 commit comments