From 2648f6c29554ad63b043f1f2f0cae1d7825d4fdd Mon Sep 17 00:00:00 2001 From: liuqiang Date: Tue, 30 Jun 2026 20:05:48 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(harmony):=20=E4=BF=AE=E5=A4=8D=20Update?= =?UTF-8?q?Context=20=E5=A4=9A=E5=AE=9E=E4=BE=8B=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=88=86=E8=A3=82=E4=B8=8E=E7=89=88=E6=9C=AC=E9=87=8D=E7=BD=AE?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UpdateContext 改为单例模式,避免 bundle provider 与 TurboModule 各自持有 preferences 缓存导致读写分裂 - binary 版本变更时不再 clearExisting,避免清除 uuid/firstLoadMarked/hash_* 等无关 KV,修复 isFirstTime 为 false 导致 markSuccess 永不执行 - 增加 trace/logStateSnapshot 诊断日志,用于定位多实例与状态分裂问题 - PushyFileJSBundleProvider 与 PushyTurboModule 统一改用 getInstance 单例 Committed at: 2026-06-30 20:05 Committed by: liuqiang Co-Authored-By: Claude --- .../main/ets/PushyFileJSBundleProvider.ets | 2 +- .../pushy/src/main/ets/PushyTurboModule.ts | 3 +- harmony/pushy/src/main/ets/UpdateContext.ts | 61 ++++++++++++++++++- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets b/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets index d15f2407..8392235d 100644 --- a/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets +++ b/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets @@ -12,7 +12,7 @@ export class PushyFileJSBundleProvider extends JSBundleProvider { constructor(context: common.UIAbilityContext) { super(); - this.updateContext = new UpdateContext(context); + this.updateContext = UpdateContext.getInstance(context); } getURL(): string { diff --git a/harmony/pushy/src/main/ets/PushyTurboModule.ts b/harmony/pushy/src/main/ets/PushyTurboModule.ts index d4a6049f..cef3f7fb 100644 --- a/harmony/pushy/src/main/ets/PushyTurboModule.ts +++ b/harmony/pushy/src/main/ets/PushyTurboModule.ts @@ -38,7 +38,7 @@ export class PushyTurboModule extends UITurboModule { super(ctx); logger.debug(TAG, ',PushyTurboModule constructor'); this.mUiCtx = ctx.uiAbilityContext; - this.context = new UpdateContext(this.mUiCtx); + this.context = UpdateContext.getInstance(this.mUiCtx); EventHub.getInstance().setRNInstance(ctx.rnInstance); } @@ -78,6 +78,7 @@ export class PushyTurboModule extends UITurboModule { getConstants(): Object { logger.debug(TAG, ',call getConstants'); + this.context.logStateSnapshot('getConstants:enter'); const packageVersion = this.context.getPackageVersion(); const buildTime = this.context.getBuildTime(); this.context.syncStateWithBinaryVersion(packageVersion, buildTime); diff --git a/harmony/pushy/src/main/ets/UpdateContext.ts b/harmony/pushy/src/main/ets/UpdateContext.ts index d0b969cf..2bd4e112 100644 --- a/harmony/pushy/src/main/ets/UpdateContext.ts +++ b/harmony/pushy/src/main/ets/UpdateContext.ts @@ -5,6 +5,7 @@ import common from '@ohos.app.ability.common'; import { DownloadTaskParams } from './DownloadTaskParams'; import { bundleManager } from '@kit.AbilityKit'; import { util } from '@kit.ArkTS'; +import logger from './Logger'; import NativePatchCore, { STATE_OP_CLEAR_FIRST_TIME, STATE_OP_CLEAR_ROLLBACK_MARK, @@ -28,10 +29,23 @@ export class UpdateContext { private static ignoreRollback: boolean = false; private static cachedPackageVersion: string = ''; private static cachedBuildTime: string = ''; + // 单例:确保 bundle provider 与 TurboModule 共用同一份 preferences 内存状态, + // 避免 RNOH RN 实例重建后两处 UpdateContext 各自持有 preferences 缓存导致读写分裂。 + private static instance: UpdateContext | null = null; + private static instanceCounter: number = 0; + private readonly instanceId: string; + + public static getInstance(context: common.UIAbilityContext): UpdateContext { + if (!UpdateContext.instance) { + UpdateContext.instance = new UpdateContext(context); + } + return UpdateContext.instance; + } constructor(context: common.UIAbilityContext) { this.context = context; this.rootDir = context.filesDir + '/_update'; + this.instanceId = `uc#${++UpdateContext.instanceCounter}`; try { if (!fileIo.accessSync(this.rootDir)) { @@ -41,12 +55,36 @@ export class UpdateContext { console.error('Failed to create root directory:', e); } this.initPreferences(); + this.trace('ctor'); this.syncStateWithBinaryVersion( this.getPackageVersion(), this.getBuildTime(), ); } + /** + * 诊断日志:打印本实例 id 与关键状态,用于定位 preferences 多实例 / 状态分裂问题。 + * 通过 hilog 输出,prefix=pushy,可在 hilog 中按 "UpdateContext" 过滤。 + */ + private trace(point: string): void { + const snap = this.getStateSnapshot(); + logger.info( + 'UpdateContext', + `trace id=${this.instanceId} ${point}` + + ` pkg=${snap.packageVersion} bt=${snap.buildTime}` + + ` cv=${snap.currentVersion} lv=${snap.lastVersion}` + + ` ft=${snap.firstTime} fto=${snap.firstTimeOk}` + + ` rb=${snap.rolledBackVersion}` + + ` flm=${this.readString('firstLoadMarked')}` + + ` uuid=${this.readString('uuid')}`, + ); + } + + /** 对外诊断入口,供 TurboModule 在 getConstants 等关键节点打印状态。 */ + public logStateSnapshot(point: string): void { + this.trace(point); + } + private initPreferences() { try { this.preferences = preferences.getPreferencesSync(this.context, { @@ -269,9 +307,17 @@ export class UpdateContext { return; } + logger.info( + 'UpdateContext', + `binary version changed, resetting update state id=${this.instanceId}`, + ); UpdateContext.ignoreRollback = false; this.cleanUp(); - this.persistState(nextState, { clearExisting: true }); + // 仅重置状态机字段(currentVersion / lastVersion / firstTime / firstTimeOk / + // rolledBackVersion)。不再 clearExisting,避免连带清除 uuid / firstLoadMarked / + // hash_* 等与 binary 版本无关的 KV —— 它们在多实例场景下本就脆弱,连带清除会 + // 让 getConstants() 永远读到空,从而 isFirstTime=false、markSuccess 永不执行。 + this.persistState(nextState); } public setKv(key: string, value: string): void { @@ -388,8 +434,10 @@ export class UpdateContext { throw Error(`Bundle version ${hash} not found.`); } + this.trace(`switchVersion:before ${hash}`); this.runStateOperation(STATE_OP_SWITCH_VERSION, hash); UpdateContext.ignoreRollback = false; + this.trace(`switchVersion:after ${hash}`); } catch (e) { console.error('Failed to switch version:', e); throw e; @@ -398,6 +446,7 @@ export class UpdateContext { public consumeFirstLoadMarker(): boolean { const marked = this.readString('firstLoadMarked') === 'true'; + this.trace(`consumeFirstLoadMarker:marked=${marked}`); if (marked) { this.preferences.deleteSync('firstLoadMarked'); this.flushPreferences('clear first load marker'); @@ -407,6 +456,7 @@ export class UpdateContext { public getBundleUrl() { UpdateContext.isUsingBundleUrl = true; + this.trace('getBundleUrl:enter'); const launchState = NativePatchCore.runStateCore( STATE_OP_RESOLVE_LAUNCH, this.getStateSnapshot(), @@ -422,6 +472,11 @@ export class UpdateContext { if (launchState.consumedFirstTime) { UpdateContext.ignoreRollback = true; } + this.trace( + `getBundleUrl:load=${launchState.loadVersion}` + + ` consumed=${launchState.consumedFirstTime}` + + ` rollback=${launchState.didRollback}`, + ); let version = launchState.loadVersion || ''; while (version) { @@ -442,7 +497,9 @@ export class UpdateContext { } public getCurrentVersion(): string { - return this.getStateSnapshot().currentVersion || ''; + const cv = this.getStateSnapshot().currentVersion || ''; + this.trace(`getCurrentVersion:${cv}`); + return cv; } private rollBack(): string { From c9974a71a213df026a01a4cc551f9ef14dd2ec69 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Wed, 1 Jul 2026 10:29:17 +0800 Subject: [PATCH 2/2] fix(harmony): tighten update context singleton logging --- harmony/pushy/src/main/ets/PushyTurboModule.ts | 15 ++++++++++++++- harmony/pushy/src/main/ets/UpdateContext.ts | 4 ++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/harmony/pushy/src/main/ets/PushyTurboModule.ts b/harmony/pushy/src/main/ets/PushyTurboModule.ts index cef3f7fb..aa2fcfb3 100644 --- a/harmony/pushy/src/main/ets/PushyTurboModule.ts +++ b/harmony/pushy/src/main/ets/PushyTurboModule.ts @@ -96,7 +96,7 @@ export class PushyTurboModule extends UITurboModule { this.context.clearRollbackMark(); } - return { + const result = { downloadRootDir: `${this.mUiCtx.filesDir}/_update`, currentVersionInfo, packageVersion, @@ -107,6 +107,19 @@ export class PushyTurboModule extends UITurboModule { rolledBackVersion, uuid, }; + const logResult = { + downloadRootDir: result.downloadRootDir, + currentVersionInfo: result.currentVersionInfo, + packageVersion: result.packageVersion, + currentVersion: result.currentVersion, + buildTime: result.buildTime, + isUsingBundleUrl: result.isUsingBundleUrl, + isFirstTime: result.isFirstTime, + rolledBackVersion: result.rolledBackVersion, + uuidSet: !!result.uuid, + }; + logger.info(TAG, `,getConstants result: ${JSON.stringify(logResult)}`); + return result; } setLocalHashInfo(hash: string, info: string): boolean { diff --git a/harmony/pushy/src/main/ets/UpdateContext.ts b/harmony/pushy/src/main/ets/UpdateContext.ts index 2bd4e112..daa37418 100644 --- a/harmony/pushy/src/main/ets/UpdateContext.ts +++ b/harmony/pushy/src/main/ets/UpdateContext.ts @@ -42,7 +42,7 @@ export class UpdateContext { return UpdateContext.instance; } - constructor(context: common.UIAbilityContext) { + private constructor(context: common.UIAbilityContext) { this.context = context; this.rootDir = context.filesDir + '/_update'; this.instanceId = `uc#${++UpdateContext.instanceCounter}`; @@ -76,7 +76,7 @@ export class UpdateContext { ` ft=${snap.firstTime} fto=${snap.firstTimeOk}` + ` rb=${snap.rolledBackVersion}` + ` flm=${this.readString('firstLoadMarked')}` + - ` uuid=${this.readString('uuid')}`, + ` uuidSet=${!!this.readString('uuid')}`, ); }