Skip to content

Commit bb7495f

Browse files
sunnylqmclaude
andcommitted
fix(harmony): register 10.50+ methods in the RNOH TurboModule glue
PushyTurboModule.cpp's methodMap is the only bridge RNOH gives JS into the ArkTS module, and it was never taught the four methods added since 10.50: syncNativeConfig, getNativeCheckCache, getBundleHash, resetToPackagedBundle. Missing registration means `undefined` on the JS side, and the JS feature-detects then treat the capability as "old native" and silently skip it — so the native cold-start check never armed on Harmony in 10.51.0-10.52.0 (nativeConfig was never persisted; the ets orchestrator, resume and zero-delay marker were dead code), and resetToPackagedBundle was unusable from JS. Why CI missed it: the harmony e2e suite only runs e2e/harmony/** (smoke + local-update); native-check.test.ts never executed there, and the graceful old-native fallback hid the breakage for three releases. Verified on the DevEco api20 simulator end-to-end, which also settled the open Harmony question: an uncaught JS error does NOT kill the process on RNOH (it only logs), so the orchestrator completes its round after JS dies — a brick that crashes 1s into every launch was replaced and booted the fix on the next launch with no crash-hold needed. Design doc §11.5 rewritten accordingly; probe scaffolding checked in (entry.crashprobe.ts, entry.diag.ts). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 70e0f22 commit bb7495f

7 files changed

Lines changed: 101 additions & 7 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// §11 Harmony crash-semantics probe (not part of the regular e2e suites):
2+
// does an uncaught JS error in release kill the whole process (the
3+
// Android/iOS behaviour the crash rescue exists for), or does RNOH survive
4+
// it? Thrown at +1s — before the native check's 5s delay — so a surviving
5+
// process also proves the orchestrator round outlives JS death.
6+
import { AppRegistry } from 'react-native';
7+
import { name as appName } from '../app.json';
8+
import App from './app';
9+
10+
setTimeout(() => {
11+
throw new Error('HM_CRASH_PROBE: uncaught JS error at +1s');
12+
}, 1000);
13+
14+
AppRegistry.registerComponent(appName, () => App);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Diagnostic entry: why does syncNativeConfig never run on RNOH?
2+
import { AppRegistry } from 'react-native';
3+
import { PushyModule } from 'react-native-update';
4+
import { name as appName } from '../app.json';
5+
import App from './app';
6+
7+
const mod = PushyModule as unknown as Record<string, unknown>;
8+
console.error(
9+
`RNU_DIAG typeof syncNativeConfig=${typeof mod.syncNativeConfig} ` +
10+
`typeof getConstants=${typeof mod.getConstants} ` +
11+
`keys=${JSON.stringify(Object.keys(mod)).slice(0, 300)}`
12+
);
13+
Promise.resolve()
14+
.then(() => (mod.syncNativeConfig as (c: string) => Promise<void>)('{"probe":true}'))
15+
.then(() => console.error('RNU_DIAG direct syncNativeConfig call OK'))
16+
.catch((e: Error) => console.error(`RNU_DIAG direct call failed: ${e.message}`));
17+
18+
AppRegistry.registerComponent(appName, () => App);

