Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
check_changes:
runs-on: ubuntu-latest # npm --provenance requires a GitHub-hosted runner
# Room for the native-required wait below.
timeout-minutes: 60
timeout-minutes: 130
outputs:
harmony_changed: ${{ steps.check.outputs.changed }}
prev_tag: ${{ steps.check.outputs.prev_tag }}
Expand All @@ -31,17 +31,24 @@ jobs:
GH_TOKEN: ${{ github.token }}
SHA: ${{ github.sha }}
run: |
for attempt in $(seq 1 45); do
# The gate turns green only after the slowest native E2E: e2e-ios
# is a build job (timeout 45 min) followed by its test legs
# (timeout 60 min), plus macOS queueing. A green release commit
# took 55 min (10.60.0), so 45 failed releases cut right after the
# push; wait out both budgets instead.
# Checks at minute 0 through 120: the last poll lands on the
# boundary itself, with no trailing sleep before giving up.
for attempt in $(seq 0 120); do
state="$(gh api "repos/$GITHUB_REPOSITORY/commits/$SHA/status" \
--jq '[.statuses[] | select(.context == "native-required")][0].state // "missing"')"
echo "native-required for $SHA: $state"
case "$state" in
success) exit 0 ;;
pending|missing) sleep 60 ;;
pending|missing) if [ "$attempt" -lt 120 ]; then sleep 60; fi ;;
*) echo "::error::native-required is $state for $SHA; fix CI before publishing"; exit 1 ;;
esac
done
echo "::error::native-required for $SHA did not turn green within 45 minutes"
echo "::error::native-required for $SHA did not turn green within 120 minutes"
exit 1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public class UpdateContext {
// 4 (clear first time) exists in the state core but has no Android caller.
private static final int STATE_OP_CLEAR_ROLLBACK_MARK = 5;
private static final int STATE_OP_RESOLVE_LAUNCH = 6;
private static final String KEY_FIRST_LOAD_MARKED = "firstLoadMarked";
static final String KEY_FIRST_LOAD_MARKED = "firstLoadMarked";
// Bumped by resetToPackagedBundle. The cold-start check runs for minutes
// and may already hold a decision when the app resets to the packaged
// bundle; the orchestrator samples this counter and abandons activation
Expand Down Expand Up @@ -134,7 +134,7 @@ private UpdateContext(Context context) {
// Execute cleanUp before clearing SharedPreferences to avoid race condition
cleanUp(DownloadTaskParams.DEFAULT_MAX_AGE_DAYS);
SharedPreferences.Editor editor = this.sp.edit();
editor.clear();
removeVersionScopedKeys(editor);
applyState(editor, nextState);
persistEditor(editor, "sync state with binary version");
clearNativeCheckCache();
Expand Down Expand Up @@ -436,6 +436,15 @@ private static void putNullableString(
}
}

// Not editor.clear(): that also wiped the uuid (re-bucketing every gray
// release on each app upgrade) and the native config (no crash hold and
// no cold-start round until JS ran again). See VersionScopedKeys.
private void removeVersionScopedKeys(SharedPreferences.Editor editor) {
for (String key : VersionScopedKeys.select(sp.getAll().keySet())) {
editor.remove(key);
}
}

private void applyState(SharedPreferences.Editor editor, StateCoreResult state) {
putNullableString(editor, "packageVersion", state.packageVersion);
putNullableString(editor, "buildTime", state.buildTime);
Expand Down Expand Up @@ -618,8 +627,10 @@ public void markSuccess() {
* (so the next launch resolves to the built-in bundle) and delete the
* downloaded versions, keeping only the directory of the version this
* process is running from (a silent reset must not break its on-demand
* asset loads). Only the client uuid survives — it identifies the install
* for gray release bucketing and must not change on reset.
* asset loads). The install's own keys survive (VersionScopedKeys): the
* client uuid identifies the install for gray release bucketing and must
* not change on reset, and the native config stays with the host, as on
* iOS and Harmony.
*/
public void resetToPackagedBundle() {
synchronized (commitLock) {
Expand All @@ -637,18 +648,14 @@ private void resetToPackagedBundleLocked() {
resetState.buildTime = getBuildTime();
resetState.firstTime = false;
resetState.firstTimeOk = true;
String uuid = sp.getString("uuid", null);
// The cached check response still advertises the version this reset
// removes; drop it first so a failure aborts before the state wipe.
if (!clearNativeCheckCache()) {
throw new IllegalStateException("Failed to persist state for reset check cache");
}
SharedPreferences.Editor editor = sp.edit();
editor.clear();
removeVersionScopedKeys(editor);
applyState(editor, resetState);
if (uuid != null) {
editor.putString("uuid", uuid);
}
persistEditorOrThrow(editor, "reset to packaged bundle");
ignoreRollback = false;
Log.i(TAG, "Reset to packaged bundle");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package cn.reactnative.modules.update;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
* The keys of the main state file that describe installed versions rather
* than the install itself: per-version info (hash_*), the first-load marker,
* the native round's incomplete breadcrumb and the response cache's legacy
* location. A binary upgrade or a reset to the packaged bundle drops exactly
* these (plus the state fields applyState rewrites). Everything else — the
* client uuid that buckets gray releases, the persisted native config the
* cold-start round and crash hold run from, the bundle hash cache keyed by
* package version — belongs to the install and survives, as on iOS and
* Harmony.
*/
final class VersionScopedKeys {
static final String HASH_INFO_PREFIX = "hash_";

private VersionScopedKeys() {
}

static List<String> select(Collection<String> keys) {
List<String> doomed = new ArrayList<>();
for (String key : keys) {
if (key == null) {
continue;
}
if (key.startsWith(HASH_INFO_PREFIX)
|| key.equals(UpdateContext.KEY_FIRST_LOAD_MARKED)
|| key.equals(SyncCoordinator.KEY_ROUND_INCOMPLETE)
|| key.equals(SyncCoordinator.KEY_RESP_CACHE)) {
doomed.add(key);
}
}
return doomed;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cn.reactnative.modules.update;

import static org.junit.Assert.assertEquals;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import org.junit.Test;

public class VersionScopedKeysTest {
@Test
public void dropsVersionKeysAndKeepsInstallKeys() {
List<String> doomed = VersionScopedKeys.select(Arrays.asList(
"uuid",
"nativeConfig",
"bundleHashCache",
"packageVersion",
"currentVersion",
"hash_abc",
"hash_def",
"firstLoadMarked",
"nativeCheckIncomplete",
"nativeCheckResp",
null
));
assertEquals(
new HashSet<>(Arrays.asList(
"hash_abc",
"hash_def",
"firstLoadMarked",
"nativeCheckIncomplete",
"nativeCheckResp")),
new HashSet<>(doomed));
}

@Test
public void keyNamesMatchTheirOwners() {
assertEquals("firstLoadMarked", UpdateContext.KEY_FIRST_LOAD_MARKED);
assertEquals("nativeCheckIncomplete", SyncCoordinator.KEY_ROUND_INCOMPLETE);
assertEquals("nativeCheckResp", SyncCoordinator.KEY_RESP_CACHE);
assertEquals("nativeConfig", SyncCoordinator.KEY_CONFIG);
}
}
8 changes: 5 additions & 3 deletions harmony/pushy/src/main/ets/ArchiveLimits.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import fileIo from '@ohos.file.fs';
import statvfs from '@ohos.file.statvfs';
import {
ERROR_FILE_OPERATION_FAILED,
ERROR_DELTA_FAILED,
createPushyError,
getErrorMessage,
Expand Down Expand Up @@ -66,11 +65,14 @@ export function checkOriginalSize(

/**
* 目标所在文件系统必须还能再写 bytesToWrite + 安全余量。取不到可用空间
* (返回 0/异常)视为未知,放行。
* (返回 0/异常)视为未知,放行。空间不足以 `code` 失败:错误码按阶段归类,
* 与 Android/iOS 一致——下载阶段 DOWNLOAD_FAILED,解压阶段 PATCH_FAILED。
* 换镜像的判定(IsMirrorRetryableCode)与补丁健康度遥测都按这个码聚合。
*/
export async function ensureFreeSpace(
target: string,
bytesToWrite: number,
code: string,
): Promise<void> {
let probe = target;
while (probe.length > 1 && !fileIo.accessSync(probe)) {
Expand All @@ -92,7 +94,7 @@ export async function ensureFreeSpace(
const needed = Math.max(0, bytesToWrite) + FREE_DISK_MARGIN_BYTES;
if (free < needed) {
throw createPushyError(
ERROR_FILE_OPERATION_FAILED,
code,
`insufficient disk space: need ${needed} bytes, have ${free}`,
);
}
Expand Down
14 changes: 10 additions & 4 deletions harmony/pushy/src/main/ets/DownloadTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ export class DownloadTask {
}
const originalSize = await this.readOriginalSize(archiveFile);
checkUncompressedSize(archiveStat.size, originalSize);
await ensureFreeSpace(unzipDirectory, originalSize);
await ensureFreeSpace(unzipDirectory, originalSize, ERROR_DELTA_FAILED);
try {
await zlib.decompressFile(archiveFile, unzipDirectory);
} catch (e) {
Expand Down Expand Up @@ -569,8 +569,9 @@ export class DownloadTask {
received += data.byteLength;
if (!writeError && baseOffset + received > MAX_ARCHIVE_BYTES) {
// 未知长度/分块传输的兜底:超过上限即停写,请求在下面结算时失败。
// 下载阶段:与 Android/iOS 一样归为 DOWNLOAD_FAILED。
writeError = createPushyError(
ERROR_DELTA_FAILED,
ERROR_DOWNLOAD_FAILED,
`archive too large: exceeded ${MAX_ARCHIVE_BYTES}`,
);
}
Expand All @@ -592,7 +593,11 @@ export class DownloadTask {
}
try {
if (probeFreeSpace) {
await ensureFreeSpace(params.targetFile, reserve);
await ensureFreeSpace(
params.targetFile,
reserve,
ERROR_DOWNLOAD_FAILED,
);
}
await fileIo.write(writer.fd, data);
} catch (error) {
Expand Down Expand Up @@ -828,14 +833,15 @@ export class DownloadTask {
}
if (totalAll > MAX_ARCHIVE_BYTES) {
writeError = createPushyError(
ERROR_DELTA_FAILED,
ERROR_DOWNLOAD_FAILED,
`archive too large: ${totalAll} bytes`,
);
throw writeError;
}
await ensureFreeSpace(
params.targetFile,
totalAll > 0 ? totalAll - baseOffset : 0,
ERROR_DOWNLOAD_FAILED,
);
// 先落 sidecar 再放行写入,流中崩溃才有得续。
await this.writeResumeMeta(params.targetFile, buildResumeMeta(totalAll));
Expand Down
Loading
Loading