You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
v0.1.12: one-click self-update with SHA-256 verify + on-exit swap
User clicks "Install update" in the version banner → plugin downloads
the new DLL, verifies SHA-256, stages a .new file next to the loaded
one, and spawns a hidden PowerShell helper that swaps the file once
ACT exits. User effort: one click + restart ACT.
──────────────────────────────────────────────────────────────────
Why this design — the constraint that shapes everything else:
Windows holds an exclusive lock on a loaded .NET DLL for the process
lifetime. The plugin can't overwrite its own DLL from inside ACT.
Workaround is "write to <dll>.new, then atomic-rename once the
process exits". The rename has to be done by something OTHER than
the plugin (because the plugin dies with ACT), so we spawn a
detached PowerShell job that Wait-Process'es on ACT's PID and does
the Move-Item once it returns.
──────────────────────────────────────────────────────────────────
New pieces:
* src/Core/UpdateChecker.cs — ParseLatestDllAsset extracts the
browser_download_url + digest from the GH release feed. Strips
the "sha256:" prefix, returns ("","") on any missing piece.
* src/Core/UpdateStatus.cs — UpdateCheckResult gains LatestDllUrl
+ LatestDllSha256. Empty digest = installer refuses to stage,
user falls back to browser.
* src/Core/PluginUpdater.cs — pure download + verify. Refuses
without a digest, on size overflow (10 MB cap vs ~76 KB DLL),
on any hash mismatch. Constant-time hex compare.
* src/UpdateInstaller.cs — UI-side staging. Writes <dll>.new,
drops EQ2Lexicon.ACTPlugin.UPDATE_READ_ME.txt with manual
recovery steps, spawns the hidden PowerShell helper. If the
helper spawn fails (no PowerShell, restricted exec policy, AV
blocking child procs) the staged file + readme are still there
so the user can complete the swap by hand.
UI changes (SettingsPanel):
* Version banner now has TWO buttons: "Install update" (primary)
+ "Download in browser" (secondary). Install only shown when
we have both URL and digest; Download always available as the
fallback for any failure case.
* New italic status line below the buttons shows
"Downloading…" → "Verified. Staging…" → "Update staged. Close
and reopen ACT to apply" — or a clear error.
* Successful stage disables both buttons so the user can't
re-stage in the same session.
Plugin.BeginAutoUpdateAsync orchestrates the pipeline. All
exceptions caught and surfaced via SetUpdateInstallStatus —
clicking Install must never crash ACT or leave the UI in a
half-disabled state.
Tests: 10 new PluginUpdaterTests pinning the verify branch
(happy path, hash mismatch, no-digest refusal, oversized
rejection, fetcher exceptions, case-insensitive hash compare).
5 new UpdateChecker tests for ParseLatestDllAsset (real GH
shape, draft skip, non-DLL asset skip, missing digest, case-
insensitive prefix). Total 174/174.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CLAUDE.md
+18-1Lines changed: 18 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -35,7 +35,9 @@ The split also unlocks GitHub Actions CI: the workflow at `.github/workflows/ci.
35
35
|`src/Core/Snapshots.cs`| Plain DTOs (`EncounterSnapshot`, `CombatantSnapshot`, `DamageTypeAggregate`, `AttackTypeSnapshot`) that mirror the slice of ACT's data model the payload builder reads. |
36
36
|`src/Core/PluginConfig.cs`| User settings persisted as XML. `Load(path)` and `Save(path)` take the path from the caller (resolved in `Plugin.cs` via `ActHelpers.GetConfigPath()`). API token is DPAPI-encrypted on disk (current-user scope) with a `DPAPI:` prefix; legacy plaintext from v0.1.0–v0.1.4 still loads. |
37
37
|`src/Core/UploadClient.cs`|`HttpClient` wrapper. `TestConnectionAsync` (GET `/api/auth/whoami`), `UploadEncounterAsync` (POST `/api/parses/ingest`). `ValidateServerUrl` rejects non-https except `localhost`/`127.0.0.1`/`[::1]`. Adds `X-Lexicon-Signature` HMAC header to every upload (see PayloadSigner) and blocks uploads when `UpdateStatus.UploadBlocked == true`. |
38
-
|`src/Core/UpdateChecker.cs`| GitHub-releases-feed fetcher + pure version-comparison logic. `Compute(currentVersion, releasedVersions)` returns an `UpdateCheckResult` with `Current`/`SlightlyStale`/`TooOld`/`DevBuild`/`Unknown`. Threshold = `StaleThresholdVersions` (2) — older than that blocks uploads. Failure modes (network down, GH 5xx, rate-limit) all collapse to `Unknown` so the gate fails OPEN. |
38
+
|`src/Core/UpdateChecker.cs`| GitHub-releases-feed fetcher + pure version-comparison logic. `Compute(currentVersion, releasedVersions)` returns an `UpdateCheckResult` with `Current`/`SlightlyStale`/`TooOld`/`DevBuild`/`Unknown`. Threshold = `StaleThresholdVersions` (2) — older than that blocks uploads. Failure modes (network down, GH 5xx, rate-limit) all collapse to `Unknown` so the gate fails OPEN. `ParseLatestDllAsset` extracts the latest-release DLL URL + SHA-256 digest for the self-update flow. |
39
+
|`src/Core/PluginUpdater.cs`| Download + SHA-256 verify for the self-update flow (v0.1.12+). Pure (`IDllAssetFetcher` injects HTTP). Constant-time hash compare. Refuses to install without a digest, on size overflow (10 MB cap vs ~76 KB DLL), and on any hash mismatch. |
40
+
|`src/UpdateInstaller.cs`| UI-side staging for the self-update flow. Writes `<dll>.new` next to the running DLL, drops a manual-recovery readme, spawns a hidden PowerShell `Wait-Process -Id <act_pid>; Move-Item -Force *.new *.dll` that swaps the file once ACT exits. Recovery readme covers the case where the helper script gets killed by AV. |
39
41
|`src/Core/PayloadSigner.cs`| HMAC-SHA256 of the upload body keyed by the user's API token. See "Payload integrity" below for the threat model and what this does/doesn't defend. |
40
42
|`tests/EQ2Lexicon.ACTPlugin.Tests/`| xUnit project, `net48`. 72 tests. References Core only (UI types aren't testable without ACT). Covers PayloadBuilder, PluginConfig (incl. DPAPI roundtrip), UploadClient (incl. URL validator). Coverage collected via `coverlet.collector`. |
41
43
@@ -144,6 +146,21 @@ Failure modes (offline, GitHub 5xx, rate-limit, malformed JSON) all collapse to
144
146
145
147
No caching of the GitHub response — request volume is at most a handful per user per day, well under the unauthenticated 60/h/IP limit, and re-fetching on every ACT start means a user who just installed an update sees the banner clear instantly after restarting ACT.
146
148
149
+
## Self-update (v0.1.12+)
150
+
151
+
The version banner gets a primary "Install update" button (alongside the existing "Download in browser" secondary fallback) when both an asset URL and a SHA-256 digest were resolved from the GitHub release feed. Click flow:
152
+
153
+
1.**Download** the DLL bytes from `LatestDllUrl` via HttpClient. Capped at 10 MB; refused on any non-2xx response. Bytes held in memory only.
154
+
2.**Verify** SHA-256 against `LatestDllSha256` (pulled from the release feed's `digest` field) via constant-time compare. Mismatch → refuse, surface error, don't touch disk. **No digest** → also refuse — shipping unverified code into a running .NET process is unacceptable, the user gets nudged to the browser fallback.
155
+
3.**Stage** by writing the bytes to `EQ2Lexicon.ACTPlugin.dll.new` next to the loaded DLL. (Windows allows creating this file; it does NOT allow overwriting the loaded `.dll` itself — that's why we don't just overwrite.)
156
+
4.**Drop a recovery readme** (`EQ2Lexicon.ACTPlugin.UPDATE_READ_ME.txt`) in the same folder with manual swap instructions, in case the helper script fails.
157
+
5.**Spawn a hidden PowerShell helper**: `Wait-Process -Id <act_pid>; Move-Item -Force *.new *.dll`. Sleeps in the background until ACT exits, then atomic-renames. User sees nothing.
158
+
6.**UI message**: "Update staged. Close and reopen ACT to apply — the swap happens automatically." Buttons disable.
159
+
160
+
User effort: one click + restart ACT whenever convenient. The Railway URL fallback covers users who don't restart for days — the v0.1.12 staging sits there, the v0.1.11 keeps uploading via parses.eq2lexicon.com (same Railway service), no breakage.
161
+
162
+
**What it doesn't try to do**: relaunch ACT itself. Spawning a "wait then re-launch host" helper from a plugin is the kind of behaviour that gets flagged by AV. The user restart is small friction; the auto-relaunch isn't worth the suspicion budget.
163
+
147
164
## Language assumptions (English-only EQ2)
148
165
149
166
The plugin assumes English ACT EQ2-parser output throughout. ACT does have German + French EQ2 parser plugins on record, but as of 2026 there are **no active non-English EQ2 servers** — the EU/JP servers (Storms = French, Valor = German, Sebilis = Japanese) were consolidated into the English-only Thurgadin server in 2016, and the current TLE servers (Varsoon, Kaladim) are English-only. So this assumption is safe today; documenting it so future-you doesn't have to re-research it.
0 commit comments