Skip to content

0.1.27-beta.1 随机闪退:child window 上屏时踩到已死的 view service / Random crash when a popover orders over an orphaned NSRemoteView #98

Description

@CtriXin

环境 / Environment

  • Glint 0.1.27-beta.1 (build 202607300827), app.glint.Glint
  • macOS 27.0 (26A5388g), Apple M1 Max
  • 复现于 upstream main@d297ef4,代码与 release 一致 / reproduced against upstream main@d297ef4

现象 / Symptom

两天内随机闪退两次,无操作规律,秒级重开后正常。系统没有留下 .ips(本机
DiagnosticReports 被清理过),证据取自 unified log。

Two random crashes in two days. No user-visible trigger, no dialog — the whole
window (and every terminal in it) just disappears.

pid 崩溃时刻 存活时长
4958 2026-08-04 23:57:06 ~13 h
6555 2026-08-05 19:23:32 ~19 h

两次的异常类型、抛出点、调用栈逐帧相同

崩溃栈 / Stack

崩溃前 15–25 ms,两次都先出现:

ViewBridge: assertion failed: '<private>' in -[NSRemoteView containingWindowWillOrderOnScreen:] on line 4221
ViewBridge: Error Domain=com.apple.ViewBridge.error Code=8 ... NSViewBridgeErrorException
SafariPlatformSupport: View service did terminate with error: <private>

然后:

FAULT: NSInternalInconsistencyException  [com.apple.hiservices:HIExceptions]

 0 CoreFoundation  __exceptionPreprocess
 1 libobjc.A.dylib objc_exception_throw
 3 ViewBridge      -[NSRemoteView containingWindowWillOrderOnScreen:]
 4 CoreFoundation  __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__
 8 Foundation      -[NSNotificationCenter postNotificationName:object:userInfo:]
 9 AppKit          -[NSWindow _doWindowWillBeVisibleAsSheet:]
10 AppKit          -[NSWindow _reallyDoOrderWindowAboveOrBelow:]
13 AppKit          _NSWindowWalkOrderingGroupInternal
14 AppKit          -[NSWindow _rebuildOrderingGroup:]
16 AppKit          -[NSWindow addChildWindow:ordered:]
17 AppKit          -[NSPopover showRelativeToRect:ofView:preferredEdge:]
18 SwiftUI         PopoverBridge.updatePopover(...)
21 SwiftUI         PopoverBridge.preferencesDidChange(_:)
22 SwiftUI         NSHostingView.preferencesDidChange()
23 SwiftUICore     ViewGraph.updateOutputs(at:)
27 SwiftUI         NSHostingView.layout()
37 AppKit          ___NSViewLayout_block_invoke
56 QuartzCore      CA::Transaction::run_commit_handlers
60 AppKit          stepTransactionFlush
74 AppKit          -[NSApplication run]

注意 Glint 自己的代码不在抛出栈上。

机制 / Mechanism

  1. macOS 会把跨进程的 NSRemoteView 挂进主窗口的 ordering group —— 这些 UI 应用
    从未主动请求:文本输入光标 HUD(TextInputUI.xpc.CursorUIViewService)和
    数据检测器(SafariPlatformSupport.Helper)。两次崩溃前的日志里都能看到它们
    随着终端输入被反复 activate。
  2. 系统回收其中一个 view service 后(View service did terminate,ViewBridge
    Code=8),主进程侧的 NSRemoteView 仍留在窗口层级里。
  3. 下一个 child window 上屏时,-[NSWindow addChildWindow:ordered:]
    _rebuildOrderingGroup: 向 ordering group 内所有 view 广播
    containingWindowWillOrderOnScreen:
  4. 那个孤儿 remote view 断言失败并抛 NSInternalInconsistencyException
  5. 这个异常一路无人捕获,进程被终止。

所以触发条件是「系统回收 view service 的时机」+「此时恰好弹出一个 child window」,
应用侧完全不可控,也解释了为什么看起来随机。

触发面 / Trigger surface

main@d297ef4 上共 6 处 .popover

  • Glint/Agent/AgentPaneSummary.swift:258 — hover 350 ms 后自动弹出,无需点击
  • Glint/Chrome/ContentView.swift:1213 / 1521 / 1703
  • Glint/Chrome/SettingsView.swift:2138 / 2363

异常 reason 被 <private> 屏蔽,无法确定具体是哪一处;AgentPaneSummary 因为
hover 自动弹出,触发频率最高。不过任何 sheet / menu 也走同一条
addChildWindow: 广播路径,所以这不是某个 popover 的问题。

已验证无效的方案 / Measured and rejected

为省掉重复劳动,这两条我做了对照实验,都无效

  1. 覆盖 -[NSApplication reportException:] —— 这条路径上它从未被调用
    最小复现(SwiftUI app,从 NSView.layout() raise)两种配置都是 exit 133
    (SIGTRAP),覆盖的方法一次都没进去。异常是 uncaught,不是 reported。
  2. NSSetUncaughtExceptionHandler —— handler 返回后进程照样终止,救不回来。

顺带一个可能对其他改动有用的发现:SwiftUI app 无法通过 Info.plist
NSPrincipalClass 替换 NSApplication 子类
—— SwiftUI.runApp 在调用
NSApplicationMain 之前就已经取到了 NSApplication.shared,principal class 那时
被忽略。要换子类只能在 App.init() 里抢先 MyApplication.shared

可行的修复 / What does work

-[NSWindow addChildWindow:ordered:] 包进 ObjC 的 @try/@catch:它是包住
整个广播的最窄的一帧,而 popover / sheet / menu 全都从这里过。

对照实验(纯 ObjC,通知 observer 在 child window 上屏时 raise,栈结构与生产崩溃
逐帧对应):

结果
无 guard *** Terminating app due to uncaught exception, exit 134
有 guard 捕获,进程存活,exit 0

判定收窄到只吞这一类:异常名为 NSInternalInconsistencyException 调用栈里
ViewBridge / NSRemoteView 帧;其余一律重抛,应用自身的 bug 仍然照常崩。
吞掉时打一条 fault 日志,不静默。代价上限是一次 popover 没弹出来。

我在 fork 上已经实现并验证:312 个测试通过,Release 构建通过,新增测试覆盖判定的
命中/不命中两个方向并断言 guard 确实安装在真实启动路径上。

如果你觉得这个方向可以,我可以直接提一个 PR。 也完全理解如果你更倾向别的做法
(比如不用 swizzle)—— 上面的机制分析和实验数据应该够定位了。

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