test(hls): [PKG-HLS-SDK-01] capture scoped Firefox native exceptions

This commit is contained in:
Harvey Zhao committed 2026-09-15 17:33:49 +08:00
1 parent e0c5cf27de
commit 2802cbacc5
14 files changed
+26842 -4

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,80 @@
# PKG-HLS-SDK-01:Firefox 原生异常取证
状态仍为 doing;本检查点没有修复或关闭 HLS-CRASH-01 / HLS-PLAYBACK-01。
## 新证据与边界
Windows 的 Playwright Firefox 155.0(firefox-1543)在直接 Hls 1.5.17、
enableWorker=true、HTTP 本地样本、立即切组时捕获到原生异常。没有加载 ArtPlayer
或插件,也没有替换 SDK/worker 输出。播放器与 SDK 配置沿用已有诊断器。
- 首轮 6 次:4 passed、2 failed,两次页面崩溃都在显式 destroy 之前。
第一次保留 96 条宿主事件,第二次保留 95 条。
- 2026-09-15 09:24:38 UTC(北京时间 17:24:38),PID 29080 的 ProcDump
记录 unhandled C0000005.ACCESS_VIOLATION 并写出 3,768,909 字节转储。
SHA-256:`e1b6e514521aa5e1cdc3a4ea5b896b6fd11c7b331dcc6882fbcceff605cd1a31`。
- 转储异常线程 36484,名称 `DOM Worker`;指令地址归属实际加载的 `xul.dll`,
相对模块偏移 `0x3bc4be0`。异常参数 `[0, 8]` 表示读取地址 `0x8`。
这只是故障位置及异常元数据,**没有函数名、符号堆栈或已证明的根因**。
- 首次崩溃前最后收到 BUFFER_FLUSHED/video,主控制器 FRAG_LOADING、level 1,
音频 WAITING_TRACK,两个 SourceBuffer 均为空且 updating=false,播放时钟
0.413481、90p、10 帧。宿主 receipt phase 是 low-group,后续断言 phase 是
high-group;异步收集不能证明最后一条事件就是原生异常的直接原因。
- 后续 6 次和最终文件名隔离 smoke 2 次播放全部通过,未产生转储。它们不是修复证据。
三轮分别尝试监控 14、14、10 个经过归属筛选的进程;所有监控进程均已退出。
第一轮两个监控报告权限不足,捕获转储的监控退出码为 1;后两轮各一个监控权限不足。
因此没有声称完整覆盖全部 Firefox 子进程,也没有为此提升到管理员或 SYSTEM。
最终脚本遇到任何监控非零退出、没有目标或未停止监控会返回失败;后两轮即使播放
全通过,整体诊断仍退出 1。详细日志与指纹见[机器证据](../baselines/hls-native-validation.json)。
这次原生异常不能自动解释另一项 Hls 1.7.2 播放停滞,也不能证明所有历史页面崩溃
都具有同一原因。下一步需要匹配本构建的符号/堆栈或能区分触发路径的最小复现;
不以禁用 worker、放宽播放断言或修改 ArtPlayer destroy 顺序代替兼容修复。
## 工具、结构和运行方式
新增 Windows 专用、显式调用的外围诊断,不改动原 HLS runner 的默认路径:
| 文件 | 职责 |
| --- | --- |
| `refactor/scripts/hls-native-diagnostic.mjs` | 启动已有 runner,验证工具,发现目标,保存监控/退出/转储指纹 |
| `refactor/scripts/firefox-process-scope.mjs` | 根据存活 runner 的 PID、创建时间、路径和完整祖先链筛选 Firefox |
| `refactor/scripts/firefox-minidump.mjs` | 有边界检查地读取异常、模块、线程名;不展开堆栈、不上传 |
| 对应两个 `.test.mjs` | 误选其他浏览器、PID 复用、坏时间、祖先缺失、循环和二进制截断等回归 |
[Microsoft ProcDump 12.01](https://learn.microsoft.com/en-us/sysinternals/downloads/procdump)
从官方 `https://download.sysinternals.com/files/Procdump.zip` 下载到忽略的工具缓存。
zip SHA-256:`68e057587b0fd654efa095f76d80d633c0e5c60ea26fd3e7c0011c076bb2d00c`;
procdump64.exe SHA-256:`d1fc99ae304bd1d2bf28abeb62531da959e2431916194981b88c958fd713a8e6`。
实际 FileVersion 为 12.01,Authenticode Valid,签名者 Microsoft Corporation。
每次运行重新核对 exe 哈希、版本和签名。标准 EULA 由 `-accepteula` 接受;
未注册系统级事后调试器。它是可选本地开发工具,不是 npm 依赖或发布包内容。
Node 使用 .node-version 的 24.21.0;Yarn 保持 Classic 1.22.22;需要 PATH 中有
PowerShell 7(本机 7.6.5)。初次尝试 Windows PowerShell 5 因继承的模块环境
无法加载 Security 模块而在启动浏览器前失败,改用本机已验证的 pwsh.exe。
```sh
node --test refactor/scripts/firefox-process-scope.test.mjs refactor/scripts/firefox-minidump.test.mjs
node refactor/scripts/hls-native-diagnostic.mjs --procdump refactor/.cache/toolchains/procdump/procdump64.exe -- --version 1.5.17 --iterations 6 --controller-state --capture-before-destroy --switch-boundary immediate
node refactor/scripts/firefox-minidump.mjs <本次生成的转储路径>
```
监控只按精确 PID 使用 `-mm -e -n 1 -at 10`;不使用全局进程名等待、系统安装、
上传或 kill 选项。每个目标使用不同文件名,避免同秒崩溃覆盖证据。轮询存在发现
窗口,附加也会影响调度,不能把监控期间的通过视为无观察器通过。退出优先交给
原 runner 的 browser.close;如需取消仍在运行的监控,再核对目标身份并使用
`-cancel PID`。不结束用户浏览器、不强杀被调试进程。
解析依据是 Microsoft 的 [MINIDUMP_EXCEPTION_STREAM](https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_exception_stream)、
[MINIDUMP_EXCEPTION](https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_exception) 和
[MINIDUMP_MODULE](https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_module) 定义。
原始转储只保留在本机忽略缓存;提交元数据、有限事件和完整性指纹,不提交进程内存。
## 验证与回退
6 项针对筛选与二进制边界的回归通过;五个新脚本只读 ESLint 通过。
两轮实现检查及最终 smoke 的区别和原始失败都保留,没有把早期报告重写成最终脚本运行。
没有播放器源码、公开 API/类型、依赖清单、锁文件、版本、分发产物修改,无需重建播放器。
撤销本检查点仅移除可选原生诊断与相关说明;不推送、部署、发布或上报第三方 issue。
+7
View File
@@ -109,3 +109,10 @@ worker 切源/清理覆盖两个 SDK 与新旧核心/插件四组合;候选插
随后直接1.5.17两次切组崩溃均在destroy之前;可选观察器向Node复制事件后, 随后直接1.5.17两次切组崩溃均在destroy之前;可选观察器向Node复制事件后,
其中一次页面失联仍保留97条事件。此处有最后收到的音视频flush状态,尚无原生堆栈。 其中一次页面失联仍保留97条事件。此处有最后收到的音视频flush状态,尚无原生堆栈。
详情及后续取证方向见[控制器检查点](changes/2026-09-15-PKG-HLS-SDK-01-controller-state.md)。 详情及后续取证方向见[控制器检查点](changes/2026-09-15-PKG-HLS-SDK-01-controller-state.md)。
## 可选 Windows 原生异常诊断
PKG-HLS-SDK-01 已捕获直接 Hls 1.5.17 的 Firefox DOM Worker 原生异常,
xul.dll+0x3bc4be0、C0000005 读取0x8;尚缺符号堆栈与根因,风险继续 open。
范围隔离、工具版本、重跑命令、监控权限缺口与后续绿色对照的限制见
[原生取证记录](changes/2026-09-15-PKG-HLS-SDK-01-native-exception.md)。
+1 -1
View File
@@ -573,7 +573,7 @@
- PKG-HLS-02: [记录](hls-validation.md) [记录](baselines/hls-sdk.json) [记录](baselines/hls-validation.json) [记录](changes/2026-09-11-PKG-HLS-02-tests.md) - PKG-HLS-02: [记录](hls-validation.md) [记录](baselines/hls-sdk.json) [记录](baselines/hls-validation.json) [记录](changes/2026-09-11-PKG-HLS-02-tests.md)
- PKG-HLS-03: [记录](changes/2026-09-12-PKG-HLS-03-modules.md) [记录](baselines/hls-modules-validation.json) [记录](hls-validation.md) - PKG-HLS-03: [记录](changes/2026-09-12-PKG-HLS-03-modules.md) [记录](baselines/hls-modules-validation.json) [记录](hls-validation.md)
- PKG-HLS-04: [记录](changes/2026-09-12-PKG-HLS-04-types.md) [记录](baselines/hls-types-validation.json) [记录](hls-validation.md) - PKG-HLS-04: [记录](changes/2026-09-12-PKG-HLS-04-types.md) [记录](baselines/hls-types-validation.json) [记录](hls-validation.md)
- PKG-HLS-SDK-01: [记录](changes/2026-09-12-PKG-HLS-SDK-01-integration.md) [记录](baselines/hls-sdk-validation.json) [记录](baselines/hls-sdk-diagnostics.json) [记录](changes/2026-09-14-PKG-HLS-SDK-01-http-teardown.md) [记录](baselines/hls-http-teardown-validation.json) [记录](changes/2026-09-15-PKG-HLS-SDK-01-switch-boundary.md) [记录](baselines/hls-switch-boundary-validation.json) [记录](changes/2026-09-15-PKG-HLS-SDK-01-sdk-sequence.md) [记录](baselines/hls-sdk-sequence-validation.json) [记录](changes/2026-09-15-PKG-HLS-SDK-01-controller-state.md) [记录](baselines/hls-controller-validation.json) - PKG-HLS-SDK-01: [记录](changes/2026-09-12-PKG-HLS-SDK-01-integration.md) [记录](baselines/hls-sdk-validation.json) [记录](baselines/hls-sdk-diagnostics.json) [记录](changes/2026-09-14-PKG-HLS-SDK-01-http-teardown.md) [记录](baselines/hls-http-teardown-validation.json) [记录](changes/2026-09-15-PKG-HLS-SDK-01-switch-boundary.md) [记录](baselines/hls-switch-boundary-validation.json) [记录](changes/2026-09-15-PKG-HLS-SDK-01-sdk-sequence.md) [记录](baselines/hls-sdk-sequence-validation.json) [记录](changes/2026-09-15-PKG-HLS-SDK-01-controller-state.md) [记录](baselines/hls-controller-validation.json) [记录](changes/2026-09-15-PKG-HLS-SDK-01-native-exception.md) [记录](baselines/hls-native-validation.json)
- PKG-DASH-01: [记录](changes/2026-09-12-PKG-DASH-01-contract.md) [记录](baselines/dash-control-contract.md) [记录](baselines/dash-control-release.json) - PKG-DASH-01: [记录](changes/2026-09-12-PKG-DASH-01-contract.md) [记录](baselines/dash-control-contract.md) [记录](baselines/dash-control-release.json)
- PKG-DASH-02: [记录](changes/2026-09-12-PKG-DASH-02-tests.md) [记录](dash-validation.md) [记录](baselines/dash-validation.json) - PKG-DASH-02: [记录](changes/2026-09-12-PKG-DASH-02-tests.md) [记录](dash-validation.md) [记录](baselines/dash-validation.json)
- PKG-DASH-03: [记录](changes/2026-09-12-PKG-DASH-03-runtime.md) [记录](baselines/dash-runtime.json) [记录](dash-validation.md) - PKG-DASH-03: [记录](changes/2026-09-12-PKG-DASH-03-runtime.md) [记录](baselines/dash-runtime.json) [记录](dash-validation.md)
+16
View File
@@ -1,5 +1,21 @@
# 进度与证据 # 进度与证据
## PKG-HLS-SDK-01 原生异常取证检查点
直接 Hls 1.5.17/worker/HTTP 场景六次中两次在 destroy 前页面崩溃;
ProcDump 捕获一次 C0000005,异常线程 DOM Worker,xul.dll+0x3bc4be0,
读取地址0x8。两次页面崩溃分别保留96/95条宿主事件。仍缺符号堆栈/根因,
不把该证据等同于另一项1.7.2停滞,也不据此改动播放器API/SDK配置。
新增限定runner子进程的可选Windows监控与有边界检查的转储元数据读取。
6项回归与lint通过;后续六次及最终两次播放通过,但有部分进程附加权限不足,
整体诊断保持失败。所有监控已退出;转储只保留本机,未上传。详见
[记录](changes/2026-09-15-PKG-HLS-SDK-01-native-exception.md)与[证据](baselines/hls-native-validation.json)。
任务/风险仍未关闭;207 done、21 doing、43 todo,总271不变。
没有生产源码、类型、锁文件、包版本或分发修改;下一步需要匹配构建的原生符号
或有区分力的最小复现。没有推送、部署或发布。
## PKG-HLS-SDK-01 控制器与原生诊断检查点 ## PKG-HLS-SDK-01 控制器与原生诊断检查点
原Firefox1.7.2切组测试20次通过,不关闭历史停滞。新增可选controller-state, 原Firefox1.7.2切组测试20次通过,不关闭历史停滞。新增可选controller-state,
+4 -2
View File
@@ -2817,11 +2817,13 @@
"refactor/changes/2026-09-15-PKG-HLS-SDK-01-sdk-sequence.md", "refactor/changes/2026-09-15-PKG-HLS-SDK-01-sdk-sequence.md",
"refactor/baselines/hls-sdk-sequence-validation.json", "refactor/baselines/hls-sdk-sequence-validation.json",
"refactor/changes/2026-09-15-PKG-HLS-SDK-01-controller-state.md", "refactor/changes/2026-09-15-PKG-HLS-SDK-01-controller-state.md",
"refactor/baselines/hls-controller-validation.json" "refactor/baselines/hls-controller-validation.json",
"refactor/changes/2026-09-15-PKG-HLS-SDK-01-native-exception.md",
"refactor/baselines/hls-native-validation.json"
], ],
"compatibleResolution": "直接原生 video(无 ArtPlayer/插件)和候选核心已有 page crashed;候选核心无观察器也复现。继续区分 SDK、Firefox/Playwright 与媒体销毁,不静默改核心 destroy 顺序或调用方 SDK 配置。", "compatibleResolution": "直接原生 video(无 ArtPlayer/插件)和候选核心已有 page crashed;候选核心无观察器也复现。继续区分 SDK、Firefox/Playwright 与媒体销毁,不静默改核心 destroy 顺序或调用方 SDK 配置。",
"closureCriteria": "取得可解释的崩溃诊断或直接 SDK 对照;验证处置和新旧核心/worker/分组销毁矩阵,不能仅以再次通过关闭。", "closureCriteria": "取得可解释的崩溃诊断或直接 SDK 对照;验证处置和新旧核心/worker/分组销毁矩阵,不能仅以再次通过关闭。",
"workspaceState": "2026-09-14 direct HTTP/no ArtPlayer/plugin/Worker observer:35 diagnostics,31 passed4 failed. Worker reset-first fails1/10 and1/5; no-worker10/10 and optional pre-destroy snapshot5/5 pass. SDK-first also fails2/5. Three phase-recorded crashes occur during final-state collection after destroy returned. Installed Firefox TargetRegistry observer receives oop-frameloader-crashed; no native stack/root cause established. Do not substitute no-worker/snapshot success or change core teardown order; risk remains open. 2026-09-15 sequence: direct native video first 1.5.17 case reports page crash in high-group phase before explicit destroy; no earlier SDK case, ArtPlayer, plugin, route, SDK logger or Worker observer. Three ordered sequences plus one default invocation total 8 passed/1 failed. Native cause remains unproven; do not restrict investigation to teardown or merge distinct signals into a proven shared cause. 2026-09-15 installed Firefox 1543 local omni.ja buildconfig includes --disable-crashreporter; playwright.cfg locks crash reporter off. No native stack collected. Environment-variable-only capture cannot restore a compile-disabled component; next evidence needs scoped Windows native debugging or a separately validated browser build. Later in this checkpoint, direct 1.5.17 crashed twice before destroy (one first-in-sequence); no ArtPlayer/plugin. Optional Node event queue retained 97 events in the second crash, ending during overlapping audio/video flush and high selection at media time 0.381837. Crash receipt was low-group; following assertion failure was high-group. No native module/exception/stack established." "workspaceState": "2026-09-14 direct HTTP/no ArtPlayer/plugin/Worker observer:35 diagnostics,31 passed4 failed. Worker reset-first fails1/10 and1/5; no-worker10/10 and optional pre-destroy snapshot5/5 pass. SDK-first also fails2/5. Three phase-recorded crashes occur during final-state collection after destroy returned. Installed Firefox TargetRegistry observer receives oop-frameloader-crashed; no native stack/root cause established. Do not substitute no-worker/snapshot success or change core teardown order; risk remains open. 2026-09-15 sequence: direct native video first 1.5.17 case reports page crash in high-group phase before explicit destroy; no earlier SDK case, ArtPlayer, plugin, route, SDK logger or Worker observer. Three ordered sequences plus one default invocation total 8 passed/1 failed. Native cause remains unproven; do not restrict investigation to teardown or merge distinct signals into a proven shared cause. 2026-09-15 installed Firefox 1543 local omni.ja buildconfig includes --disable-crashreporter; playwright.cfg locks crash reporter off. No native stack collected. Environment-variable-only capture cannot restore a compile-disabled component; next evidence needs scoped Windows native debugging or a separately validated browser build. Later in this checkpoint, direct 1.5.17 crashed twice before destroy (one first-in-sequence); no ArtPlayer/plugin. Optional Node event queue retained 97 events in the second crash, ending during overlapping audio/video flush and high selection at media time 0.381837. Crash receipt was low-group; following assertion failure was high-group. No native module/exception/stack established. 2026-09-15 native capture: direct Hls 1.5.17, worker=true, HTTP, immediate switch yielded two pre-destroy page crashes in six iterations. ProcDump 12.01 captured one unhandled C0000005 read at address 0x8, exception thread named DOM Worker, xul.dll+0x3bc4be0. First/second page crashes retained 96/95 host events. No symbol stack or root cause; do not equate the separate 1.7.2 playback stall. Later six and two iterations pass without dumps, but native monitoring is partial due to permission-denied subprocesses; all monitors exited. Risk remains open."
}, },
{ {
"id": "HLS-PLAYBACK-01", "id": "HLS-PLAYBACK-01",
+65
View File
@@ -0,0 +1,65 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
// Read only exception/module/thread-name metadata; this is not a stack unwinder.
export function inspectFirefoxMinidump(bytes) {
const region = (offset, size) => {
assert(Number.isSafeInteger(offset) && Number.isSafeInteger(size) && offset >= 0 && size >= 0 && offset + size <= bytes.length, 'Truncated minidump region')
return bytes.subarray(offset, offset + size)
}
const u32 = offset => region(offset, 4).readUInt32LE()
const u64 = offset => region(offset, 8).readBigUInt64LE()
const hex = value => `0x${value.toString(16)}`
const string = (offset) => {
const size = u32(offset)
assert.equal(size % 2, 0, 'Invalid UTF-16 minidump string')
return region(offset + 4, size).toString('utf16le')
}
assert.equal(region(0, 4).toString('ascii'), 'MDMP', 'Invalid minidump signature')
const count = u32(8)
assert(count <= 4096, 'Excessive minidump stream count')
const streams = Array.from({ length: count }, (_, i) => {
const at = u32(12) + i * 12
const stream = { type: u32(at), size: u32(at + 4), rva: u32(at + 8) }
region(stream.rva, stream.size)
return stream
})
const stream = (type, minimum) => {
const matches = streams.filter(item => item.type === type)
assert(matches.length === 1 && matches[0].size >= minimum, `Missing, duplicated or truncated stream ${type}`)
return matches[0]
}
const exceptionStream = stream(6, 168)
const e = exceptionStream.rva
const parameters = u32(e + 32)
assert(parameters <= 15, 'Invalid exception parameter count')
const exception = { threadId: u32(e), code: hex(u32(e + 8)), flags: u32(e + 12), address: hex(u64(e + 24)), parameters: Array.from({ length: parameters }, (_, i) => hex(u64(e + 40 + i * 8))) }
const moduleStream = stream(4, 4)
const moduleCount = u32(moduleStream.rva)
assert(moduleCount * 108 + 4 <= moduleStream.size, 'Truncated module list')
const modules = Array.from({ length: moduleCount }, (_, i) => {
const at = moduleStream.rva + 4 + 108 * i
return { name: string(u32(at + 20)), base: hex(u64(at)), size: u32(at + 8), codeView: region(u32(at + 80), u32(at + 76)).toString('hex') }
})
const faultModule = modules.find(module => BigInt(module.base) <= BigInt(exception.address) && BigInt(exception.address) < BigInt(module.base) + BigInt(module.size))
let threadName
if (streams.some(item => item.type === 24)) {
const names = stream(24, 4)
const count = u32(names.rva)
assert(count * 12 + 4 <= names.size, 'Truncated thread name list')
for (let i = 0; i < count; i++) {
const at = names.rva + 4 + i * 12
if (u32(at) === exception.threadId)
threadName = string(Number(u64(at + 4)))
}
}
return { exception, threadName, faultModule, moduleOffset: faultModule && hex(BigInt(exception.address) - BigInt(faultModule.base)), moduleCount, stackResolved: false }
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
assert.equal(process.argv.length, 3, 'Usage: node refactor/scripts/firefox-minidump.mjs <dump>')
console.log(JSON.stringify(inspectFirefoxMinidump(fs.readFileSync(process.argv[2])), null, 2))
}
@@ -0,0 +1,60 @@
import assert from 'node:assert/strict'
import { Buffer } from 'node:buffer'
// eslint-disable-next-line test/no-import-node-test -- Exercise binary diagnostic metadata without requiring a real browser crash.
import { test } from 'node:test'
import { inspectFirefoxMinidump } from './firefox-minidump.mjs'
function fixture() {
const bytes = Buffer.alloc(512)
bytes.write('MDMP')
bytes.writeUInt32LE(3, 8)
bytes.writeUInt32LE(32, 12)
for (const [i, type, size, rva] of [[0, 6, 168, 80], [1, 4, 112, 248], [2, 24, 16, 360]]) {
bytes.writeUInt32LE(type, 32 + i * 12)
bytes.writeUInt32LE(size, 36 + i * 12)
bytes.writeUInt32LE(rva, 40 + i * 12)
}
bytes.writeUInt32LE(123, 80)
bytes.writeUInt32LE(0xC0000005, 88)
bytes.writeBigUInt64LE(0x10000000008n, 104)
bytes.writeUInt32LE(2, 112)
bytes.writeBigUInt64LE(8n, 128)
bytes.writeUInt32LE(1, 248)
bytes.writeBigUInt64LE(0x10000000000n, 252)
bytes.writeUInt32LE(1024, 260)
bytes.writeUInt32LE(380, 272)
bytes.writeUInt32LE(14, 380)
bytes.write('xul.dll', 384, 'utf16le')
bytes.writeUInt32LE(1, 360)
bytes.writeUInt32LE(123, 364)
bytes.writeBigUInt64LE(410n, 368)
bytes.writeUInt32LE(20, 410)
bytes.write('DOM Worker', 414, 'utf16le')
return bytes
}
test('exception metadata identifies the module and thread without inventing a stack', () => {
const result = inspectFirefoxMinidump(fixture())
assert.equal(result.exception.code, '0xc0000005')
assert.deepEqual(result.exception.parameters, ['0x0', '0x8'])
assert.equal(result.faultModule.name, 'xul.dll')
assert.equal(result.moduleOffset, '0x8')
assert.equal(result.threadName, 'DOM Worker')
assert.equal(result.stackResolved, false)
})
test('invalid binary regions, counts and encoding are rejected', () => {
assert.throws(() => inspectFirefoxMinidump(Buffer.alloc(4)), /signature/)
assert.throws(() => inspectFirefoxMinidump(fixture().subarray(0, 100)), /Truncated/)
for (const [offset, value, message] of [[8, 5000, /stream count/], [112, 16, /parameter count/], [248, 1000, /module list/], [360, 1000, /thread name list/], [380, 15, /UTF-16/]]) {
const bytes = fixture()
bytes.writeUInt32LE(value, offset)
assert.throws(() => inspectFirefoxMinidump(bytes), message)
}
})
test('addresses outside all module ranges remain unattributed', () => {
const bytes = fixture()
bytes.writeBigUInt64LE(8n, 104)
assert.equal(inspectFirefoxMinidump(bytes).faultModule, undefined)
})
@@ -0,0 +1,25 @@
import path from 'node:path'
// Require a live, unchanged runner and a complete creation-ordered ancestry chain.
export function selectFirefoxProcesses(rows, root, executable) {
const samePath = value => typeof value === 'string' && path.win32.normalize(value).toLowerCase() === path.win32.normalize(executable).toLowerCase()
const byId = new Map(rows.map(row => [row.pid, row]))
const currentRoot = byId.get(root.pid)
if (!currentRoot || !Number.isFinite(Date.parse(root.created)) || currentRoot.created !== root.created || currentRoot.executable !== root.executable)
return []
return rows.filter((row) => {
if (row.pid === root.pid || !samePath(row.executable))
return false
const seen = new Set([row.pid])
let child = row
while (child.parent !== root.pid) {
const parent = byId.get(child.parent)
if (!parent || seen.has(parent.pid) || !samePath(parent.executable) || !Number.isFinite(Date.parse(parent.created)) || Date.parse(parent.created) > Date.parse(child.created))
return false
seen.add(parent.pid)
child = parent
}
return Number.isFinite(Date.parse(row.created)) && Number.isFinite(Date.parse(child.created))
&& Date.parse(child.created) >= Date.parse(root.created)
})
}
@@ -0,0 +1,24 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Native debugger target selection must be checked independently of Windows attachment.
import { test } from 'node:test'
import { selectFirefoxProcesses } from './firefox-process-scope.mjs'
const executable = 'C:\\test\\firefox.exe'
const root = { pid: 1, parent: 0, created: '2026-09-15T00:00:00.000Z', executable: 'C:\\node.exe' }
const row = (pid, parent, extra = {}) => ({ pid, parent, created: '2026-09-15T00:00:01.000Z', executable, ...extra })
test('only the diagnostic runner own Firefox tree is selected', () => {
const rows = [root, row(2, 1), row(3, 2), row(4, 99), row(5, 4), row(6, 1, { executable: 'C:\\user\\firefox.exe' }), row(7, 6), row(8, 2, { executable: 'c:/TEST/firefox.exe' })]
assert.deepEqual(selectFirefoxProcesses(rows, root, executable).map(item => item.pid), [2, 3, 8])
})
test('missing and reused runner PIDs reject every target', () => {
assert.deepEqual(selectFirefoxProcesses([row(2, 1)], root, executable), [])
assert.deepEqual(selectFirefoxProcesses([{ ...root, created: '2026-09-15T00:00:02.000Z' }, row(2, 1)], root, executable), [])
assert.deepEqual(selectFirefoxProcesses([{ ...root, executable: 'C:\\other.exe' }, row(2, 1)], root, executable), [])
})
test('reused ancestor PIDs, cycles, missing ancestry and invalid creation times fail closed', () => {
const rows = [root, row(2, 1, { created: '2026-09-15T00:00:03.000Z' }), row(3, 2), row(4, 5), row(5, 4), row(6, 88), row(7, 1, { created: 'invalid' }), row(8, 1, { created: '2026-09-14T00:00:00.000Z' }), row(9, 7)]
assert.deepEqual(selectFirefoxProcesses(rows, root, executable).map(item => item.pid), [2])
})
+130
View File
@@ -0,0 +1,130 @@
import assert from 'node:assert/strict'
import { execFile, spawn } from 'node:child_process'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { setTimeout } from 'node:timers/promises'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import { firefox } from '@playwright/test'
import { selectFirefoxProcesses } from './firefox-process-scope.mjs'
const execute = promisify(execFile)
const repo = fileURLToPath(new URL('../../', import.meta.url))
const sha256 = filename => createHash('sha256').update(fs.readFileSync(filename)).digest('hex')
const powershell = script => execute('pwsh.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { windowsHide: true, timeout: 15000, maxBuffer: 4 * 1024 * 1024 })
async function snapshot() {
const { stdout } = await powershell('Get-CimInstance Win32_Process -Filter "Name = \'firefox.exe\' OR Name = \'node.exe\'" | ForEach-Object { [pscustomobject]@{ pid=[int]$_.ProcessId; parent=[int]$_.ParentProcessId; created=$_.CreationDate.ToUniversalTime().ToString("o"); executable=$_.ExecutablePath } } | ConvertTo-Json -Compress')
const rows = JSON.parse(stdout)
return Array.isArray(rows) ? rows : [rows]
}
async function main() {
assert.equal(process.platform, 'win32', 'ProcDump diagnostic requires Windows')
assert.equal(process.versions.node, fs.readFileSync(path.join(repo, '.node-version'), 'utf8').trim().replace(/^v/, ''))
const [toolOption, toolPath, separator, ...args] = process.argv.slice(2)
assert(toolOption === '--procdump' && toolPath && separator === '--', 'Usage: node refactor/scripts/hls-native-diagnostic.mjs --procdump <procdump64.exe> -- <hls-sdk-diagnostic options>')
const tool = path.resolve(toolPath)
assert.equal(sha256(tool), 'd1fc99ae304bd1d2bf28abeb62531da959e2431916194981b88c958fd713a8e6', 'Use the verified ProcDump 12.01 executable')
// Paths travel through an environment variable, never through PowerShell source text.
const signature = await execute('pwsh.exe', ['-NoProfile', '-NonInteractive', '-Command', '$s = Get-AuthenticodeSignature -LiteralPath $env:ARTPLAYER_PROCDUMP; [pscustomobject]@{status=$s.Status.ToString(); signer=$s.SignerCertificate.Subject; version=(Get-Item -LiteralPath $env:ARTPLAYER_PROCDUMP).VersionInfo.FileVersion} | ConvertTo-Json -Compress'], { windowsHide: true, timeout: 15000, env: { ...process.env, ARTPLAYER_PROCDUMP: tool } })
const identity = JSON.parse(signature.stdout)
assert.equal(identity.status, 'Valid')
assert(identity.signer.startsWith('CN=Microsoft Corporation,'))
assert.equal(identity.version, '12.01')
const directory = fs.mkdtempSync(path.join(repo, 'refactor/.cache/hls-native-'))
const runnerPath = path.join(repo, 'refactor/scripts/hls-sdk-diagnostic.mjs')
const executable = firefox.executablePath()
const report = { started: new Date().toISOString(), directory, args, tool: { ...identity, sha256: sha256(tool) }, firefox: { executable, sha256: sha256(executable) }, runnerSHA256: sha256(runnerPath), wrapperSHA256: sha256(fileURLToPath(import.meta.url)), scopeSHA256: sha256(fileURLToPath(new URL('./firefox-process-scope.mjs', import.meta.url))), attachments: [], errors: [] }
const handles = []
const runnerLog = fs.openSync(path.join(directory, 'runner.log'), 'wx')
const runner = spawn(process.execPath, [runnerPath, ...args], { cwd: repo, windowsHide: true, stdio: ['ignore', runnerLog, runnerLog] })
let finished = false
const completion = new Promise((resolve) => {
runner.on('error', (error) => {
report.errors.push(String(error))
finished = true
resolve()
})
runner.on('close', (code, signal) => {
report.runnerExit = { code, signal }
finished = true
resolve()
})
})
console.log(`Native diagnostic: ${directory}; runner PID ${runner.pid}`)
try {
const root = (await snapshot()).find(row => row.pid === runner.pid)
assert(root, 'Diagnostic runner exited before its process identity could be verified')
report.root = root
const seen = new Set()
// Completion is updated by the child-process event handlers.
// eslint-disable-next-line no-unmodified-loop-condition -- The runner close event ends observation.
while (!finished) {
const rows = await snapshot()
for (const target of selectFirefoxProcesses(rows, root, executable)) {
const key = `${target.pid}:${target.created}`
if (seen.has(key))
continue
seen.add(key)
assert(seen.size <= 200, 'Native diagnostic process limit reached')
const entry = { ...target, started: new Date().toISOString(), log: `procdump-${target.pid}-${handles.length}.log`, dump: `firefox-${target.pid}-${handles.length}.dmp` }
report.attachments.push(entry)
const log = fs.openSync(path.join(directory, entry.log), 'wx')
const child = spawn(tool, ['-accepteula', '-mm', '-e', '-n', '1', '-at', '10', String(target.pid), path.join(directory, entry.dump)], { windowsHide: true, stdio: ['ignore', log, log] })
const handle = { target, child, entry, log, finished: false }
handle.completion = new Promise((resolve) => {
child.on('error', (error) => {
entry.error = String(error)
handle.finished = true
resolve()
})
child.on('close', (code, signal) => {
entry.exit = { code, signal }
handle.finished = true
resolve()
})
})
handles.push(handle)
}
await setTimeout(250)
}
}
catch (error) {
report.errors.push(String(error))
}
finally {
// The bounded existing runner owns browser shutdown. Never kill the debuggee.
await completion
for (const handle of handles) {
if (!handle.finished) {
try {
const live = (await snapshot()).find(row => row.pid === handle.target.pid)
if (live?.created === handle.target.created && live.executable === handle.target.executable) {
const result = await execute(tool, ['-cancel', String(handle.target.pid)], { windowsHide: true, timeout: 15000 })
handle.entry.cancel = result.stdout
}
}
catch (error) {
handle.entry.cancelError = String(error)
}
await Promise.race([handle.completion, setTimeout(15000)])
}
handle.entry.monitorStopped = handle.finished
fs.closeSync(handle.log)
}
fs.closeSync(runnerLog)
report.finished = new Date().toISOString()
report.monitoringHadFailures = !handles.length || handles.some(handle => handle.entry.error || handle.entry.exit?.code !== 0 || !handle.finished)
report.dumps = fs.readdirSync(directory).filter(name => name.endsWith('.dmp')).map(name => ({ name, bytes: fs.statSync(path.join(directory, name)).size, sha256: sha256(path.join(directory, name)) }))
fs.writeFileSync(path.join(directory, 'report.json'), `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ runnerExit: report.runnerExit, attachments: handles.length, dumps: report.dumps, errors: report.errors, monitorsStopped: handles.every(handle => handle.finished), monitoringHadFailures: report.monitoringHadFailures }))
}
process.exitCode = report.runnerExit?.code === 0 && !report.errors.length && !report.monitoringHadFailures ? 0 : 1
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
+3 -1
View File
@@ -2156,7 +2156,9 @@
"changes/2026-09-15-PKG-HLS-SDK-01-sdk-sequence.md", "changes/2026-09-15-PKG-HLS-SDK-01-sdk-sequence.md",
"baselines/hls-sdk-sequence-validation.json", "baselines/hls-sdk-sequence-validation.json",
"changes/2026-09-15-PKG-HLS-SDK-01-controller-state.md", "changes/2026-09-15-PKG-HLS-SDK-01-controller-state.md",
"baselines/hls-controller-validation.json" "baselines/hls-controller-validation.json",
"changes/2026-09-15-PKG-HLS-SDK-01-native-exception.md",
"baselines/hls-native-validation.json"
] ]
}, },
{ {
+8
View File
@@ -42,3 +42,11 @@ Monaco 浏览器资产匹配的精确 API 类型。只使用 type import,不
ENG-01 的 [npm 验证报告](baselines/toolchain-validation.json) 是切换前历史证据,不代表 Yarn 已验证。[Yarn 验证报告](baselines/yarn-validation.json) 记录最终干净冻结安装、20 项 Node 测试、21 库包 63 产物及文档站构建全部通过;63 产物 SHA-256 与 ENG-01 完全相同。保留的旧锁条目版本变化为零,9 个 workspace runtime 依赖解析与 npm 基线一致。搜索 peer 警告由 SITE-05 接续验证。 ENG-01 的 [npm 验证报告](baselines/toolchain-validation.json) 是切换前历史证据,不代表 Yarn 已验证。[Yarn 验证报告](baselines/yarn-validation.json) 记录最终干净冻结安装、20 项 Node 测试、21 库包 63 产物及文档站构建全部通过;63 产物 SHA-256 与 ENG-01 完全相同。保留的旧锁条目版本变化为零,9 个 workspace runtime 依赖解析与 npm 基线一致。搜索 peer 警告由 SITE-05 接续验证。
构建通过不代表运行时、类型、真实浏览器或 npm 发布验收已完成。Chrome 不可用时按 release-reviews.md 使用内置浏览器并注明实际环境。 构建通过不代表运行时、类型、真实浏览器或 npm 发布验收已完成。Chrome 不可用时按 release-reviews.md 使用内置浏览器并注明实际环境。
## 可选原生浏览器诊断工具
PKG-HLS-SDK-01 将官方 Microsoft ProcDump 12.01 解压到忽略的
`refactor/.cache/toolchains/procdump`,运行时核对精确哈希和 Authenticode。
用途仅为本次HLS测试的Firefox子进程异常取证,配合PowerShell 7.6.5;
不进入npm依赖或CI默认安装,不更改Yarn/锁文件。来源、校验值、许可接受、
权限限制和复现命令见[原生取证记录](changes/2026-09-15-PKG-HLS-SDK-01-native-exception.md)。
+11
View File
@@ -467,3 +467,14 @@ The real docs AMD loader must serve all 76 language files. Expected token offset
types and multiline state are checked in each engine. Fixtures are regenerated by types and multiline state are checked in each engine. Fixtures are regenerated by
`yarn verify:monaco-basic-sources`; see `scripts/site-vendor/monaco/README.md` for `yarn verify:monaco-basic-sources`; see `scripts/site-vendor/monaco/README.md` for
the fixed archive, historical compiler and upstream coverage limitations. the fixed archive, historical compiler and upstream coverage limitations.
## Optional Firefox native capture on Windows
`node refactor/scripts/hls-native-diagnostic.mjs --procdump <verified-procdump64.exe> -- <hls-sdk-diagnostic options>`
runs the existing diagnostic as its own child and monitors only verified Firefox descendants.
It requires Node from .node-version, PowerShell 7, and the exact signed ProcDump 12.01 binary.
This does not change regular browser tests. Nonzero monitor exits require review even if
playback passes; native attachment can affect timing and permission failures leave gaps.
All dumps stay in ignored local cache. `firefox-minidump.mjs` reads exception/module/thread
metadata but does not resolve a native stack. See
[the native evidence and commands](../../refactor/changes/2026-09-15-PKG-HLS-SDK-01-native-exception.md).