mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
test(hls): [PKG-HLS-SDK-01] capture scoped Firefox native exceptions
This commit is contained in:
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。
|
||||
@@ -109,3 +109,10 @@ worker 切源/清理覆盖两个 SDK 与新旧核心/插件四组合;候选插
|
||||
随后直接1.5.17两次切组崩溃均在destroy之前;可选观察器向Node复制事件后,
|
||||
其中一次页面失联仍保留97条事件。此处有最后收到的音视频flush状态,尚无原生堆栈。
|
||||
详情及后续取证方向见[控制器检查点](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
@@ -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-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-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-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)
|
||||
|
||||
@@ -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 控制器与原生诊断检查点
|
||||
|
||||
原Firefox1.7.2切组测试20次通过,不关闭历史停滞。新增可选controller-state,
|
||||
|
||||
+4
-2
@@ -2817,11 +2817,13 @@
|
||||
"refactor/changes/2026-09-15-PKG-HLS-SDK-01-sdk-sequence.md",
|
||||
"refactor/baselines/hls-sdk-sequence-validation.json",
|
||||
"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 配置。",
|
||||
"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",
|
||||
|
||||
@@ -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])
|
||||
})
|
||||
@@ -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
@@ -2156,7 +2156,9 @@
|
||||
"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"
|
||||
"baselines/hls-controller-validation.json",
|
||||
"changes/2026-09-15-PKG-HLS-SDK-01-native-exception.md",
|
||||
"baselines/hls-native-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 接续验证。
|
||||
|
||||
构建通过不代表运行时、类型、真实浏览器或 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)。
|
||||
@@ -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
|
||||
`yarn verify:monaco-basic-sources`; see `scripts/site-vendor/monaco/README.md` for
|
||||
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).
|
||||
Reference in new issue
Block a user