react-native-update 10.51.0 → 10.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NATIVE_CHECKUPDATE_DESIGN.md +108 -0
- package/NATIVE_CHECK_FOLLOWUPS.md +46 -0
- package/README-CN.md +2 -0
- package/README.md +2 -0
- package/android/build.gradle +10 -0
- package/android/src/main/java/cn/reactnative/modules/update/CrashRescue.java +115 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +259 -26
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +209 -19
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +6 -0
- package/harmony/pushy/src/main/ets/DownloadTask.ts +381 -72
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +32 -2
- package/harmony/pushy/src/main/ets/UpdateContext.ts +5 -0
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +308 -25
- package/ios/RCTPushy/RCTPushyDownloader.mm +361 -74
- package/package.json +1 -1
- package/src/client.ts +8 -0
- package/src/type.ts +6 -0
|
@@ -420,3 +420,111 @@ afterDownload)`:本地 silent 策略 或 响应标记 forceBoot 即激活。
|
|
|
420
420
|
`binding.config.forceBoot` 合成进下发 config(灰度与全量两分支),绑定列表
|
|
421
421
|
接口补 select config;pushy-admin / cresc-admin 发布菜单加"全量+强制启动
|
|
422
422
|
(救砖)",已绑定行显示⚡标记 + 切换项(重发同绑定翻转标记)。
|
|
423
|
+
|
|
424
|
+
## 11. 崩溃时刻救援 + 断点续传(10.52.0,补"启动即崩"的架构缺口)
|
|
425
|
+
|
|
426
|
+
### 11.1 缺口(2026-08-12 Android 真机实测,见 NATIVE_CHECK_FOLLOWUPS.md)
|
|
427
|
+
|
|
428
|
+
§10 的编排器救得了"markSuccess 后 10s 才崩"的砖(原生 commit 领先崩溃点
|
|
429
|
+
567ms),救不了"启动 230ms 即崩"的砖:5s 延迟 + 检查/下载本身 ~4.4s,而 JS
|
|
430
|
+
崩溃杀掉整个进程,该轮进度全部丢弃——重启多少次都不收敛。
|
|
431
|
+
|
|
432
|
+
根因不是延迟参数,是**时序**:编排器的救援窗口永远排在崩溃点之后。修复原则
|
|
433
|
+
只有两条,满足其一即可——把救援挪到 JS 执行之前,或放进不跑 JS 的执行上下
|
|
434
|
+
文。本节方案属于前者的极限形式:**崩溃发生的那一刻,进程还活着,JS 已永远
|
|
435
|
+
不会再跑——这是天然的、无误判的救援窗口。**
|
|
436
|
+
|
|
437
|
+
### 11.2 选型:crash-handler hold,弃用启动阻塞与本地熔断
|
|
438
|
+
|
|
439
|
+
- **主机制:uncaught handler 扣住进程完成救援。** 崩溃本身就是触发信号,
|
|
440
|
+
确定性 100%:不需要崩溃环检测(marker/退出原因/计数阈值)那套状态机,也
|
|
441
|
+
没有"误判阻塞健康用户启动"的脚枪。首次崩溃当场救,修复在垂死会话末尾
|
|
442
|
+
commit,下次启动零阻塞直接进修复版——比任何检测方案快一个启动身位。
|
|
443
|
+
- **启动阻塞式检查:否决。** 它必须依赖检测态(至少崩 2-3 次才敢升级),
|
|
444
|
+
误判代价是惩罚健康启动;iOS 还要与 watchdog 抢预算。crash-handler 在
|
|
445
|
+
触发确定性、生效速度、健康路径成本三个维度全面占优。
|
|
446
|
+
- **本地熔断(Phase 2/Rung 1,自动 reset 回内置包):本版不做**(维护者
|
|
447
|
+
2026-08-12 裁定)。取舍记录:熔断能自治救"坏热更包本身"类砖,但需要
|
|
448
|
+
检测态且有"多计数毁好版本"的历史教训(§7、Phase 2 评审);crash-rescue
|
|
449
|
+
+ 服务端重绑同时覆盖"坏包"与"毒化存储"两类砖(重绑即远程回滚,§10.7),
|
|
450
|
+
单一机制更简。代价是救援依赖服务端动作(运营重绑/发修复版),无网或无
|
|
451
|
+
人值守时坏包砖不会自愈——接受,写进产品口径。
|
|
452
|
+
- **崩溃处理器与 crash reporter 的关系:链式委托,不是抢钩子。** 保存前任
|
|
453
|
+
handler、救援结束必须调用它——Sentry/Crashlytics/Bugly 全都遵守同一惯
|
|
454
|
+
例,两个安装顺序方向都成立。残余风险:个别 reporter 拿到控制权后不回调
|
|
455
|
+
前任直接终止,救援被截胡——接受,靠遥测观察。
|
|
456
|
+
|
|
457
|
+
### 11.3 handler 机制规格(Android/iOS 一致)
|
|
458
|
+
|
|
459
|
+
- **安装点** = 编排器锚点:Android `getBundleUrl`(schedule 时)、iOS
|
|
460
|
+
`+bundleURL`(scheduleFromColdStart 时),严格早于任何 JS 执行,而 JS 崩
|
|
461
|
+
溃必然晚于 bundle 加载,覆盖天然完整。与编排器同门控:`#if !DEBUG` /
|
|
462
|
+
`BuildConfig.DEBUG`,且落盘 config 存在才安装(灰度开关语义一致)。
|
|
463
|
+
- **触发门控**(handler 内,全部本地判断):进程存活 < 60s,**或**当前有
|
|
464
|
+
在途检查轮(语义是"把没跑完的轮次收尾",覆盖实测里 5~9.4s 之间被打断
|
|
465
|
+
的窗口)。中途随机崩溃(uptime ≥ 60s 且无在途轮)不触发,不拖慢正常崩
|
|
466
|
+
溃 UX。本轮已完成 → 直接放行(无 hold)。
|
|
467
|
+
- **预算按崩溃线程分档**:主线程崩溃 3.5s(Android ANR ~5s / iOS watchdog
|
|
468
|
+
约束),其余线程 10s(RN release 的 JS fatal 重抛在 mqt/exception 线程,
|
|
469
|
+
是常态)。
|
|
470
|
+
- **死锁免疫靠结构,不靠协作**:救援工作跑在**新线程**上,handler 只
|
|
471
|
+
latch/semaphore 带超时等待。即使救援线程死锁(如某线程带着状态锁崩了、
|
|
472
|
+
ObjC uncaught 路径不跑 @finally),handler 超时后照常委托前任 → 进程终
|
|
473
|
+
止,卡死线程陪葬。handler 自身 try-catch-all + once 守卫,第二个线程再
|
|
474
|
+
崩不等待、直接委托(多崩场景宁可少救,绝不挂死)。
|
|
475
|
+
- **救援轮语义 = "确保本进程有一轮跑完"**:在途 → 等其完成(至多预算);
|
|
476
|
+
未开跑 → 立即跑一轮受限轮(HTTP 超时与下载 deadline 全部被剩余预算钳
|
|
477
|
+
制);已跑完 → 返回。与延迟线程的竞态由 roundStarted CAS 收敛为单轮。
|
|
478
|
+
- **救援轮强制激活**:`activate = decision.activate || rescue`。JS 已死,
|
|
479
|
+
没有第二决策者;alert 类策略下不强制激活,修复版就永远躺在磁盘上(§10.7
|
|
480
|
+
同一个洞的 handler 变体)。守卫不变:本机 rolledBack 黑名单仍赢过一切、
|
|
481
|
+
first_time 崩溃保护对激活版本仍生效、服务端 fromHash==toHash 天然防循
|
|
482
|
+
环。hashInfo 追加 `crashRescue: true`(与 forceBootRescue 对称,供 JS
|
|
483
|
+
在该版本活到 markSuccess 时上报遥测)。
|
|
484
|
+
- **覆盖边界(产品口径)**:只逮 exception 形态的崩溃(JS fatal 的原生重
|
|
485
|
+
抛)。native signal(SIGSEGV)、ANR、OOM、watchdog 杀逃逸——与威胁模型
|
|
486
|
+
匹配(热更砖的崩溃源就是 JS bundle),但不得在文案里过度承诺。
|
|
487
|
+
|
|
488
|
+
### 11.4 断点续传(所有 rung 的地基)
|
|
489
|
+
|
|
490
|
+
每次启动的进度必须**单调累积**,handler 窗口被截断才有意义。三端同一规格:
|
|
491
|
+
|
|
492
|
+
- **sidecar**:`<archive>.resume`(JSON:url/etag/lastModified/total),
|
|
493
|
+
下载响应头到达后写入。归档与 sidecar 同生共死:解压成功删归档时一并删;
|
|
494
|
+
patch 阶段失败(downloadPhaseCompleted 后)两者都删——坏归档绝不能变成
|
|
495
|
+
永久毒药;**有归档无 sidecar = 不可信,删掉重来**。
|
|
496
|
+
- **续传请求**:归档存在且 sidecar 匹配 url 时,`Range: bytes=<size>-`,
|
|
497
|
+
有验证器(etag/lastModified)则带 `If-Range`。响应 206 → 校验
|
|
498
|
+
Content-Range 起点后追加;200 → 服务端不支持/文件已变,截断重写并更新
|
|
499
|
+
sidecar;416 → size == sidecar.total 视为下载阶段已完成直接进解压(覆
|
|
500
|
+
盖"下载完、解压前死"的窗口),否则删两者本次内重发一次全量请求。
|
|
501
|
+
- **完整性**:无逐字节 hash 可校(版本 hash 不是归档 hash),依赖三层:
|
|
502
|
+
If-Range 验证器防混拼、最终长度对账、解压/patch 本身的结构校验;任何一
|
|
503
|
+
层失败走"删两者重来",自愈。
|
|
504
|
+
- **进度事件**:received 含续传偏移量,total 取 Content-Range 总长。
|
|
505
|
+
- **round-incomplete 标记**:runOnce 入口落盘 KV 标记、finally 清除。进程
|
|
506
|
+
死于轮中 → 标记残留 → **下次启动编排器跳过 5s 延迟立即(异步)续传**。
|
|
507
|
+
这不是阻塞、没有误判成本,是"上次没救完"最诚实的信号,让被截断的
|
|
508
|
+
handler 窗口在后续启动快速收敛。
|
|
509
|
+
|
|
510
|
+
### 11.5 分端落地
|
|
511
|
+
|
|
512
|
+
- **Android**:`Thread.setDefaultUncaughtExceptionHandler` 链式;uptime 用
|
|
513
|
+
`Process.getStartElapsedRealtime()`;OkHttp/DownloadTask 全程可用(Java
|
|
514
|
+
monitor 随异常解栈释放,无带锁死线程问题,但结构性预算仍兜底)。
|
|
515
|
+
- **iOS**:`NSSetUncaughtExceptionHandler` 链式;RCTFatal 在 release 下
|
|
516
|
+
raise NSException → uncaught → 进入本 handler;NSURLSession/信号量模式
|
|
517
|
+
照旧可用。下载器从 NSURLSessionDownloadTask 改为 data task 流式追加写
|
|
518
|
+
(downloadTask 的临时文件失败即丢,存不下 partial,是续传的硬前提)。
|
|
519
|
+
- **Harmony:本版只做续传 + 零延迟标记,不做 crash hold。** errorManager
|
|
520
|
+
的 error observer 是同步回调,而 ArkTS 网络 IO 只有异步形态——回调里阻
|
|
521
|
+
塞主线程即死锁,救援无法在退出前完成;OHOS 也没有可在崩溃上下文同步驱动
|
|
522
|
+
的 C HTTP 面。备选路线(未验证,留待 spike):`appRecovery` 的 JS_CRASH
|
|
523
|
+
自动重启 × 零延迟续传,靠多次自动重启累积进度。文档如实标注鸿蒙边界。
|
|
524
|
+
|
|
525
|
+
### 11.6 验证
|
|
526
|
+
|
|
527
|
+
三端语法/编译校验照旧(javac / clang -fsyntax-only / DevEco tsc),无 C++
|
|
528
|
+
改动不触发 .so 重编。发版 10.52.0 后按 2026-08-12 同一实验复测:markSuccess
|
|
529
|
+
→ 落盘标记 → 每次启动 ~230ms 即崩,验证 handler 窗口完成 check+download+
|
|
530
|
+
commit、下次启动进修复版,时序记录回 NATIVE_CHECK_FOLLOWUPS.md。
|
|
@@ -363,3 +363,49 @@ strict、77 金标向量 + ASan/UBSan、`.so` 符号与 16KB 对齐、iOS clang
|
|
|
363
363
|
- [ ] 服务端已推送未激活:pushy 主机 `install-npm-release.sh` +
|
|
364
364
|
`service-cli.sh restart all`(验 serverVersion);cresc 跑
|
|
365
365
|
`update-cloudrun.sh`;两个 admin 走各自 CI
|
|
366
|
+
|
|
367
|
+
## 2026-08-12 救砖实测:能救什么、救不了什么(Android 真机 + 生产服务端)
|
|
368
|
+
|
|
369
|
+
两轮实测,同一套 forceBoot 通道,结论截然相反,差别只在崩溃时刻。
|
|
370
|
+
|
|
371
|
+
**能救(已验证)**:markSuccess 之后 10s 才崩的砖。
|
|
372
|
+
时间线:proc_start → 9.4s 原生检测 commit `set for next launch` → 10.0s JS 崩溃 →
|
|
373
|
+
下次启动进入修复版并 markSuccess。救援仅领先崩溃点 567ms。
|
|
374
|
+
|
|
375
|
+
**救不了(已验证)**:markSuccess 后写 storage 标记、下次启动读到标记即崩的砖
|
|
376
|
+
(用户提出的理想场景,崩溃点约 proc_start+230ms)。连续 4 次冷启动,原生检测
|
|
377
|
+
一次都没能走到 commit,设备停留在 "keeps stopping"。
|
|
378
|
+
|
|
379
|
+
**根因(不是延迟参数问题)**:5s 固定延迟只是其一,检查+下载本身实测还需 ~4.4s。
|
|
380
|
+
即使把延迟归零也远超 230ms。更本质的是原生检测线程与 JS 同进程,JS 崩溃时
|
|
381
|
+
进程整体被杀,该轮下载进度全部丢弃,下次启动从零开始——无论重启多少次都无法收敛。
|
|
382
|
+
|
|
383
|
+
**可行方向(未实施,需三端评估)**:
|
|
384
|
+
1. 下载断点续传 + 跨启动累积:每次启动补几百毫秒,多次启动后凑齐并激活(治本,改动最大)
|
|
385
|
+
2. 原生检测移入独立进程:JS 崩溃不再连坐(Android 可行;iOS 无此模型)
|
|
386
|
+
3. 上次启动异常检测 → 本次启动同步阻塞式检查(牺牲启动速度换救援,需谨慎)
|
|
387
|
+
|
|
388
|
+
在此之前,产品口径应明确:forceBoot 救的是"能活过启动阶段、之后才出问题"的砖,
|
|
389
|
+
不覆盖"启动即刻崩"的场景。文档与卖点文案不应过度承诺。
|
|
390
|
+
|
|
391
|
+
## 2026-08-12 缺口闭环(10.52.0):crash-handler hold + 断点续传
|
|
392
|
+
|
|
393
|
+
上面三个方向重新评审后全部改判,最终方案见设计文档 §11:
|
|
394
|
+
|
|
395
|
+
- **主机制 = 崩溃时刻扣留进程**(维护者提出):uncaught handler 里 hold 住
|
|
396
|
+
垂死进程数秒完成检查+下载。崩溃即信号,零误判、零检测态、首崩即救,
|
|
397
|
+
且修复在垂死会话末尾 commit,下次启动零阻塞直接进修复版。方向 2(独立
|
|
398
|
+
进程)与方向 3(启动阻塞)被它整体取代;方向 1(续传)保留为地基。
|
|
399
|
+
- **断点续传三端落地**:sidecar(url/验证器/总长)+ Range/If-Range,
|
|
400
|
+
206/200/416 全处理;归档与 sidecar 同生共死;下载阶段失败保留 partial,
|
|
401
|
+
patch 阶段失败判定归档有毒连 sidecar 一起删。
|
|
402
|
+
- **round-incomplete 标记**:轮次开始落盘、结束清除;残留 = 上个进程死于
|
|
403
|
+
轮中,下次启动跳过 5s 延迟立即续传。
|
|
404
|
+
- **救援轮强制激活** + hashInfo `crashRescue` 标记(与 forceBootRescue 对
|
|
405
|
+
称);本机 rolledBack 黑名单与 first_time 保护语义不变。
|
|
406
|
+
- **边界**:只逮 exception 形态崩溃(native signal/ANR/OOM 逃逸);鸿蒙本
|
|
407
|
+
版无 crash hold(errorManager 同步回调驱动不了 ArkTS 异步 IO),只有续
|
|
408
|
+
传+零延迟。
|
|
409
|
+
|
|
410
|
+
- [ ] 10.52.0 发版后按本节上方同一实验复测 crash-on-launch 砖,时序记录
|
|
411
|
+
回此文件
|
package/README-CN.md
CHANGED
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
- **不阻塞启动**:延迟数秒、跑在后台线程,成果在**下次启动**生效。
|
|
40
40
|
- **是否自动激活取决于你的配置**:`updateStrategy` 为 `silentAndNow` / `silentAndLater` 且未关闭自动检查(`checkStrategy` 不为 `null`)时,原生侧才会把下载好的版本设为下次启动生效;其余情况只下载,激活权仍在 JS。
|
|
41
41
|
- **救砖指令**:控制台可按版本标记「强制启动」,被标记的版本无视上述策略直接在下次启动生效——这是把已被坏版本卡死的设备捞回来的手段。设备本地的崩溃回滚保护仍然优先,已回滚过的版本不会被再装回去。
|
|
42
|
+
- **崩溃时刻救援**(10.52.0 起,Android 与 iOS):应用在启动阶段死于未捕获的 JS 错误时,SDK 会短暂扣住垂死的进程(数秒、有硬上限),把检查与下载做完——即使版本每次启动零点几秒就崩,也能被换掉。该窗口内下载到的修复版一律设为下次启动生效(JS 已经没有机会做决策了)。崩溃上报不受影响:SDK 以链式方式保存并在结束后调用原有的崩溃处理器。不覆盖:原生(非 JS)崩溃、ANR、OOM 击杀。
|
|
43
|
+
- **断点续传**(10.52.0 起):更新下载在进程死亡后保留进度、下次以 HTTP Range 续传,反复的短命启动也能单调累积进度;紧随中断轮次的下一次启动会跳过延迟立即续传。鸿蒙暂无崩溃时刻扣留,但续传与立即重试同样生效。
|
|
42
44
|
- **可以关闭**:`disableNativeCheck: true`。关闭后每次冷启动少一次后台请求,代价是**放弃上述自愈能力**——被坏热更卡死的设备将无法自动恢复。仅在这次请求本身构成问题时(流量/耗电预算、隐私清单申报、需用户同意后才可联网)才建议关闭。
|
|
43
45
|
|
|
44
46
|
### Diff 算法对比
|
package/README.md
CHANGED
|
@@ -37,6 +37,8 @@ What to know:
|
|
|
37
37
|
- **It never blocks startup**: it is delayed by a few seconds, runs off the main thread, and its result takes effect on the *next* launch.
|
|
38
38
|
- **Whether it activates depends on your configuration**: only with `updateStrategy` set to `silentAndNow` / `silentAndLater` *and* automatic checks left on (`checkStrategy` not `null`) will the native side mark a downloaded version for the next launch. Otherwise it downloads and leaves activation to JS.
|
|
39
39
|
- **Rescue directive**: the dashboard can mark a version "force boot", which activates on the next launch regardless of the strategies above — this is how a fleet stuck on a broken version is recovered. The device-local crash-rollback guard still wins: a version this device already rolled back from is never reinstalled.
|
|
40
|
+
- **Crash-moment rescue** (since 10.52.0, Android & iOS): when the app dies of an uncaught JS error during startup, the SDK briefly holds the dying process (a few seconds, bounded) to finish the check and download — so even a version that crashes a fraction of a second into every launch gets replaced. In that window the downloaded fix is always activated for the next launch, since JS is no longer around to decide. Crash reporters keep working: the SDK chains the previous crash handler and always hands the crash over afterwards. Not covered: native (non-JS) crashes, ANRs and OOM kills.
|
|
41
|
+
- **Resumable downloads** (since 10.52.0): update downloads survive process death and resume from where they stopped (HTTP Range), so repeated short-lived launches still make monotonic progress; a launch that follows an interrupted round skips the startup delay and resumes immediately. On HarmonyOS the crash-moment hold is not available yet, but resume + immediate retry apply.
|
|
40
42
|
- **It can be turned off**: `disableNativeCheck: true` removes one background request per cold start, at the cost of **giving up the recovery above** — a device bricked by a bad update can no longer heal itself. Choose it only when that request is itself the problem (traffic/battery budgets, privacy manifests, consent-gated networking).
|
|
41
43
|
|
|
42
44
|
## Diff Algorithm Comparison
|
package/android/build.gradle
CHANGED
|
@@ -154,6 +154,16 @@ android {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// AGP 9 disables the resValues feature for libraries by default; the
|
|
158
|
+
// pushy_build_time resource below needs it back on. The block exists
|
|
159
|
+
// since AGP 4, so guard like supportsNamespace() does for old projects.
|
|
160
|
+
def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0].toInteger()
|
|
161
|
+
if (agpMajor >= 4) {
|
|
162
|
+
buildFeatures {
|
|
163
|
+
resValues true
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
157
167
|
buildTypes {
|
|
158
168
|
release {
|
|
159
169
|
resValue("string", "pushy_build_time", "${minutesSinceEpoch}")
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
package cn.reactnative.modules.update;
|
|
2
|
+
|
|
3
|
+
import android.os.Looper;
|
|
4
|
+
import android.os.Process;
|
|
5
|
+
import android.os.SystemClock;
|
|
6
|
+
import android.util.Log;
|
|
7
|
+
import java.util.concurrent.CountDownLatch;
|
|
8
|
+
import java.util.concurrent.TimeUnit;
|
|
9
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Crash-moment brick rescue (NATIVE_CHECKUPDATE_DESIGN §11): when the app is
|
|
13
|
+
* dying of an uncaught exception during startup, the process is still alive
|
|
14
|
+
* and JS will never run again — a natural, false-positive-free window to
|
|
15
|
+
* finish the cold-start update check before the crash dialog appears.
|
|
16
|
+
*
|
|
17
|
+
* Etiquette: the previous default handler (crash reporters chain the same
|
|
18
|
+
* way) is always invoked afterwards, whether the rescue ran, timed out or
|
|
19
|
+
* threw. The rescue itself runs on a worker thread and the handler only
|
|
20
|
+
* waits with a hard timeout, so even a deadlocked rescue (a thread that died
|
|
21
|
+
* holding a lock) can only delay the process death, never prevent it.
|
|
22
|
+
*/
|
|
23
|
+
final class CrashRescue {
|
|
24
|
+
private static final AtomicBoolean installed = new AtomicBoolean(false);
|
|
25
|
+
private static final AtomicBoolean rescueAttempted = new AtomicBoolean(false);
|
|
26
|
+
// ≈ process start: install() runs during the first bundle resolution.
|
|
27
|
+
// Deliberately not Process.getStartElapsedRealtime(), which is API 24+
|
|
28
|
+
// while the module's minSdk floor is lower — a NoSuchMethodError here
|
|
29
|
+
// would silently disable the whole rescue on those devices.
|
|
30
|
+
private static volatile long installedAtElapsedRealtime;
|
|
31
|
+
private static final long TRIGGER_UPTIME_MILLIS = 60000;
|
|
32
|
+
private static final long BUDGET_BACKGROUND_THREAD_MILLIS = 10000;
|
|
33
|
+
// A held main thread stops input dispatch; stay under the ~5s ANR window.
|
|
34
|
+
private static final long BUDGET_MAIN_THREAD_MILLIS = 3500;
|
|
35
|
+
|
|
36
|
+
private CrashRescue() {
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static void install() {
|
|
40
|
+
if (!installed.compareAndSet(false, true)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
installedAtElapsedRealtime = SystemClock.elapsedRealtime();
|
|
44
|
+
final Thread.UncaughtExceptionHandler previous =
|
|
45
|
+
Thread.getDefaultUncaughtExceptionHandler();
|
|
46
|
+
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
|
|
47
|
+
@Override
|
|
48
|
+
public void uncaughtException(Thread thread, Throwable error) {
|
|
49
|
+
try {
|
|
50
|
+
maybeHoldForRescue(thread);
|
|
51
|
+
} catch (Throwable ignored) {
|
|
52
|
+
// The dying process owes the previous handler its turn no
|
|
53
|
+
// matter what the rescue did.
|
|
54
|
+
}
|
|
55
|
+
if (previous != null) {
|
|
56
|
+
previous.uncaughtException(thread, error);
|
|
57
|
+
} else {
|
|
58
|
+
// Mirror RuntimeInit's default behaviour so the process
|
|
59
|
+
// still dies when no other handler was registered.
|
|
60
|
+
Process.killProcess(Process.myPid());
|
|
61
|
+
System.exit(10);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private static void maybeHoldForRescue(Thread crashedThread) {
|
|
68
|
+
// Once per process; a second crashing thread passes straight through
|
|
69
|
+
// instead of waiting behind the first (§11.3: prefer under-rescuing
|
|
70
|
+
// over wedging the teardown).
|
|
71
|
+
if (!rescueAttempted.compareAndSet(false, true)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
long uptimeMillis =
|
|
75
|
+
SystemClock.elapsedRealtime() - installedAtElapsedRealtime;
|
|
76
|
+
boolean roundInFlight = NativeCheckOrchestrator.isRoundInFlight();
|
|
77
|
+
// Early crashes are the brick signature; a crash with an in-flight
|
|
78
|
+
// round is worth finishing regardless of uptime. Everything else is
|
|
79
|
+
// an ordinary crash whose UX must not be delayed.
|
|
80
|
+
if (uptimeMillis >= TRIGGER_UPTIME_MILLIS && !roundInFlight) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
boolean onMainThread = crashedThread == Looper.getMainLooper().getThread();
|
|
84
|
+
long budgetMillis = onMainThread
|
|
85
|
+
? BUDGET_MAIN_THREAD_MILLIS
|
|
86
|
+
: BUDGET_BACKGROUND_THREAD_MILLIS;
|
|
87
|
+
final long deadlineNanos =
|
|
88
|
+
System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(budgetMillis);
|
|
89
|
+
Log.i(UpdateContext.TAG, "crash rescue: holding process for up to "
|
|
90
|
+
+ budgetMillis + "ms (uptime " + uptimeMillis + "ms)");
|
|
91
|
+
|
|
92
|
+
final CountDownLatch done = new CountDownLatch(1);
|
|
93
|
+
Thread worker = new Thread(new Runnable() {
|
|
94
|
+
@Override
|
|
95
|
+
public void run() {
|
|
96
|
+
try {
|
|
97
|
+
NativeCheckOrchestrator.runRescue(deadlineNanos);
|
|
98
|
+
} catch (Throwable e) {
|
|
99
|
+
Log.w(UpdateContext.TAG, "crash rescue failed: " + e);
|
|
100
|
+
} finally {
|
|
101
|
+
done.countDown();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}, "pushy-crash-rescue");
|
|
105
|
+
worker.setDaemon(true);
|
|
106
|
+
worker.start();
|
|
107
|
+
try {
|
|
108
|
+
if (!done.await(budgetMillis, TimeUnit.MILLISECONDS)) {
|
|
109
|
+
Log.w(UpdateContext.TAG, "crash rescue: budget exhausted, letting go");
|
|
110
|
+
}
|
|
111
|
+
} catch (InterruptedException e) {
|
|
112
|
+
Thread.currentThread().interrupt();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|