Installer tasks (I1..I10)
Specs are in installer/requirements.md, rules in installer/CLAUDE.md,
behaviour in installer/DESIGN.md. Test conventions are in
tests/README.md — read it before adding a test.
I1 — Toolchain and window
- Add the
x86_64-pc-windows-msvctarget - Compile
res/installer.rcfrombuild.rsviaembed_resource- Watch
../rbr.icotoo, or a new icon is not a reason to rebuild
- Watch
- Put the app manifest, icon and VERSIONINFO in the .rc
- Use a plain
fn mainrather thanwWinMain - Pin every dependency with
= - Add
tests/Test-BinarySize.ps1as the release gate
I2 / I2a — Pre-wizard layouts
Superseded by I2b. Two layouts were built and removed; what survived them is in the current design. See the CHANGELOG and git history.
I2b — Wizard
Protocol groundwork in shared
- Derive the tree from the package’s storage path:
category(),tree_levels(),leaf() - Add
valid_nameas the choke point that stops a lookup key becoming a path, andPackageRef::parsesplitting on the first separator - Add the remaining
GAME_*constants - Add the category vocabulary
Shell
- Host the scenes in a PropertySheet, one dialog template each, with a per-scene primary label and a Cancel that confirms
- Own
WizardStateat the sheet - Remove the superseded ribbon and two-column code and strings
- Size every page 330x215 DLU, which scene 5 forces: three lists visible at once, each full width so a co-driver’s path is not clipped
Scenes
- Scene 1 — language combo, defaulting to the OS UI language
- Scene 2 — MOTD with an offline fallback, the secret field, and
server status from
/health - Scene 3 — path edit, Browse, and a game dropdown fed by a debounced scan
- Scene 3 — three route buttons: Download/Update, Configure/Install,
Restore/Uninstall
- Lay them out in two columns, the main flow alone on the right as the default button, wider and as tall as the other two together
- Measure that arrangement in
Test-UiLayout.ps1, since “looks primary” is the whole point of it - Leave the sheet’s Next disabled: the routes are alternatives, not one primary and two afterthoughts
- Scene 4 — the catalogue as a checkbox tree with partial states and
parent/child propagation, falling back to the cache when offline
- Refuse Download until every choice category has a pick, naming the ones missing
- Scene 4b — the download phase on a page of its own, between the tree
and the questions
- Share scene 6’s template and handlers: it is the same screen at an earlier moment
- End a failure or a cancel at the summary rather than walking on to questions about an install that cannot happen
- Scene 5 — one single-select list per choice category, each option
labelled with its path inside that category
- Preselect a sole option, and refuse Install until every list that has options has a pick
- Default to what is already installed, so the Configure route does not make the user re-answer last time’s questions
- Carry picks across a trip Back to scene 4 and forward again
- Scene 6 — phase label, progress bar, current package with its own
byte count, and a live log pane
- No Back and no Next: the page runs itself and walks on
- Scene 7 — result, game and folder, the picks, the extras, what came
down the wire, and where the log is
- Hide the log pane on success and show it on failure
- The restore scene — see “Restore” below
- Add the run log (
runlog.rs): in memory plus a line-by-line flush- Write it to
<game dir>\.pacenotes\logs\install-<stamp>.log, one file per run, newest five kept - Attach it when scene 3 settles a folder, replaying what came before, so the file is the whole run
- Write it to
- Write English and Polish strings for every caption, label and modal
Models (no Win32, tested cross-platform)
-
tree.rs— manifest plus cache to nodes with states, check propagation and per-category validation -
choices.rs— scene 5’s lists, the path labels, preselection and the no-choice extras
I3 — WinHTTP client
- Wrap open/connect/request; add
getandget_streamingwith a sink - Read in 64 KiB chunks, reporting progress
- Set explicit timeouts, or a dead server parks the worker and Cancel does nothing
- Treat a body shorter than its Content-Length as an error
- Turn WinHTTP error codes into readable text via winhttp.dll
- Add
--selftest-http, writing a report to a file or a message box- Never write the token into the report
I4 — Worker thread and progress protocol
- Post a
Messageper job as a leakedBoxthe receiver reclaims - Spawn with
std::thread; only the UI thread touches controls - Carry a request id, so a late answer to a superseded request is recognised and dropped
- Support cancellation through a shared flag read between chunks
- Veto the window close until the worker reports back
- Carry phase, item, per-item and per-phase byte counts, and an
optional log line in the progress message
- Throttle posts to ten a second
- Hand the job a
Reporterrather than the window handle, so “a worker never touches a control” holds by construction
I5 — API and cache
- Write
sha256.rswith no dependencies- Test against the published vectors AND against chunked input
- Add
fetch_manifest(game),fetch_motdandhealth - Lay the cache out at
<game dir>\.pacenotes\cache\- Open it when scene 3 settles a folder, as the log does
- Key
cache.jsonby game as well, since directory+name is not unique across games - Store a
PackageInfosnapshot per entry, so the tree can show local-only packages offline - Write the index atomically
- Download to a temp file, verify the sha256, then move into place
- Build the URL with
PackageInfo::download_path(), never by hand - Reject a digest mismatch
- Build the URL with
- Give each leaf a cache state, withholding an entry whose payload has vanished rather than failing at install time
I6 — Zip reader
- Read the EOCD and central directory; inflate and stored
- Create the directories a .NET zip carries no entries for
- List entries without extracting, for the backup phase
- Return
/-separated paths on every host: they are ledger keys - Guard against zip-slip, treating
\as a separator and rejecting:outright- Fail the package WHOLE, so nothing is half-installed
- Check each entry’s CRC32 during extraction
- Use a nibble table rather than the usual 256-entry one
- Detect and refuse Zip64 by name rather than misreading it
- Decode CP437 names without the UTF-8 flag, as .NET does
- Test against real
Compress-Archiveoutput, deflated and stored - Add a manual check over a directory of real packages
I7 — Ledger and engine
Ledger (ledger.rs)
- Read and write
.pacenotes\installed.jsonatomically - Key entries by
game/directory/name - Record per file: the path as written, a case-folded map key, the
sha256 of what was written, and
prior - Record
exclusive_groupas installed, so uninstall works offline - Add
owners_of(path), so a file is deleted only when no other entry lists it - Write
state: "installing"BEFORE extracting - Recover in
plan()rather than a startup scan: aninstallingentry is torn down and laid again - Never overwrite an unreadable ledger — it may be a newer build’s
Engine (engine.rs)
-
plan(): selection plus ledger plus cache to ordered actions- Supersede a ledgered member of the same category or group
- Reinstall when the sha changed
- Enforce
requireswithin the game, satisfied by the SELECTION alone — “already installed” does not count, since anything unselected is uninstalled in the same run
- Uninstall before install, always
- Pre-flight before touching anything: free space and a writability probe
- Verify each payload by its sha256 rather than CRC-checking every entry
- Write each file to a temp name and rename
- Uninstall by
prior, and only when the on-disk sha still matches what was written - Prune emptied directories, deepest first
- Detect overlaps as a pure function over the file lists the pre-flight already read
- Measure “the game is not running” — the engine refuses when told,
but
wizard/scene/installation.rsstill passesfalse- Check for the marker process (I9)
Conflict reporting
- Log every overlap, naming each writer in install order
- POST the report after
execute(), so what is reported is what happened- Trim it to the server’s 8 KiB cap by measuring, not guessing
- Convert keys to the refs the report carries, dropping any that will not split rather than guessing
- Send no token, no identity and no local paths, and say in the log that it was sent
- Add
post_jsontohttp.rs, sharing one request path withget
Per-game configuration step (install.rs)
-
configure(game, game_dir, picks)dispatching on the game, handed only a category-to-key map - RBR: set
languageandsoundsinPaceNote.ini, preserving every other line, and write from a template only when the file is absent- Report a missing plugin directory for the caller to localize
- Keep the author’s spacing, inline comments, commented-out lines and the file’s CRLF
- Check
soundsagainst what is on disk, keeping what the package shipped when it is not there - Write nothing on a second run, so a re-run takes no restore point
- Put the step’s edits back into the ledger
(
engine::record_configured), or the next uninstall reads our own edit as somebody else’s - Log a file the step wrote that no package owns, rather than adopting it — see requirements.md, “deliberately not done”
Per-game choice categories
- Take the list from the compiled-in schema rather than a constant
- Keep the localized label and explanation in the
.rctables, mapped by category id, because UI text must not come from a server
- Keep the localized label and explanation in the
- Demand a pick on scene 4 only in those categories
- Ask one question per entry on scene 5, in schema order, and skip the scene when a game declares none
- Carry
install_rootas a token the installer knows, never a path- Refuse a token this build cannot place
- Decide what the token means for the three games other than RBR
Restore
- Write a restore point whenever a run changes anything, holding the
pre-run ledger and the files that run replaced
- Name points by UTC stamp, sortable and legal on Windows
- Suffix the name when two runs share a second, rather than overwriting the first run’s way back
- Never prune points automatically
- List points newest first, each a date plus what the run after it did
- Preview the chosen point: removals first, then put-backs, then what is left alone, then what cannot come back
- Offer “remove everything this installer added” as a restore to an empty state — the same code, not a second path
- Plan purely (
restore::plan), and hand the worker the same value the preview showed- Install by payload digest, not through the cache index, which holds only the newest version of each key
- Leave a package whose payload is gone installed rather than removing what cannot be replaced
- Write the snapshot’s per-file provenance afterwards, or a later uninstall deletes a file the user always had
- Run it on the progress screen like any other phase
- Take a restore point of its own first
- Re-run the configuration step with the picks the restored state implies
- Re-download a payload the restore needs but the cache lacks — see requirements.md, “deliberately not done”
Crash reporting
- Install a panic hook that writes file, line and message to the run
log, or beside the exe before a folder is settled
- Open a window for the FIRST panic only: the modal pumps the queue and can deliver the message that panicked
- Show the user something localized and plain, offering to open the
log
- Re-apply the chosen language first, since the hook can run on the worker thread
- Add
--selftest-panicandtests/Test-CrashReport.ps1
Tests
-
plan()table tests: fresh install, no-op re-run, switching a pick, supersede, changed sha, withdrawn package, unfinished install, everyrequirescase -
execute()in a tempdir: unowned-file backup, an identical file put back on uninstall, a file replaced by Steam left alone, a shared file kept, recovery frominstalling, a corrupt payload, free space, restore points - The RBR step against the real shipped
PaceNote.ini - End to end through the GUI against a live server
- Restore: the bad-update rollback, the provenance proof, a missing payload, the point-name collision, and a full pass through the UI
I8 — Auth flow
- Use the secret as the bearer for
/packagesand/download- Send no token with
/report/conflict
- Send no token with
- Validate it with
GET /whoami, not/packages- Map 401 to rejected and everything else to “could not ask”, so a protocol change cannot lock a paying user out
- Warn when a key is within 14 days of expiry, naming the date rather than a day count
- Handle no 403 at all, and pin that in the auth matrix
Server protocol changes
- Check the install schema by hash (S11)
- Compile
schemas/<game>.tomlinto a static table frombuild.rs - Keep a list of older hashes a build still accepts, for rollouts
- Block downloading for that game only, and only when something has to come down the wire
- Run the check only on a manifest that came from a server
- Take behaviour from the installer’s own copy of the schema
- Test that the compiled-in copy is byte-identical and hashes the same, and that it has no CRLF
- Compile
- Accept versions of the form
sha-xxxxxxxx(D5) — nothing to do, as no scene shows a version - Give the test fixtures their own
schema.toml
Pre-release hardening
From the design review’s period 2. Specs for each are in
installer/requirements.md; the findings are in DESIGN-REVIEW.md.
Ordered security first, then what support needs, then what a user hits.
- C1 Decide what happens on a redirect
- Find out whether WinHTTP re-sends
Authorizationacross a host change, and record the answer - Turn redirects off and follow them by hand: a hop limit,
httpsonly, and the header dropped when the host changes - Refuse to redirect a request that carries a body, rather than choosing silently between re-sending it and making it a GET
- Pin it with
tests/Test-Redirects.ps1, driving the real WinHTTP path rather than a reimplementation of it
- Find out whether WinHTTP re-sends
- S1 Build the user agent from
CARGO_PKG_VERSION - §7 Give the run log a header and per-line timestamps
- D12 Try
/packageseven when/healthfailed - D12 Cap what
http::getwill buffer - P11 Probe writability on scene 3, offering to restart elevated
- §7 Retry a transient download, and list failures on scene 7
I9 — Polish
- Check free disk space (in
preflight, which knows what the plan needs) - Replace the unwraps left in UI paths with a modal and a log line
- Sweep orphaned payloads from the cache on startup
- Decide where “remove from cache” lives now there is no library view
- Fix keyboard and tab order, default buttons, and Esc per scene
- Download/Update → Back → Configure/Install → Back → Download/Update skips the
“at least one item checked” check, and repeating it locks scene 4; the
configuration screen must not open unless every category has at least
one item downloaded and cached
- Scene 4b fetched again what an earlier transfer had cached, once a selection could come back to it
- Show the message of the day with the line breaks of its
.txt - Give the buttons that carry the normal flow forward a light green background: Next, its relabelled forms, Finish, and Download/Update
- The installation process is quite slow. Check for possible optimisations, like copying, moving, writing files in bulk.
- Show a category with only some items ticked as partially selected, not blank
- Open the Download tree with every row expanded, and enlarge the window beyond 800x600 to show more of it
I10 — Release gate
- Save a
cargo bloatreport todocs/bloat.txt - Pass the 700 kB gate on the release build, and record the final size
- Version every build from its release tag,
v/X.Y.Z, so no version is ever typed by hand-
build.rsreads the tag, refuses any other shape, and falls back to0.0.0-devwithout one - The user agent and the conflict report take that version
- VERSIONINFO comes from a header
build.rswrites, so its four fields cannot drift -
Cargo.toml’s version stops meaning anything, and says so - Release on pushing a
v/*tag, retiring the merge and manual releases that readCargo.toml - Replace
Test-VersionSync.ps1with a test that builds from a tag and reads the version back out of the binary - Update DEVELOPMENT.md, the CLAUDE.md files and the test docs
-
- If over budget, pull a lever from
requirements.md: the icon first, then a hand-rolled JSON parser - Decide what to do about
vcruntime140.dll: document the dependency or build with a static CRT and re-measure - Manual end-to-end on a clean Windows VM, per game: online with and
without a secret, offline from cache, switching a pick on a re-run,
and a server dropped mid-download
- The clean VM is also what settles the vcruntime question