Skip to content

Commit 1e2685c

Browse files
CopilotJusterZhu
andauthored
Add durable update coordination and next-launch reconciliation
Co-authored-by: JusterZhu <11714536+JusterZhu@users.noreply.github.com>
1 parent cce6ac1 commit 1e2685c

19 files changed

Lines changed: 1998 additions & 19 deletions

README-EN.md

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ The project uses composable abstractions so you can replace version comparison,
2222
- **Extensible architecture**: `IVersionComparer`, `IUpdateDownloader`, `IHashValidator`, `IApkInstaller`, and more are replaceable.
2323
- **Resumable downloading**: sidecar metadata + streaming writes for better reliability on unstable networks.
2424
- **Unified event model**: built-in validation, progress, completion, and failure events for UI/log integration.
25+
- **Durable coordinator**: complete-attempt serialization, pending-install tracking, next-launch version confirmation and explicit recovery.
2526

2627
## Quick Start
2728

@@ -53,7 +54,7 @@ dotnet add package GeneralUpdate.Avalonia.Android
5354
dotnet test tests/GeneralUpdate.Avalonia.Android.Tests/GeneralUpdate.Avalonia.Android.Tests.csproj
5455
```
5556

56-
### Basic Usage
57+
### Low-level Usage
5758

5859
```csharp
5960
using GeneralUpdate.Avalonia.Android;
@@ -91,6 +92,63 @@ if (check.Success && check.UpdateFound && check.PackageInfo is { } packageInfo)
9192
}
9293
```
9394

95+
### Coordinated Updates and Next-launch Confirmation
96+
97+
For new integrations, use `GeneralUpdateBootstrap.CreateCoordinator(options)` instead of manually chaining the three
98+
low-level operations. It owns the bootstrap it creates, serializes the complete workflow, and records a durable intent
99+
**before** launching Android's installer. Keep the coordinator for the host's update-service lifetime.
100+
101+
```csharp
102+
using GeneralUpdate.Avalonia.Android.Enums;
103+
104+
await using var coordinator = GeneralUpdateBootstrap.CreateCoordinator(options);
105+
coordinator.StateChanged += (_, args) =>
106+
Console.WriteLine($"{args.Result.Stage}: {args.Result.Outcome}");
107+
108+
// Read the actual installed app version from the host, not the server's target version.
109+
var startup = await coordinator.ReconcileAsync(installedVersion, cancellationToken);
110+
if (startup.Outcome == UpdateCoordinatorOutcome.NoPendingUpdate)
111+
{
112+
// Invoke from the host's update command/policy, not from a notification callback.
113+
var result = await coordinator.RunAsync(installedVersion, cancellationToken);
114+
// InstallerLaunched is a handoff; Updated is reported only by reconciliation.
115+
}
116+
```
117+
118+
| Operation | Contract |
119+
|---|---|
120+
| `RunAsync(currentVersion, ct)` | Check → download/verify → persist intent → launch installer. Existing pending state returns `PendingUpdateExists` without another install. |
121+
| `ReconcileAsync(currentVersion, ct)` | Offline startup check. Installed version equal to or newer than the pending target returns `Updated`; the old version remains `AwaitingInstallation` or `RecoveryRequired`. No intent returns `NoPendingUpdate`. |
122+
| `RetryAsync(currentVersion, ct)` | Explicit recovery of a pending attempt: reconcile first, then rediscover and verify the same target. A changed server target returns `RecoveryRequired` without overwriting the earlier handoff. Never installs a persisted path or reuses stored credentials. Use `RunAsync` again after failures that left no pending intent. |
123+
| `AbandonAsync(ct)` | Explicitly forget pending tracking, including corrupt state. Does **not** cancel Android installation, delete APKs, or roll back the app/data. |
124+
125+
`StateChanged` reports named stages and a terminal outcome, rather than treating a generic “completed” notification as
126+
installation success. Pass an `IUpdateEventDispatcher` to `CreateCoordinator` for Avalonia UI dispatch, as shown below.
127+
Check `Outcome` and `FailureReason`: `InstallerLaunched`, `NoUpdate` and `Updated` have different meanings.
128+
Subscribe to `AddListenerDownloadProgressChanged` for byte/speed progress and register `AddListenerUpdatePrecheck` before
129+
starting operations for optional-update policy (`true` still means skip; forced updates bypass it). These are forwarded
130+
through the coordinator, so factory users do not need access to its underlying bootstrap.
131+
The legacy `CreateDefault` / `IAndroidBootstrap` API remains available and unchanged.
132+
133+
The factory stores only a versioned attempt ID, original/target versions, timestamp and handoff phase in
134+
`<NoBackupFilesDir>/generalupdate/pending-update.json`. No APK path, URL, package credentials or exception is serialized.
135+
The file is atomically replaced from a flushed temporary file in the same directory. Corrupt, oversized or unknown-schema state
136+
fails closed instead of silently starting another update. A write failure before handoff prevents installer launch; uncertainty
137+
after handoff remains pending for reconciliation. This protects process-restart recovery, not arbitrary storage hardware failure.
138+
For a custom location/store, pass `pendingStore: new JsonPendingUpdateStore(privatePersistentPath)` (services namespace);
139+
do not place the record in a cache, shared downloads folder or backup-restored location.
140+
The default JSON store holds an exclusive `.lock` file lease for the entire workflow, preventing cooperating coordinator
141+
instances/processes using the same state path from overwriting each other's intent. Do not delete that lock file while in use.
142+
Custom stores can implement `IPendingUpdateStoreLeaseProvider`; otherwise the host must enforce a single coordinator.
143+
Separate state paths do not protect a shared APK staging directory, so use one coordinator per staging directory.
144+
145+
**Recovery policy:** reconcile on each app launch and when returning from the installer, using the actual installed version.
146+
An unchanged version is not proof the user rejected installation—it may still be in progress. Offer explicit retry or abandonment;
147+
do not automatically loop on either. If the server now offers a different target, reconcile the earlier handoff or explicitly abandon
148+
its tracking before starting a new attempt. A successful reconciliation confirms the observed version, not application health or successful
149+
data migration. Silent installation, automatic relaunch, OS downgrade/rollback, signed manifests and APK identity preflight are not
150+
provided. The host still supplies installation permissions/FileProvider configuration and must validate device behavior.
151+
94152
### Server-Driven Version Validation
95153

96154
`ValidateAsync(currentVersion, cancellationToken)` only needs the version installed on the device: the component queries
@@ -234,12 +292,13 @@ Cancellation while waiting for the gate still throws `OperationCanceledException
234292
cancellation during verification returns a canceled result. Notification exceptions are logged and isolated, whereas a pre-check
235293
exception produces a failed validation result. Installed-version confirmation is still not part of disposal or a completed event.
236294

