merge: Campaign LA LA10 - updater review-closed
This commit is contained in:
commit
da4fb3de19
40 changed files with 9778 additions and 68 deletions
|
|
@ -339,11 +339,21 @@ src/
|
|||
adjacent
|
||||
`.<pak>.acdream-bake.<guid:N>.tmp` files are
|
||||
transaction-owned crash residue
|
||||
Updates/ -> pinned GitHub manifest + strict SemVer/RID
|
||||
authority, bounded verified streaming download,
|
||||
hardened ZIP extraction, immutable
|
||||
`app/<version>/` installs, atomic `current.json`
|
||||
activation/rollback, and durable next-start
|
||||
launcher self-update journal; one OS-handle
|
||||
shared-session/exclusive-update barrier spans
|
||||
every launcher process
|
||||
-> references Platform only; no Avalonia or game-host dependency
|
||||
|
||||
AcDream.Launcher/ Avalonia 12 Windows/Linux desktop shell
|
||||
ViewModels/ -> thin MVVM projection over Launcher.Core,
|
||||
including the first-run DAT/bake wizard
|
||||
including the first-run DAT/bake wizard and
|
||||
nonfatal startup/manual update state, actions,
|
||||
progress, cancellation, rollback, and errors
|
||||
-> references Launcher.Core only (Platform transitively); it never owns
|
||||
a second profile, process, status, or credential state graph
|
||||
-> every per-RID publish composes the separately published self-contained
|
||||
|
|
|
|||
|
|
@ -471,13 +471,230 @@ gate (user): clean-profile first-run against real DATs.
|
|||
verify, unpack to `DataDirectory/app/<version>/`, atomic `current.json`
|
||||
pointer swap, refuse while any session runs, keep previous version for
|
||||
one-step rollback.
|
||||
- Launcher self-update: staged download + rename-dance on next start.
|
||||
- Launcher self-update: staged download + target-local atomic replacement on
|
||||
next start.
|
||||
- Session-config composition targets `app/current`'s binaries.
|
||||
|
||||
**Acceptance:** manifest/download/verify/swap tests against a local HTTP
|
||||
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 use a separate internal fixture constructor that may admit loopback HTTP;
|
||||
that allowance never propagates to the production feed. Production manifest
|
||||
and artifact URIs use HTTPS. Automatic redirects are disabled and every
|
||||
redirect hop is validated before it is requested; redirect loops, a chain over
|
||||
five hops, and any HTTPS-to-HTTP downgrade are rejected. `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, download
|
||||
archives, corrupt-version quarantine directories, and pointer temporaries are
|
||||
transaction-owned by exact lowercase GUID names and are removed only under the
|
||||
exclusive update lease; near-matching user names are preserved. 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; launcher disposal requests
|
||||
child termination and does not release that handle until the child is actually
|
||||
observed terminal. 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` (schema 3):
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 3,
|
||||
"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 the verified staged launcher becomes the next-start helper and
|
||||
waits for the initiating launcher PID without invoking a shell. It first copies
|
||||
the complete verified payload into the target-local
|
||||
`.acdream-self-update-<transactionId>/incoming/` tree. The plan then advances
|
||||
to `applying`; `apply` is an ordinally sorted union of new payload paths, the
|
||||
owned metadata path, and obsolete paths from the previous ownership record:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"path": "acdream-launcher.exe",
|
||||
"operation": "install",
|
||||
"hadOriginal": true,
|
||||
"priorSha256": "<64 hex characters>",
|
||||
"priorSize": 123,
|
||||
"priorUnixMode": 0,
|
||||
"replacementSha256": "<64 hex characters>",
|
||||
"replacementSize": 456,
|
||||
"replacementUnixMode": 0
|
||||
},
|
||||
{
|
||||
"path": "new-support.dat",
|
||||
"operation": "install",
|
||||
"hadOriginal": false,
|
||||
"priorSha256": null,
|
||||
"priorSize": null,
|
||||
"priorUnixMode": null,
|
||||
"replacementSha256": "<64 hex characters>",
|
||||
"replacementSize": 456,
|
||||
"replacementUnixMode": 0
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Every `hadOriginal` entry persists the exact pre-mutation SHA-256, length, and
|
||||
Linux mode bits; a no-original entry has all three prior fields null. Every
|
||||
install entry likewise persists the verified replacement metadata, while a
|
||||
remove entry has all three replacement fields null. The journal is invalid
|
||||
unless those fields agree with `hadOriginal` and `operation`.
|
||||
|
||||
Existing targets are replaced with one same-filesystem atomic replace whose
|
||||
backup is also target-local. Previously absent noncanonical files use one
|
||||
same-filesystem rename; obsolete owned files use one rename into backup. The
|
||||
canonical launcher path therefore contains either the complete old file or the
|
||||
complete new file at every durable crash boundary. Rollback first performs a
|
||||
zero-mutation preflight of the complete target-local transaction and every
|
||||
journal entry. It rejects reparse points, unsafe parents, unrecorded paths,
|
||||
ambiguous file layouts, and any SHA-256/length/mode mismatch in a prior,
|
||||
incoming, or discard file. Only a fully preflighted rollback may atomically
|
||||
restore backups; newly created files move to target-local discard rather than
|
||||
being deleted. The complete prior target set is then reverified before the
|
||||
plan enters durable `rolledBack` state while retaining the journal. Retry is
|
||||
allowed only after that prior set is reverified again and the plan returns to
|
||||
`staged`. Thus rollback is atomic per file and idempotent after a process/power
|
||||
loss. Any ambiguity preserves the applying plan and transaction evidence and
|
||||
forbids launching the canonical path for manual recovery. Linux mode bits come
|
||||
from the verified incoming file. A helper that cannot immediately
|
||||
acquire the exclusive update lease defers the staged plan and exits without
|
||||
restarting the old launcher, preventing restart loops.
|
||||
|
||||
Successful application writes strict target ownership metadata at
|
||||
`<launcher directory>/launcher.install.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"version": "1.2.3",
|
||||
"rid": "win-x64",
|
||||
"files": [
|
||||
{ "path": "acdream-launcher.exe", "sha256": "<64 hex characters>", "size": 123, "unixMode": 0 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The archive may not supply that reserved metadata path. A prior valid record is
|
||||
the only authority for obsolete-file removal; the first managed update does
|
||||
not infer ownership of unrelated legacy files. On success the plan becomes
|
||||
`awaitingConfirmation`; the new launcher confirms at its first managed
|
||||
instruction, after which the helper releases its lease and the confirmed
|
||||
launcher reclaims plan, data-transaction, and target-local residue. 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).
|
||||
The helper restarts the restored canonical launcher only after a fresh complete
|
||||
verification of the retained `rolledBack` journal; rollback corruption or an
|
||||
unsafe backup/discard tree exits without starting either launcher.
|
||||
Reading `pending.json` never performs cleanup. Ordinary startup attempts the
|
||||
exclusive lease without waiting and skips update cleanup entirely when another
|
||||
session/staging transaction owns it. All plan paths are re-derived/contained
|
||||
under pinned roots; the target directory must equal the actual launcher base
|
||||
directory.
|
||||
|
||||
Every portable archive and persisted relative path rejects Windows device
|
||||
segments on every host: `CON`, `PRN`, `AUX`, `NUL`, `CLOCK$`, `CONIN$`,
|
||||
`CONOUT$`, `COM1`-`COM9`, `LPT1`-`LPT9`, and the Windows-equivalent superscript
|
||||
forms `COM¹`/`COM²`/`COM³` and `LPT¹`/`LPT²`/`LPT³`, including extensions.
|
||||
|
||||
## LA11 — closeout
|
||||
|
||||
- One connected-gate script `docs/research/2026-XX-XX-campaign-la-test-script.md`
|
||||
|
|
|
|||
|
|
@ -298,11 +298,23 @@ preview would be a deliberate divergence we are NOT taking.
|
|||
install to `DataDirectory/app/<version>/`; atomic pointer swap
|
||||
(`current.json`); never while any session is running; keep the previous
|
||||
version for one-step rollback.
|
||||
- **Launcher self-update:** same feed; staged download; rename-dance swap
|
||||
on next start (a running exe can't replace itself on Windows).
|
||||
- **Launcher self-update:** same feed; staged download; target-local atomic
|
||||
replacement on next start after the running process exits.
|
||||
- **Feed hosting:** GitHub Releases (user-confirmed). Manifest and zips
|
||||
are release assets; the launcher pins the repo/owner in its config.
|
||||
|
||||
The exact v1 manifest, extracted-version record, `current.json` activation
|
||||
pointer and launcher ownership record, shared-session/exclusive-update OS
|
||||
lease, and durable self-update plan schema 3 are pinned in
|
||||
`docs/plans/2026-08-14-launcher-campaign.md` under **Pinned updater
|
||||
contracts (v1, BINDING)**. That section is normative: implementations reject
|
||||
unknown/duplicate fields and unsupported versions, use strict SemVer 2.0
|
||||
precedence, verify bounded streamed downloads before safe ZIP extraction, and
|
||||
use per-hop redirect validation plus same-filesystem atomic replacement. The
|
||||
LA9 DAT/pak install record remains the sole content descriptor fed to session
|
||||
configs; LA10 changes only which verified `app/current.json` client binaries
|
||||
the process supervisor executes.
|
||||
|
||||
## 10. Testing
|
||||
|
||||
- **Launcher.Core unit tests** (new test project, registered in
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue