Lot 8: distribution — installer, purge, release workflow - #2
Merged
Merged
Conversation
The distribution page lists what is left in the order it is taken, with the three MSI measurements first, and links to a new Lot 13 page that carries the updater's decisions: never a silent poll, WinHTTP, SHA-256, GitHub's keyless release endpoints, and the relaunch. What the updater demands of the package stays with the package. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ompt A minimal package built from PowerShell through Windows Installer's COM automation, one text file and no UI, went through all three from an unelevated shell with exit 0 and no prompt; the logs are quoted. The summary stream's "no elevation required" bit turns out to be the whole mechanism, and MSIINSTALLPERUSER is ignored in its presence. The choice is closed: an MSI authored that way, per-user by that bit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
build.rs now writes one resource script per binary: the icon and a VERSIONINFO whose FileVersion carries the commit, whose description and OriginalFilename name the file, whose copyright line is read from LICENSE and whose company is the package author. A tree with uncommitted changes is flagged as a private build. Windows Installer's repair and upgrade rules need versioned files, and the Properties dialog now says what a file is. The checklist reads the blocks back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
scripts\msi.ps1 writes the package with nothing but Windows Installer's COM automation and makecab: the two executables and the license, per-user without a prompt, older versions removed first, newer ones refused, deterministic identifiers so the same commit gives the same package. The release step builds it next to the zip and runs every ICE the SDK ships over it, unpacking MsiVal2 without elevation and registering its evaluator per user under the CLSID it actually serves. Measured along the way, in the package's own logs: the summary bit alone leaves ProgramFilesFolder at C:\Program Files (x86); ALLUSERS=2 alone turns per-machine for an administrator; the documented pair with MSIINSTALLPERUSER=1 is what redirects to %LOCALAPPDATA%\Programs. FindRelatedProducts has to precede LaunchConditions or a downgrade slips through. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`gamemode-executor purge` lists what it will remove and asks: the logon task, the configuration wherever it was found, the log wherever it was written, the session marker, the two profile folders once empty, and last the executables -- through msiexec when Windows Installer owns them, through a detached shell after this process has exited when they were unpacked by hand. It refuses while a game is running and stops the watcher first, with WM_CLOSE on its session window. It removes only what it recognises as its own: a task it did not register and a folder holding anything else stay. The plan is computed apart from discovering the machine, so tests drive it on scratch folders, including the detached shell. A test also holds the upgrade code to the one scripts\msi.ps1 writes. The reference and How it works describe the command and what uninstalling leaves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
release.yml runs on a vX.Y.Z tag: it checks the tag names the version in Cargo.toml, runs the same checklist as a developer, and publishes the installer, the zip and their SHA-256 checksums with gh. The notes are written by scripts\release-notes.ps1 from what the build produced -- the documentation link the binary carries, the checksums, the commits since the previous tag -- so they can be tried locally and need no API. The README and Getting started now send people to the release rather than to a source build, and say what uninstalling leaves and what purge removes; the reference and AGENTS.md carry the release procedure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first field run opened a console: a detached cmd.exe has no console of its own, so ping created a visible one. CREATE_NO_WINDOW alone gives the shell a hidden console its children inherit, and a child outlives its parent on Windows without any flag. The same run left the program's folder behind because the PowerShell it was typed into sat inside it; purge now says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The shell that deletes a hand-installed copy after purge exits was a cmd.exe pausing on the batch idiom ping -n 3, a guessed delay from a program that promises to connect to nothing. It is now a hidden Windows PowerShell that waits for exactly this process with Wait-Process and then removes what it was given; the installed case starts msiexec the same way. Only single quotes reach the command line, so std's quoting carries it intact, and the test hands the shell the id of a process that has already exited. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four remarks from the first install from the package, all taken. The package ends with two custom actions, the program's own commands run as the user: init, which writes the starter configuration only where there is none, and install-task, which registers the logon task only where there is none and then starts the watcher -- the icon appearing is the confirmation an install without a UI otherwise lacks. Both keep what exists unless --force, which the package never passes; init used to fail on an existing file, which would have failed every upgrade. The starter configuration names no program any more: two beeps, commented out, and the recipes for the rest. That makes a configuration with no commands valid, which it was not -- the watcher then detects and logs and runs nothing. The zip stops shipping a config.toml, so both ways in leave the same machine. A test-only family lets the package be installed beside the real one, which is how the actions were measured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… does The installer's actions flashed a console twice at the end of every install: Windows Installer does not hide an executable action's console, and the probe that had said otherwise could not see a console host's window. The actions now run through gamemode-executorw.exe, which has none, and the maintainer saw no window on the next install. Rather than give the twin two hidden verbs of its own, both binaries now parse the same `Cli` from a new `cli` module in the library and differ in one boolean, whether there is a console to print to. `init`, `install-task` and `uninstall-task` open the log -- the configuration's `log_dir` when one can be read, the default location otherwise -- and write what they did at `info` under a new `setup` category: configuration written, kept or replaced; task registered, kept, replaced or removed; watcher started. A person at the console sees the same lines; the installer's run leaves them in the log, where until now nothing said how a machine had been set up. `schtasks` runs with CREATE_NO_WINDOW too. The distribution record corrects its "no window" note in place, the windowless-watcher page records why the twin took the whole command line, and the reference, how-it-works and getting-started pages describe the shared command line and the setup lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…es it The first uninstall from Programs and Features put up the Restart Manager dialog asking to close "GameModeExecutor watcher": the package could start the watcher but not stop it, so InstallValidate found it holding the files about to go. `stop` is Quit from outside -- WM_CLOSE on the session window, then a wait on the single-instance mutex, which the watcher releases only after its last log line. It lives in `service`, `purge` uses it instead of its own copy, and the package runs it as an immediate action before InstallValidate on an uninstall and on an upgrade, from the executable already installed, carrying on if that fails so a version too old to know the verb degrades to the dialog rather than to an error. The mutex is now read with OpenMutexW rather than taken, so a watcher starting at that instant cannot mistake the probe for a running instance. Two processes write the log while a watcher is being stopped. The file is opened for appending only, so the file system places each write at the end and a line is one write; 80 processes writing at once left 80 whole lines. Recorded where the file is opened and in the reference. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The immediate action ran in the interactive session: the watcher logged Stopped, the action logged the stop 252 ms later, and the Restart Manager never asked, since no session-end line was written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first real upgrade stopped the watcher from the new package, then ran the old product's own stop action during RemoveExistingProducts, which found nothing and logged a second line for it. The condition now excludes a product being removed by an upgrade, and the upgrade is recorded: 700 ms from stop to start, one product listed, no dialog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ummary Uninstalling left the logon task registered and armed: at the next logon it would have started an executable that was no longer there and failed, every time. The task is the package's infrastructure, not the user's data, so an UnregisterTask action now runs `uninstall-task` on an uninstall, deferred, before RemoveFiles takes the executable it runs. Not on the removal an upgrade performs, so a delay or a configuration path chosen with `install-task` survives an upgrade as before. The summary information read "Installation Database" in Explorer's Details tab, the SDK's customary title, which says what the file is to a tool and nothing to a person. It now names the product and version, says what the program does, carries the commit and the documentation link, and sets the creation time Explorer otherwise takes from the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI kept only the zip, and inside GitHub's own zip at that. Both artefacts now go into one artifact named after the version, at compression level 0: GitHub serves every artifact as a zip of its own whatever it holds, so the level keeps it a plain container around two files that are already compressed. Raw files come from the release workflow, which attaches them to the release itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lot 8, distribution: a per-user Windows Installer package, a
purgecommand, a tag-driven release workflow, and the field fixes from installing it for real. Design record indocs/design/08-distribution.md.VERSIONINFOblock with the version, the commit and the copyright line fromLICENSE; a dirty tree is flagged as a private build.scripts/msi.ps1, authored from Windows Installer's own automation andmakecab, no third-party tooling: per user, no elevation, into%LOCALAPPDATA%\Programs\GameModeExecutor, deterministic GUIDs, downgrades refused, older versions removed first.build.ps1 releasebuilds it and runs the SDK's 102 ICEs over it, failing on any finding.stopbefore the Restart Manager would ask to close the watcher,init(keeps an existing configuration),install-task(keeps an existing task, starts the watcher so the icon is the confirmation),uninstall-taskon removal only.gamemode-executor purge: lists and removes every trace — task, configuration, log, marker, executables — through Windows Installer or a detached shell as appropriate, refuses mid-game, leaves anything it does not recognise.src/cli.rs): the twin parses the same arguments and prints nothing. The setup commands log what they did under a newsetupcategory, so a machine's log says how it was set up.v*tag builds on a runner, validates, and publishes the installer, the zip, checksums and notes generated from the commits.Field verification
On the maintainer's machine, 2026-09-17/18, read from the log and watched on screen: install, reinstall, upgrade 0.1.0 → 0.1.1 → back, uninstall with no dialog and no orphaned task, purge. The first install on a second machine is still to come and does not block this merge.
Test plan
.\scripts\build.ps1 release— 112 + 3 tests, 102 ICEs clean,release OKdist\, log read after eachwindows-latest(run 35289134694)🤖 Generated with Claude Code