237-
Use a single host coordinator and a private staging directory for the full check → download/verify → install sequence.
295+
Use a single coordinator and a private staging directory for the full check → download/verify → install sequence.
238296
Only hand the returned verified path to the installer; do not modify or remove the APK while installation may be reading it.
239297
The public installer method also supports independent calls, so it does not establish verification provenance for arbitrary paths.
240-
Persist the target version before handoff, reconcile the actual installed version on next launch, and clear obsolete staging files
241-
only when no update/installer is using them. Keep resumable partial files for a bounded retention period.
242-
Permission prompting, actual installation outcome, app relaunch and recovery from a bad release or data migration remain host/platform
298+
`CreateCoordinator` handles target-version persistence and next-launch reconciliation; call `ReconcileAsync` at startup with the
299+
actual installed version. With the low-level API, implement that tracking in the host. Clear obsolete staging files only when no
300+
update/installer is using them. Keep resumable partial files for a bounded retention period.
301+
Permission prompting, app relaunch and recovery from a bad release or data migration remain host/platform
243302
responsibilities; they are not made reliable merely by a successful installer intent.
244303

245304
## Source Review and Production Readiness
@@ -260,13 +319,20 @@ retained with revision-pinned evidence; **its defect descriptions refer to the p
260319
| Callback errors | Synchronous notification subscriber, dispatcher and logger exceptions cannot replace operation outcomes. A throwing pre-check fails validation instead of bypassing host policy. | `BootstrapLifecycleTests`: throwing subscribers/loggers/dispatchers and fail-closed pre-check. |
261320
| Package license | NuGet metadata now declares Apache-2.0, matching the existing LICENSE. | MSBuild property evaluation against LICENSE. |
262321

263-
Validation after remediation: **160/160 core tests passed** (no failures or skips), including HEAD-rejection fallback and
322+
Validation of the earlier remediation: **160/160 core tests passed** (no failures or skips), including HEAD-rejection fallback and
264323
authentication-policy failures returning terminal validation results before provider/network invocation.
265-
The local Android build could not run because the `android` workload is missing (`NETSDK1147`); current PR CI requires approval.
324+
At that stage the local Android build was blocked by the missing `android` workload (`NETSDK1147`).
266325
Tests for these fixes use the existing .NET core test project; they are not Android device installation tests.
267-
The fixes do **not** add desktop support, installer completion callbacks, automatic restart/rollback, independent manifest signing,
268-
APK identity preflight, persisted workflow state, directory-wide coordination, or a runnable Avalonia sample.
269-
UI dispatch, verified-path handoff, cache retention and next-launch reconciliation remain explicit host responsibilities described above.
326+
The coordinator addition now provides persistent intent tracking, complete-attempt orchestration, offline installed-version
327+
reconciliation, explicit retry/abandon recovery and stage-specific outcomes (see the new integration section).
328+
Coordinator validation: **57 focused tests and all 217 core tests passed**. With the Android workload installed,
329+
the Android library **Release build succeeded**, including the default factory. Coverage includes real HTTP downloader/storage/hash
330+
integration with fake transport/installer, persistent state across recreated coordinators, uncertain handoff, concurrency and recovery.
331+
The build retains the existing dependency advisory noted below and three XML-documentation warnings. PR CI still requires approval;
332+
no device/emulator installation, restart or application-health validation was performed.
333+
The fixes do **not** add desktop support, native installer completion callbacks, automatic restart/rollback, independent manifest signing,
334+
APK identity preflight, or a runnable Avalonia sample.
335+
UI dispatch, providing the actual installed version, invoking reconciliation at startup and cache retention remain host responsibilities.
270336
Metadata-provider/storage extensibility and production dependency/device validation remain follow-up work, not silently resolved findings.
271337

272338
### Historical scope and evidence (before remediation)
@@ -373,7 +439,9 @@ Snapshots are in-memory and do not establish verified-package provenance or cras
373439
shutdown and post-install reconciliation. The default logger is no-op; production hosts need stage/failure telemetry without credentials.
374440

375441
**Dependency risks:** [AndroidX Core is the runtime package dependency; SourceLink is private build tooling][review-project].
376-
No dependency advisory audit or transitive inventory was performed for this assessment, so version age alone is not a vulnerability finding.
442+
The original assessment did not include a dependency advisory audit or transitive inventory.
443+
The subsequent coordinator build reports a pre-existing `Microsoft.Build.Tasks.Git` 8.0.0 advisory
444+
([GHSA-23fw-v26w-5fgq](https://github.com/advisories/GHSA-23fw-v26w-5fgq)); this change does not update dependencies.
377445
Validate the resolved dependency graph, Android workload/toolchain compatibility and packaged artifact on supported devices before release.
378446

379447
### Original validation evidence and remaining production release gates

README.md

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
- **可扩展架构**`IVersionComparer``IUpdateDownloader``IHashValidator``IApkInstaller` 等均可替换。
2323
- **断点续传下载**:支持 sidecar 元数据与流式写入,提升弱网场景稳定性。
2424
- **统一事件通知**:提供验证、进度、完成、失败等事件用于 UI/日志集成。
25+
- **持久化协调器**:完整流程串行化、待安装状态跟踪、下次启动版本确认以及显式恢复。
2526

2627
## 快速开始
2728

@@ -91,6 +92,51 @@ if (check.Success && check.UpdateFound && check.PackageInfo is { } packageInfo)
9192
}
9293
```
9394

95+
### 完整流程协调与下次启动确认
96+
97+
新接入可使用 `GeneralUpdateBootstrap.CreateCoordinator(options)`,无需自行串联三个低层调用。
98+
协调器按“查询 → 下载并验证 → 持久化意图 → 拉起安装器”执行;`CreateDefault` 低层 API 保持兼容。
99+
100+
```csharp
101+
using GeneralUpdate.Avalonia.Android.Enums;
102+
103+
await using var coordinator = GeneralUpdateBootstrap.CreateCoordinator(options);
104+
coordinator.StateChanged += (_, args) =>
105+
Console.WriteLine($"{args.Result.Stage}: {args.Result.Outcome}");
106+
107+
// installedVersion 必须来自当前实际安装的应用,不能传服务端目标版本。
108+
var startup = await coordinator.ReconcileAsync(installedVersion, cancellationToken);
109+
if (startup.Outcome == UpdateCoordinatorOutcome.NoPendingUpdate)
110+
{
111+
// 由宿主更新命令/策略触发,不要在通知回调里同步等待此调用。
112+
var result = await coordinator.RunAsync(installedVersion, cancellationToken);
113+
}
114+
```
115+
116+
- `RunAsync`:串行执行整个流程;存在待确认记录时返回 `PendingUpdateExists`,不会覆盖并再次安装。
117+
- `ReconcileAsync`:离线核对待确认目标版本。实际版本达到或超过目标才返回 `Updated`
118+
仍为旧版本则返回 `AwaitingInstallation``RecoveryRequired`,没有记录为 `NoPendingUpdate`
119+
- `RetryAsync`:显式恢复,先核对安装版本,再重新查询服务器并下载验证同一目标;若服务端目标改变,
120+
返回 `RecoveryRequired` 并保留原交接记录,需先核对或明确放弃后再开始新尝试。
121+
不会安装从磁盘恢复的任意路径,也不会重复安装已经达到的目标版本。
122+
- `AbandonAsync`:显式放弃跟踪,也可清除损坏状态;不取消系统安装、不删除 APK、不回滚应用或数据。
123+
124+
默认状态保存在 `<NoBackupFilesDir>/generalupdate/pending-update.json`,不是可被清理的 APK 缓存。
125+
只记录 schema、尝试 ID、原始/目标版本、时间和交接阶段,不保存 URL、APK 路径、凭据或异常。
126+
状态通过同目录临时文件刷新后原子替换;安装前保存失败就停止,安装交接后的不确定状态留待下次启动核对。
127+
损坏、过大或未知 schema 不会被当成“无更新”,而是明确失败。可注入 `IPendingUpdateStore`
128+
自定义文件位置必须是应用私有持久化目录,不应参与备份恢复。
129+
默认 JSON 存储对整个流程持有 `.lock` 文件独占租约,协调使用同一路径的实例/进程;使用中不要删除锁文件。
130+
自定义存储可实现 `IPendingUpdateStoreLeaseProvider`,否则宿主必须保证单协调器。不同状态路径不能保护共用的 APK 目录。
131+
132+
保持协调器与宿主更新服务相同生命周期,释放时可 `await DisposeAsync()`
133+
`StateChanged` 区分阶段与最终 `Outcome``InstallerLaunched` 仅表示交接,不能展示为安装成功。
134+
UI 线程仍需传入 `IUpdateEventDispatcher`;应用每次启动及从安装器返回时调用 `ReconcileAsync`
135+
协调器还转发 `AddListenerDownloadProgressChanged``AddListenerUpdatePrecheck`,无需获取内部 bootstrap;
136+
pre-check 应在开始操作前注册,仍保持 `true` 表示跳过、强制更新不调用的兼容语义。
137+
旧版本仍在运行不等于用户拒绝安装,可能尚未完成;应由用户明确选择重试或放弃,不要自动循环。
138+
此核心闭环确认的是实际安装版本,不是应用健康或数据迁移成功;静默安装、自动重启、系统回滚仍不提供。
139+
94140
### 服务端版本校验
95141

96142
`ValidateAsync(currentVersion, cancellationToken)` 只需要当前应用的版本号:组件按
@@ -174,7 +220,12 @@ ZIP、差分包、驱动包不会交给 Android 安装器;`body` 为空数组
174220
[英文评审正文](https://github.com/GeneralLibrary/GeneralUpdate.Avalonia/blob/main/README-EN.md#source-review-and-production-readiness)
175221
下表保留原始评审背景;当前已按项修复 B1–B5、S1、回调异常隔离及许可证元数据,详见正文的修复状态表。
176222
新增回归测试覆盖重试/续传、超时与取消、状态及清理失败、资源释放竞争、认证源限制和重定向拒绝。
177-
修复后的核心测试 **160/160 通过**;本地 Android 构建因缺少工作负载(`NETSDK1147`)受阻,当前 PR CI 尚需批准。
223+
此前缺陷修复后的核心测试 **160/160 通过**;当时本地 Android 构建因缺少工作负载(`NETSDK1147`)受阻。
224+
本次协调器新增验证:**57 个专项测试、全部 217 个核心测试通过**;安装 Android 工作负载后,
225+
包含默认工厂的 Android 库 **Release 构建成功**。覆盖真实下载器、文件及哈希处理与模拟网络/安装器、
226+
协调器重建后的持久化确认、不确定交接、并发及恢复。构建仍报告已有的
227+
`Microsoft.Build.Tasks.Git` 8.0.0 安全公告([GHSA-23fw-v26w-5fgq](https://github.com/advisories/GHSA-23fw-v26w-5fgq)
228+
和 3 个 XML 文档警告;未修改依赖版本,PR CI 仍需批准,尚未进行真机/模拟器安装、重启或应用健康验证。
178229
这些测试不等价于真机安装或自动回滚验证。
179230

180231
| 维度 | 结论与风险 | 建议 |
@@ -193,8 +244,9 @@ ZIP、差分包、驱动包不会交给 Android 安装器;`body` 为空数组
193244
没有证据支持把旧的“写流未关闭即重命名”问题、Zip Slip 或 Android 签名绕过列为当前缺陷。
194245

195246
**生产结论:不能直接作为开箱即用的跨平台、全闭环生产更新器。**
196-
上述修复并未新增桌面支持、安装完成确认、自动重启/回滚、独立清单签名、APK 身份预检或可运行的 Avalonia 示例。
197-
仍需限定可信更新源、补齐宿主协调及恢复逻辑,并通过 Android 真机故障场景验收后,
247+
新增协调器已提供持久化意图、下次启动的安装版本确认、完整流程串行化及重试/放弃恢复。
248+
仍未新增桌面支持、原生安装器完成回调、自动重启/回滚、独立清单签名、APK 身份预检或可运行的 Avalonia 示例。
249+
仍需限定可信更新源、在宿主启动时调用核对并处理平台权限和健康恢复,并通过 Android 真机故障场景验收后,
198250
作为 Android 更新基础组件使用;详细上线门槛见完整评审。
199251

200252
## 目录结构

0 commit comments

Comments
 (0)