Skip to content

Commit 18b3104

Browse files
chenchaoyiclaude
andauthored
fix(helper): pump run loop for CLLocationManager auth before scan (#45)
Real root cause of the macOS 26 install hang. The v1.0.3 fix added disclaim-responsibility + CLLocationManager.startUpdatingLocation() + Thread.sleep(0.3) to runScanAndDumpJSON. The disclaim hop and the manager init are correct; the Thread.sleep was wrong. Thread.sleep does NOT pump the run loop. CLLocationManager's delegate-callback handshake with locationd never actually completes inside a short-lived CLI subprocess. CoreWLAN's macOS 26 redaction gate checks whether the process is a *registered* location consumer — not just authorised — so scans came back redacted (ssid/bssid null) even when the bundle's TCC grant was in place. The bundle GUI sees authorizationStatus as granted because that's a synchronous read of cached state; the redaction gate is the stricter check. This kept the user stuck at "需要以下权限:- 定位服务" through v1.0.3 / v1.0.4 / v1.0.5 even after clicking Allow on every prompt. Fix: new LocationAuthProbe : CLLocationManagerDelegate that flips a flag when `locationManagerDidChangeAuthorization` resolves status to non-.notDetermined. The scan subcommand pumps `RunLoop.current.run(mode:.default, before:…)` in 50ms slices, exiting as soon as the callback fires (typically <100ms when the grant is already in TCC.db) or after a 2s timeout. Only then does scanForNetworks run. Same pattern as the existing runBluetoothStatusProbe. Verified locally end-to-end: - helper/build.sh rebuilds → new cdhash 6e74d56e... - user grants Location on the new cdhash via `open helper/...` - 3 cold-start subprocess scans (helper GUI killed between runs to defeat warm-cache effects) → 100% unredacted rows each run Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d510f97 commit 18b3104

5 files changed

Lines changed: 143 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,37 @@ the project follows [Semantic Versioning](https://semver.org/) where
99
practical. The leading `v0.x` line is allowed to break minor
1010
behaviours between releases.
1111

12+
## [1.0.6] — 2026-05-13
13+
14+
The v1.0.3 → v1.0.5 chain attempted to fix CoreWLAN scan
15+
redaction under install.sh-installed helpers via
16+
disclaim-responsibility + `CLLocationManager.startUpdatingLocation()`
17+
+ `Thread.sleep(0.3)`. The disclaim hop and the manager init are
18+
necessary but the third piece was wrong: `Thread.sleep` does not
19+
pump the run loop, so `CLLocationManager`'s delegate-callback
20+
handshake with `locationd` never actually completes inside the
21+
short-lived CLI subprocess. CoreWLAN's redaction gate on macOS 26
22+
checks whether the calling process is a *registered* location
23+
consumer (not just an authorized one), so scans came back redacted
24+
even when the bundle's TCC grant was in place.
25+
26+
This kept the user stuck at "需要以下权限:- 定位服务" through
27+
v1.0.3 / v1.0.4 / v1.0.5 even after clicking Allow on the popup.
28+
29+
### Fixed
30+
- **`runScanAndDumpJSON()` pumps the run loop until the location
31+
authorization callback fires.** New `LocationAuthProbe` delegate
32+
signals when `locationManagerDidChangeAuthorization` resolves to
33+
a non-`.notDetermined` state. The scan subcommand now runs
34+
`RunLoop.current.run(mode:.default, before:…)` slices of 50 ms
35+
each, exiting as soon as the callback lands (typically <100 ms
36+
on a freshly-granted bundle) or after a 2 s timeout. Only then
37+
does `scanForNetworks` get called. Mirrors the existing
38+
`runBluetoothStatusProbe` pattern.
39+
- Verified locally end-to-end: 3 cold-start subprocess scans
40+
(helper GUI killed between runs to defeat warm-cache effects)
41+
return 100% unredacted rows.
42+
1243
## [1.0.5] — 2026-05-13
1344

1445
User on macOS 26 installed v1.0.4 via the one-liner and ended up

docs/zh/CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,32 @@
88
[Semantic Versioning](https://semver.org/)`v0.x` 阶段允许破坏性的次要
99
行为变更。
1010

11+
## [1.0.6] — 2026-05-13
12+
13+
v1.0.3 → v1.0.5 一路上想修 install.sh 安装路径下 CoreWLAN scan 数据
14+
被屏蔽的问题,做法是「disclaim 责任继承 + `CLLocationManager.start
15+
UpdatingLocation()` + `Thread.sleep(0.3)`」。disclaim 和 manager 初
16+
始化都是必要的,第三块错了:`Thread.sleep` 不会 pump run loop,所
17+
`CLLocationManager``locationd` 的 delegate 回调握手在 CLI
18+
子进程的短生命周期里根本完不成。macOS 26 上 CoreWLAN 的屏蔽门看
19+
的是「调用方进程是不是已注册的 location 消费者」(不是「是否被授权」),
20+
所以即使 bundle 的 TCC 授权已经在了,scan 出来还是 null。
21+
22+
用户从 v1.0.3 一路被卡到 v1.0.5,点了 Allow 弹窗也无解,就是这个
23+
原因。
24+
25+
### 修复
26+
- **`runScanAndDumpJSON()` 现在真正 pump run loop 等
27+
`CLLocationManager` 授权回调**。新加 `LocationAuthProbe` delegate,
28+
`locationManagerDidChangeAuthorization` 回调里把 status 从
29+
`.notDetermined` 翻转出来时打标记。scan 子命令以 50 ms 为粒度跑
30+
`RunLoop.current.run(mode:.default, before:…)`,回调一落地立刻退
31+
出(已授权的 bundle 上通常 <100 ms),或 2 秒超时兜底。完成后才
32+
`scanForNetworks`。模式对齐已有的
33+
`runBluetoothStatusProbe`
34+
- 本地端到端验证:3 次冷启动 subprocess scan(每次先 kill helper
35+
GUI 防止 warm-cache 干扰)全部返回 100% 解屏蔽行。
36+
1137
## [1.0.5] — 2026-05-13
1238

1339
macOS 26 用户走一行 installer 装 v1.0.4 之后,helper bundle 的

helper/Sources/diting-tianer/main.swift

Lines changed: 84 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -53,43 +53,101 @@ import IOBluetooth
5353
// CLI mode: scan
5454
// ---------------------------------------------------------------------
5555

56+
/// Probe-only CLLocationManager delegate used by `runScanAndDumpJSON`
57+
/// to wait until the location authorization state has been determined
58+
/// (i.e. CoreLocation's TCC handshake with `locationd` has run and
59+
/// landed) before we let CoreWLAN run its redaction check. Without
60+
/// waiting on the delegate, CoreWLAN sees a CLLocationManager that
61+
/// has only been `startUpdatingLocation`'d but never received a
62+
/// state callback — and treats the process as having no active
63+
/// location session, which is what macOS 26 uses to gate
64+
/// CWNetwork.ssid / .bssid.
65+
final class LocationAuthProbe: NSObject, CLLocationManagerDelegate {
66+
private(set) var statusResolved = false
67+
private(set) var lastStatus: CLAuthorizationStatus = .notDetermined
68+
69+
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
70+
lastStatus = manager.authorizationStatus
71+
if manager.authorizationStatus != .notDetermined {
72+
statusResolved = true
73+
}
74+
}
75+
76+
// Pre-Catalina spelling — kept for completeness; macOS calls one of
77+
// these two depending on SDK target.
78+
func locationManager(_ manager: CLLocationManager,
79+
didChangeAuthorization status: CLAuthorizationStatus) {
80+
lastStatus = status
81+
if status != .notDetermined {
82+
statusResolved = true
83+
}
84+
}
85+
}
86+
5687
func runScanAndDumpJSON() -> Never {
57-
// macOS 14.4+ gates CWNetwork.ssid / .bssid behind Location Services
58-
// at the calling process level — having the bundle's TCC grant on
59-
// disk is not enough. Two things have to be true at scanForNetworks
60-
// time:
88+
// macOS 14.4+ (and tighter on 26) gates CWNetwork.ssid / .bssid
89+
// behind Location Services at the calling process level. Having
90+
// the bundle's TCC grant on disk is not enough on its own — three
91+
// things have to be true at scanForNetworks time:
92+
//
93+
// 1. The TCC subject seen by macOS is the helper bundle (not
94+
// the terminal that launched us). Inherited responsibility
95+
// from Terminal → diting → diting-tianer breaks this on
96+
// macOS 26 — tccd attributes the request to Terminal, which
97+
// has no NSLocationUsageDescription, and CWNetwork redacts
98+
// ssid / bssid silently. The fix is to disclaim our parent
99+
// so we become our own responsible process, same hop the
100+
// ble-scan subcommand has done since v0.5.0.
61101
//
62-
// 1. The TCC subject seen by macOS is the helper bundle (not the
63-
// terminal that launched us). Inherited responsibility from
64-
// Terminal → diting → diting-tianer breaks this on macOS 26 —
65-
// tccd attributes the request to Terminal, which has no
66-
// NSLocationUsageDescription, and CWNetwork redacts ssid /
67-
// bssid silently. The fix is to disclaim our parent so we
68-
// become our own responsible process, same hop the ble-scan
69-
// subcommand has done since v0.5.0.
102+
// 2. A CLLocationManager exists in this process AND has had
103+
// its authorization status callback fire. This is the
104+
// subtle bit: macOS treats a CLLocationManager that has
105+
// only been `startUpdatingLocation`'d (no delegate
106+
// callback yet) as "not yet a location-services consumer"
107+
// and CoreWLAN still redacts. The fix is to pump the run
108+
// loop until `locationManagerDidChangeAuthorization` fires,
109+
// which means CoreLocation has done its TCC handshake with
110+
// locationd and the process is now a recognised consumer.
70111
//
71-
// 2. A CLLocationManager has called startUpdatingLocation() in
72-
// this process. The bundle grant alone isn't enough; CoreWLAN
73-
// checks for an active location signal at the moment of
74-
// scanForNetworks(). We init the manager, kick it, hold a
75-
// reference for the lifetime of the function so the runtime
76-
// can't release it before the scan call hits CoreWLAN.
112+
// 3. The CLLocationManager reference stays alive across the
113+
// scanForNetworks() call so the runtime doesn't drop the
114+
// consumer registration mid-scan.
77115
//
78-
// The earlier code comment claiming CoreLocation was "more lenient"
79-
// than CoreBluetooth was wrong; CoreWLAN on macOS 26 enforces both.
116+
// The earlier code comment claiming CoreLocation was "more
117+
// lenient" than CoreBluetooth was wrong; CoreWLAN on macOS 26
118+
// enforces all three. The v1.0.3 fix attempted only (1) + (3) +
119+
// a Thread.sleep — but Thread.sleep doesn't pump the run loop,
120+
// so (2) was missed and the scan still came back redacted on
121+
// freshly-installed bundles. Mirror the existing bluetooth-status
122+
// pattern: real run-loop pump, real delegate callback wait.
80123
if ProcessInfo.processInfo.environment[kDisclaimEnv] == nil {
81124
reExecWithDisclaimedResponsibility()
82125
}
83126

84127
let locationManager = CLLocationManager()
128+
let probe = LocationAuthProbe()
129+
locationManager.delegate = probe
85130
locationManager.requestWhenInUseAuthorization()
86131
locationManager.startUpdatingLocation()
87-
// Brief settle so CoreLocation's TCC registration lands before
88-
// CoreWLAN inspects it. Empirically ~50ms is enough on M-series
89-
// Macs; 300ms gives headroom on slower hardware without making
90-
// the user wait an obvious extra beat.
91-
Thread.sleep(forTimeInterval: 0.3)
92-
_ = locationManager // hold the reference until the scan returns
132+
133+
// Pump the run loop until the authorization callback fires or a
134+
// 2-second timeout elapses. The callback only fires after
135+
// CoreLocation has fully registered with locationd, which is what
136+
// CoreWLAN's redaction gate checks for. Each `run(until:)` slice
137+
// returns control briefly so we can re-check the probe flag and
138+
// exit early once authorization is resolved — usually within ~50ms
139+
// when the grant is already in TCC.db.
140+
let deadline = Date(timeIntervalSinceNow: 2.0)
141+
while !probe.statusResolved && Date() < deadline {
142+
RunLoop.current.run(mode: .default,
143+
before: Date(timeIntervalSinceNow: 0.05))
144+
}
145+
// Proceed regardless of probe outcome. If authorization is still
146+
// .notDetermined after 2 s (TCC grant genuinely missing), the
147+
// scan will return redacted rows — same end state as before, just
148+
// not slower than necessary. Hold the manager reference past the
149+
// scan call so the consumer registration doesn't get released.
150+
_ = locationManager
93151

94152
let client = CWWiFiClient.shared()
95153
guard let iface = client.interface() else {

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "diting"
3-
version = "1.0.5"
3+
version = "1.0.6"
44
description = "macOS terminal listening post for Wi-Fi, BLE, link health, and the RF environment — your Mac hears more than it tells you"
55
readme = "README.md"
66
license = "MIT"

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)