NATIVE_CHECKUPDATE_DESIGN.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -516,11 +516,20 @@ afterDownload)`:本地 silent 策略 或 响应标记 forceBoot 即激活。
516516
raise NSException → uncaught → 进入本 handler;NSURLSession/信号量模式
517517
照旧可用。下载器从 NSURLSessionDownloadTask 改为 data task 流式追加写
518518
(downloadTask 的临时文件失败即丢,存不下 partial,是续传的硬前提)。
519-
- **Harmony:本版只做续传 + 零延迟标记,不做 crash hold。** errorManager
520-
的 error observer 是同步回调,而 ArkTS 网络 IO 只有异步形态——回调里阻
521-
塞主线程即死锁,救援无法在退出前完成;OHOS 也没有可在崩溃上下文同步驱动
522-
的 C HTTP 面。备选路线(未验证,留待 spike):`appRecovery` 的 JS_CRASH
523-
自动重启 × 零延迟续传,靠多次自动重启累积进度。文档如实标注鸿蒙边界。
519+
- **Harmony:不需要 crash hold——RNOH 的进程模型天然免疫(2026-08-13 模
520+
拟器实证)。** 未捕获的 JS 错误在 RNOH release 下只记 #RNOH_JS 错误日志,
521+
**不杀进程**:Hermes 跑在 RNOH 自管线程,异常不会重抛到 ArkTS 主线程触发
522+
errorManager。ets 编排器跑在 ArkTS 主线程事件循环上,JS 死后照常完成整
523+
轮(实测:+1s JS 崩溃,+5s 轮次检查→下载→forceBoot 激活,重启进修复
524+
版)。因此 Android/iOS 的"救援窗口排在崩溃点之后"问题在鸿蒙不存在,
525+
§10 原机制即完整覆盖 crash-on-launch 砖,且没有预算压力(600s 下载窗口
526+
完整可用)。续传 + 零延迟标记仍有价值(设备重启/系统杀进程等场景)。
527+
原 errorManager/appRecovery 的 spike 论述作废留档:当时的前提("JS 崩溃
528+
杀进程")被实证推翻。
529+
注意两个鸿蒙特有的工程事实:PushyTurboModule.cpp 的 C++ 方法表是 RNOH
530+
给 JS 的唯一桥,**新增 spec 方法必须同步注册**(10.50~10.52.0 漏注册了
531+
syncNativeConfig 等四个方法,原生检测因此从未生效,10.52.1 修复);鸿蒙
532+
模拟器是 QEMU 虚机,app 内 127.0.0.1 不通宿主,人工验证需 `hdc rport`
524533

525534
### 11.6 验证
526535

NATIVE_CHECK_FOLLOWUPS.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,3 +473,42 @@ bundle 加载失败一族(RCTInstance 先 RCTExecuteOnMainQueue 再 RCTFatal),
473473
**iOS 覆盖边界补记**:若宿主 app 调用了 `RCTSetFatalHandler`,RCTFatal 走
474474
handler 分支不再抛异常,crash-rescue 不会触发(Android 无对应抢占点)。
475475
文档口径需含此项。
476+
477+
## 2026-08-13 鸿蒙模拟器实测:两个实证 + 一个 10.51 以来的功能性断裂
478+
479+
DevEco api20 模拟器(HarmonyOS 6.0 / arm64)+ harmony_use_pushy(RN 0.72 +
480+
RNOH 0.72.96)+ 本地 e2e 服务端。探针脚手架入库:
481+
`harmony_use_pushy/e2e/entry.crashprobe.ts`(+1s 未捕获 JS 错误)、
482+
`entry.diag.ts`(TM 方法表诊断)。
483+
484+
**实证一(缺口 0 的分叉答案):RNOH 未捕获 JS 错误不杀进程。** +1s 抛错后
485+
进程存活 40s+ 无恙,错误只落 #RNOH_JS/#RNOH_ARK 日志。Hermes 在 RNOH 自管
486+
线程上,异常不重抛 ArkTS 主线程,errorManager 根本不参与。因此**鸿蒙不需要
487+
crash hold**:ets 编排器在 JS 死后照常跑完整轮。此前 §11.5 的
488+
errorManager/appRecovery 论述前提不成立,已改写。
489+
490+
**实证二(端到端救砖):** crashprobe 作基座(每次启动 JS +1s 崩),服务端
491+
forceBoot 下发 v1:+5s 轮次照常检查→下载 v1.ppk→激活(currentVersion 写
492+
入),重启直接运行 e2e-full-v1,砖消失。JS 崩溃对轮次零影响,无预算压力。
493+
494+
**功能性断裂(已修,severity 高):`PushyTurboModule.cpp` 的 C++ 方法表漏
495+
注册 10.50+ 的全部四个新方法**(syncNativeConfig / getNativeCheckCache /
496+
getBundleHash / resetToPackagedBundle)。RNOH 的 ArkTS TM 只有经 C++
497+
methodMap 注册的方法才对 JS 可见,缺失 = JS 侧 undefined → feature-detect
498+
判"老原生"静默跳过 → **原生冷启动检测在鸿蒙自 10.51.0 从未生效**
499+
(nativeConfig 从未落盘,ets 编排器+本次的续传/零延迟全是死代码);
500+
resetToPackagedBundle(10.50 Phase 1)在鸿蒙 JS 侧也一直不可用。诊断链:
501+
健康启动 preferences 无 nativeConfig → entry.diag 打 typeof=undefined +
502+
keys 只有 14 个老方法 → cpp 方法表对不上 ets 类。
503+
504+
**为什么 CI 没逮住**:harmony e2e 套件 testMatch 只含 `e2e/harmony/**`
505+
(smoke + local-update),native-check.test.ts 从未在鸿蒙执行;而 JS 的
506+
feature-detect 把缺失当"老原生"优雅跳过——这是[[no-graceful-degradation]]
507+
教训的又一实例:兼容旧原生的降级路径把新原生的接线断裂藏了三个版本。
508+
509+
- [x] cpp 方法表补注册(含"新增 spec 方法必须同步注册"的醒目注释)
510+
- [ ] harmony e2e 补 native-check 场景(driver 是 hdc+uitest 非 detox,
511+
需移植;顺带把 `hdc rport` 写进用例基建——模拟器 127.0.0.1 不通宿主)
512+
- [ ] 发版 10.52.1(鸿蒙 bridging 修复;Android/iOS 无变化)
513+
- [ ] Logger.ts isDebug 硬编码 false,轮次全程无日志可观测,排障全靠副作用
514+
(preferences/服务端流量);考虑 error 级关键节点或可配置开关

README-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
- **是否自动激活取决于你的配置**`updateStrategy``silentAndNow` / `silentAndLater` 且未关闭自动检查(`checkStrategy` 不为 `null`)时,原生侧才会把下载好的版本设为下次启动生效;其余情况只下载,激活权仍在 JS。
4141
- **救砖指令**:控制台可按版本标记「强制启动」,被标记的版本无视上述策略直接在下次启动生效——这是把已被坏版本卡死的设备捞回来的手段。设备本地的崩溃回滚保护仍然优先,已回滚过的版本不会被再装回去。
4242
- **崩溃时刻救援**(10.52.0 起,Android 与 iOS):应用在启动阶段死于未捕获的 JS 错误时,SDK 会短暂扣住垂死的进程(数秒、有硬上限),把检查与下载做完——即使版本每次启动零点几秒就崩,也能被换掉。该窗口内下载到的修复版一律设为下次启动生效(JS 已经没有机会做决策了)。崩溃上报不受影响:SDK 以链式方式保存并在结束后调用原有的崩溃处理器。不覆盖:原生(非 JS)崩溃、ANR、OOM 击杀,以及 iOS 上安装了自定义 `RCTSetFatalHandler` 的应用(那时 React Native 不再抛出本机制拦截的异常)。
43-
- **断点续传**(10.52.0 起):更新下载在进程死亡后保留进度、下次以 HTTP Range 续传,反复的短命启动也能单调累积进度;紧随中断轮次的下一次启动会跳过延迟立即续传。鸿蒙暂无崩溃时刻扣留,但续传与立即重试同样生效
43+
- **断点续传**(10.52.0 起):更新下载在进程死亡后保留进度、下次以 HTTP Range 续传,反复的短命启动也能单调累积进度;紧随中断轮次的下一次启动会跳过延迟立即续传。鸿蒙不需要崩溃时刻扣留:RNOH 下未捕获的 JS 错误不会杀死进程,JS 启动即崩时原生检测照常完成(需 10.52.1——此前版本因桥接缺陷,原生检测在鸿蒙上从未生效)
4444
- **可以关闭**`disableNativeCheck: true`。关闭后每次冷启动少一次后台请求,代价是**放弃上述自愈能力**——被坏热更卡死的设备将无法自动恢复。仅在这次请求本身构成问题时(流量/耗电预算、隐私清单申报、需用户同意后才可联网)才建议关闭。
4545

4646
### Diff 算法对比

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ What to know:
3838
- **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.
3939
- **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.
4040
- **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, OOM kills, and — on iOS — apps that install a custom `RCTSetFatalHandler` (React Native then no longer raises the exception this rescue intercepts).
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.
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. HarmonyOS does not need the crash-moment hold: an uncaught JS error does not kill the process there, so the native check completes normally even when JS dies on launch (requires 10.52.1 — earlier versions never armed the native check on HarmonyOS due to a bridging bug).
4242
- **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).
4343

4444
## Diff Algorithm Comparison

harmony/pushy/src/main/cpp/PushyTurboModule.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ PUSHY_ASYNC_METHOD(downloadPatchFromPpk)
6060
PUSHY_ASYNC_METHOD(downloadPatchFromPackage)
6161
PUSHY_ASYNC_METHOD(downloadFullUpdate)
6262
PUSHY_ASYNC_METHOD(downloadAndInstallApk)
63+
// 10.50+ additions. This table is the only bridge RNOH gives JS into the
64+
// ArkTS module: a method missing here is `undefined` on the JS side, and the
65+
// JS feature-detects then treat the whole capability as "old native" and
66+
// silently skip it — which is how the native cold-start check shipped dark
67+
// on Harmony. Every new spec method MUST be registered here.
68+
PUSHY_ASYNC_METHOD(syncNativeConfig)
69+
PUSHY_ASYNC_METHOD(getNativeCheckCache)
70+
PUSHY_ASYNC_METHOD(getBundleHash)
71+
PUSHY_ASYNC_METHOD(resetToPackagedBundle)
6372

6473
#undef PUSHY_SYNC_METHOD
6574
#undef PUSHY_ASYNC_METHOD
@@ -93,4 +102,9 @@ PushyTurboModule::PushyTurboModule(
93102
"downloadAndInstallApk", 1, HostFunction_downloadAndInstallApk);
94103
registerMethod("addListener", 1, HostFunction_addListener);
95104
registerMethod("removeListeners", 1, HostFunction_removeListeners);
105+
registerMethod("syncNativeConfig", 1, HostFunction_syncNativeConfig);
106+
registerMethod("getNativeCheckCache", 0, HostFunction_getNativeCheckCache);
107+
registerMethod("getBundleHash", 0, HostFunction_getBundleHash);
108+
registerMethod(
109+
"resetToPackagedBundle", 0, HostFunction_resetToPackagedBundle);
96110
}

0 commit comments

Comments
 (0)