feat(launcher): implement verified atomic updates
This commit is contained in:
parent
2198a0cc8e
commit
2d2a5b5046
34 changed files with 6755 additions and 61 deletions
|
|
@ -478,6 +478,131 @@ gate (user): clean-profile first-run against real DATs.
|
|||
fixture; rollback test; refusal-while-running test; self-update staging test;
|
||||
suites green. Connected gate (user): staged-manifest update swap end-to-end.
|
||||
|
||||
### Pinned updater contracts (v1, BINDING)
|
||||
|
||||
This section is the single source of truth for every LA10 feed and on-disk
|
||||
shape. Readers use strict, case-sensitive `System.Text.Json` parsing, reject
|
||||
unknown or duplicate properties, and reject unsupported schema versions
|
||||
before doing network, extraction, or activation work.
|
||||
|
||||
The production feed is pinned to GitHub owner/repository
|
||||
`eriknihlen/acdream`; the launcher reads
|
||||
`https://github.com/eriknihlen/acdream/releases/latest/download/manifest.json`.
|
||||
Tests may inject a loopback HTTP URI, but production artifacts and redirects
|
||||
must use HTTPS. `manifest.json` is:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"version": "1.2.3",
|
||||
"minimumLauncherVersion": "1.1.0",
|
||||
"clients": {
|
||||
"win-x64": {
|
||||
"url": "https://github.com/eriknihlen/acdream/releases/download/v1.2.3/acdream-client-win-x64.zip",
|
||||
"sha256": "<64 hex characters>",
|
||||
"size": 123
|
||||
}
|
||||
},
|
||||
"launchers": {
|
||||
"win-x64": {
|
||||
"url": "https://github.com/eriknihlen/acdream/releases/download/v1.2.3/acdream-launcher-win-x64.zip",
|
||||
"sha256": "<64 hex characters>",
|
||||
"size": 123
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`version` and `minimumLauncherVersion` are strict SemVer 2.0 strings. Build
|
||||
metadata is ignored for precedence; numeric identifiers are compared without
|
||||
fixed-width integer overflow. RID keys are exact lowercase portable RIDs.
|
||||
Both dictionaries are required and the running RID must have a client and a
|
||||
launcher row. Artifact sizes are positive and capped by the launcher's
|
||||
download limit; SHA-256 is exactly 64 hex characters. ZIP URLs are absolute.
|
||||
Client ZIPs have the two host executables at their root
|
||||
(`AcDream.App[.exe]`, `acdream-headless[.exe]`); launcher ZIPs have
|
||||
`acdream-launcher[.exe]` at their root. No implicit wrapper directory exists.
|
||||
|
||||
Every extracted client version has
|
||||
`DataDirectory/app/<version>/install.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"version": "1.2.3",
|
||||
"rid": "win-x64",
|
||||
"archiveSha256": "<64 hex characters>",
|
||||
"archiveSize": 123,
|
||||
"files": [
|
||||
{ "path": "AcDream.App.exe", "sha256": "<64 hex characters>", "size": 123, "unixMode": 0 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Paths use `/`, are relative, normalized, unique under ordinal-ignore-case,
|
||||
and sorted ordinally. `unixMode` contains only the portable permission bits
|
||||
captured from the ZIP entry. Startup verifies every recorded regular file by
|
||||
size/SHA, rejects unrecorded files/reparse points, and requires the two host
|
||||
executables before admitting a version. Extraction uses a random sibling
|
||||
directory under `DataDirectory/app/`; promotion to `<version>/` is one
|
||||
same-volume directory rename.
|
||||
|
||||
`DataDirectory/app/current.json` is the only activation authority:
|
||||
|
||||
```json
|
||||
{ "schemaVersion": 1, "currentVersion": "1.2.3", "previousVersion": "1.1.0" }
|
||||
```
|
||||
|
||||
`previousVersion` is omitted for the first activation. Pointer writes are
|
||||
write-through temporary-file + same-directory atomic rename. The last valid
|
||||
pointer is also atomically preserved as `current.previous.json`; startup may
|
||||
restore that exact backup only when `current.json` is missing/malformed and
|
||||
the referenced version verifies. Orphan LA10 staging directories and pointer
|
||||
temporaries are transaction-owned by exact names and are removed under the
|
||||
update lease. A corrupt installed version is never silently selected; the
|
||||
explicit one-step rollback swaps the two verified pointer versions.
|
||||
|
||||
`DataDirectory/app/.update-session.lock` is the cross-process barrier. Each
|
||||
supervised launcher activity holds a shared OS handle from before executable
|
||||
resolution until terminal process observation; an update/rollback holds the
|
||||
exclusive handle for its entire recovery/download/extract/promote/pointer
|
||||
transaction. Failure to acquire the exclusive handle is an immediate refusal,
|
||||
not a wait behind a running session. The open handle, not lock-file contents,
|
||||
owns the lease and therefore releases after process death.
|
||||
|
||||
Launcher self-update staging lives at
|
||||
`DataDirectory/launcher-update/transactions/<transactionId>/` and the sole
|
||||
durable authority is `DataDirectory/launcher-update/pending.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"transactionId": "0123456789abcdef0123456789abcdef",
|
||||
"state": "staged",
|
||||
"version": "1.2.3",
|
||||
"rid": "win-x64",
|
||||
"targetDirectory": "<absolute current launcher directory>",
|
||||
"archiveSha256": "<64 hex characters>",
|
||||
"archiveSize": 123,
|
||||
"files": [
|
||||
{ "path": "acdream-launcher.exe", "sha256": "<64 hex characters>", "size": 123, "unixMode": 0 }
|
||||
],
|
||||
"apply": null
|
||||
}
|
||||
```
|
||||
|
||||
Before mutation a next-start helper copied outside the target directory
|
||||
atomically advances the plan to `applying` and fills `apply` with each path's
|
||||
`hadOriginal` bit. It waits for the initiating launcher PID without invoking a
|
||||
shell, moves originals into the transaction backup tree, then moves verified
|
||||
staged files into place. It never opens a target with truncate/overwrite. On
|
||||
success the plan becomes `awaitingConfirmation`; the new launcher confirms at
|
||||
its first managed instruction, after which backup and plan cleanup is safe. An
|
||||
`applying` plan is rolled back before retry, and failure to start/confirm the
|
||||
new launcher restores every original (and removes every no-original target).
|
||||
All plan paths are re-derived/contained under the pinned data root except the
|
||||
target directory, which must equal the actual launcher base directory.
|
||||
|
||||
## LA11 — closeout
|
||||
|
||||
- One connected-gate script `docs/research/2026-XX-XX-campaign-la-test-script.md`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue