
你的 Mac 应用授予了麦克风权限 — 然后每次启动都忘得一干二净
GeekBye 请求麦克风权限,你授予了,它也确实能用。可下次启动:没了。而且这个应用压根就没出现在「系统设置 → 隐私与安全性 → 麦克风」里。罪魁祸首是 macOS 一个悄悄运行的安全特性,它让应用从一个转瞬即逝的路径运行 — 下面是诊断过程,以及那个一步搞定的修复。
这是一个会让你怀疑自己眼睛的 bug。你安装了 GeekBye,它请求麦克风权限,你点了 Allow,转写也能用。很好。你退出,第二天早上重新打开它 — 结果它又一次请求麦克风权限。你想去「系统设置 → 隐私与安全性 → 麦克风」里手动修一下,却发现 GeekBye **根本不在列表里。**没被拒绝,也没被允许。就是不在,仿佛它从没请求过。
每一个单独的环节看起来都是对的。提示是真的。权限当下也确实有效。应用被正确地签名并公证,用途字符串也都正确。可授权就是在每次启动时蒸发了。GeekBye v2.0.6 把它修好了 — 而根源正是 macOS 为保护你而做的最狡猾的事情之一。
那个把应用藏起来、连它自己都找不到的特性
罪魁祸首是 macOS App Translocation,一个 Gatekeeper 安全特性。当你下载一个应用并直接从 DMG 或从你的 ~/Downloads 文件夹运行它 — 只要它还处于「被隔离」的状态 — macOS 实际上并不是从你看到的位置运行它。它会透明地把它复制到一个随机化的、只读的路径,深藏在 /private/var/folders/.../AppTranslocation/… 之下,然后运行那个副本。这是一道不错的防线:它能阻止一个恶意下载篡改它旁边的文件。
但冲突就在这里。macOS 的权限系统(TCC — 那个记录谁能使用你麦克风、摄像头、屏幕的东西)是通过路径和代码身份来识别一个应用的。当应用被 translocate 时,那个路径是随机且临时的。所以当你授予麦克风权限时,macOS 尽职尽责地记录了这次授权 — *却记在了一个下次启动就不存在的路径上。*重新打开应用,macOS 把它 translocate 到一个不同的随机路径,看到一个它毫无记录的应用,于是又问了一遍。而因为那个幻影路径永远不是一个稳定的位置,应用也就永远无法在「系统设置 → 麦克风」里挣得一行永久的记录。
这个应用一直在把权限授给一个幽灵。
这也是为什么只有一部分人会遇到它。如果你那份 GeekBye 已经在 /Applications 里 — 因为你把它拖了进去,或者因为它通过自动更新到了那里 — 就没有隔离,没有 translocation,路径是稳定的,一切都完美保存。这个 bug 对我们、对任何过了首次安装的人都是隐形的,而这恰恰是那种活得最久的 bug。
修复:给应用一个真正的家
既然整个问题就是一个不稳定的路径,那修复就是把应用弄到一个稳定的路径上。v2.0.6 会检测 GeekBye 是否在被 translocate 的状态下运行(或者只是在 /Applications 之外运行),并提供一步点击的 "Move to Applications" — 使用 macOS 的重定位调用,把整个应用包复制进 /Applications 并从那里重新启动它。从那一刻起,应用就有了固定的身份:麦克风授权保住了,屏幕录制授权保住了,GeekBye 也终于出现在了你所预期的系统设置里。
这个提示很有礼貌。它提供 Move to Applications、Not Now 和 Don't Ask Again — 而且它会记住上一次的选择,所以应用绝不会去骚扰一个有意要从别处运行它的人。至于是否要显示这个提示的判断,被隔离进了小而纯粹的函数里(这份构建是被 translocate 的吗?它在 /Applications 之外吗?用户把它压下去了吗?),这样这段逻辑就能被单元测试,而无需在真实的被隔离卷上启动一个真实的、公证过的构建。
同一次发布还简化了权限体验本身。GeekBye 过去会弹出一个定制的、专门打造的应用内权限窗口 — 几百行 UI,试图重现操作系统本已做得很好的东西。v2.0.6 把它删掉了,转而依靠原生的 macOS 权限提示,再配上一个安静、非阻塞的横幅,只在某个必需权限确实缺失时才出现。代码更少,而且是用户早已熟悉的行为,因为其他所有 Mac 应用都是这么工作的。
我最自豪的部分:在相信它之前,我们先证明了它
猜是 translocation 然后就发一个修复,本来会很容易。可我们没有,这次发布先发了一个启动诊断:在启动时,GeekBye 现在会报告它自己的可执行文件路径、它是否被 translocate、它是否在 /Applications 之内,以及当前的麦克风和屏幕权限状态。那份遥测把一个看似合理的理论变成了一个被确认的理论 — 生产数据显示,那些受影响的会话确实是在 /Applications 之外的、被 translocate 的路径上运行的,和预测的分毫不差。
这个顺序很重要。「提示出现但权限不保存」有好几种可能的解释 — 签名问题、缺失的用途字符串、授权项(entitlement)问题、TCC 数据库的怪癖。我们先排除了 API 层面的原因(请求路径可以证明是正确的),然后让现实世界的数据指向身份/路径这一层,而不是发一个满怀希望的修复、然后祈祷支持工单不再来。
这个 bug 教会的三件事
- **一个会弹提示却无法保存的权限,是一个身份问题,不是一个 API 问题。**如果请求代码是对的,授权却依然消失,别再重写请求了。要问的是,操作系统把这次授权绑定到了什么路径和代码身份上 — 以及那个身份在多次启动之间是否稳定。
- **把诊断和修复一起发(或者先发)。**几个字段 — 我从哪里运行、我的权限状态是什么 — 就把一个有根据的猜测变成了一个被验证的根源,并准确告诉了我们哪些用户受了影响。在给你怀疑的那道边界打补丁之前,先给它装上仪表。
- **那些躲着开发者的 bug,正是运行在「错误」环境里的那些。**我们的 bug 住在每台开发机器的
/Applications里,所以它对我们在结构上就是隐形的,却击中了首次使用的用户。当一份报告无法复现时,第一个问题是它运行的地方有什么不同,而不是用户是不是搞错了。
GeekBye v2.0.6 把重定位提示和诊断一起发了出去。想了解这件事所处的更宏大的可靠性脉络,请看打造一个真正的版本 2 需要什么(v2.0.0),以及为什么屏幕录制录到了错误的显示器(v2.0.10) — 另一个只在特定环境里才现身的 bug。至于隔壁那次关于小细节的发布,请看从容的软件:闪烁修复与回答模式芯片(v2.0.3 + v2.0.5)。