diff --git a/AGENTS.md b/AGENTS.md index 101bd0c..2e40209 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -118,11 +118,14 @@ user's commands. It is the whole chain, end to end, with no game: cargo test -- --ignored a_real_activation_drives_a_session ``` -Verifying a change means running it as the user does: deploy the two -executables to the install folder, restart the logon task -(`schtasks /Run /TN "GameModeExecutor\Watcher"`), and read the log at `debug` -through a real game session. Restarting the watcher while a game is running -fires the stop commands; do not. +Verifying a change means running it as the user does: `gamemode-executor +stop`, copy the two release executables over the installed ones, +`gamemode-executor install-task`, and read the log at `debug` through a real +game session. A change to the package itself is verified by uninstalling +from *Programs and Features* and installing the new `dist\` package — the +product code is fixed per version, so the same version cannot install over +itself. Restarting the watcher while a game is running fires the stop +commands; do not. ## Pitfalls that have already cost time diff --git a/docs/design/08-distribution.md b/docs/design/08-distribution.md index eef72e3..a63ab92 100644 --- a/docs/design/08-distribution.md +++ b/docs/design/08-distribution.md @@ -1,11 +1,12 @@ # Lot 8 — Distribution -**Status: in progress since 2026-09-17.** Already there: `scripts/build.ps1` -runs the whole checklist and produces the zip archive in `dist/`, -`.vscode/tasks.json` drives it, and the repository is public with CI green -on a stock runner. The script was written first on purpose — a release that -cannot be made by hand is not one CI can make either. What is left, in the -order it is taken: +**Status: done 2026-09-18.** `v0.1.0` was published that night by the +workflow alone, from the tag's commit, with the installer, the zip and their +checksums — which is the "done when" below. The lot was taken 2026-09-17/18 +in the order listed; `scripts/build.ps1` was written first on purpose, since +a release that cannot be made by hand is not one CI can make either. One +field check is still outstanding and is recorded when it happens: the first +install on a second machine. - [x] Create the public GitHub repository and push — done 2026-09-17, with approval - [x] The three measurements below, on a minimal package, before any table is written — done 2026-09-17 @@ -16,9 +17,9 @@ order it is taken: the checklist, build the **MSI** and the **zip archive**, and publish a GitHub Release carrying both with their SHA-256 — nothing built or uploaded by hand. Written 2026-09-17 (`release.yml`, `scripts/release-notes.ps1`); - its first run is the first tag + first run 2026-09-18 on the `v0.1.0` tag, green, three assets published - [x] The documentation: *Getting started* and the README point at the release rather than at `cargo build`, the reference gains `purge`, *How it works* gains removal — 2026-09-17 -- [ ] Verified in the field: the MSI on two machines, one real upgrade, one purge round trip — the maintainer's machine done 2026-09-17, below +- [ ] Verified in the field: the MSI on two machines, one real upgrade, one purge round trip — the maintainer's machine done 2026-09-17/18, below: install, upgrade, uninstall, purge; the second machine's first install still to come **Done when** a tag alone produces a release a stranger can install from, and the two artefacts on it were built by the workflow from that tag's commit. @@ -366,23 +367,34 @@ unelevated shell, `/passive`, with a verbose log each time: | Upgrade to 0.2.0 | exit 0 in 6 s, no prompt; file replaced, the old product gone, one product left at 0.2.0 | `Nested installation UAC elevation tracks that of parent (is not elevated)` — `RemoveExistingProducts` at 1510 removed 0.1.0 first | | Uninstall | exit 0 in 6 s, no prompt; folder gone, registration gone; the neighbouring folders, the watcher's install and its scheduled tasks untouched | `Removal completed successfully` | -Two things the documentation had not made plain. **The summary stream's -"elevated privileges not required" bit (WordCount bit 3) is the whole -mechanism**: with it set, Windows Installer treats the package as per-user -outright, redirects `ProgramFilesFolder` to `%LOCALAPPDATA%\Programs`, and -logs `MSIINSTALLPERUSER property is not valid for UAC compliant package. -Ignoring` — so `ALLUSERS=2` and `MSIINSTALLPERUSER=1`, the dual-purpose -recipe, are not needed for a program with no per-machine story, and the -package is simpler without them. And **no SDK tool is needed to build the -database**: the COM automation creates tables, inserts rows and embeds the -cabinet, which means the release script can produce the MSI on a stock -runner the same way it produces the zip. `MsiDb`, `MsiFiler` and `Orca` -remain what they are, tools to inspect one. What the probe did not do and -the real package must: carry versioned files with `VERSIONINFO`, and pass -ICE validation (`MsiVal2`, from the SDK). +Two things the documentation had not made plain. The first this page got +wrong on the probe and corrected on the real package the same day, so both +readings stay here. **The probe's reading:** the summary stream's "elevated +privileges not required" bit (WordCount bit 3) looked like the whole +mechanism — with it set, Windows Installer ran without a prompt, put the +file under `%LOCALAPPDATA%\Programs`, and logged `MSIINSTALLPERUSER property +is not valid for UAC compliant package. Ignoring`, which read as "the +dual-purpose properties are not needed". **The real package's reading, +measured 2026-09-17 on an administrator account running unelevated:** it +takes all three. The bit lets the install run without a prompt; `ALLUSERS=2` +with `MSIINSTALLPERUSER=1` is what resolves the install to per-user and +redirects `ProgramFilesFolder`. With the bit alone the folder stayed at +`C:\Program Files (x86)`; with `ALLUSERS=2` alone the install turned +per-machine for an administrator and failed unelevated, 1603. The log's +"Ignoring" line is misleading: the property still decides how `ALLUSERS=2` +resolves. The probe had passed because its one file was small enough to be +looked for in the wrong folder without anyone noticing. And **no SDK tool is +needed to build the database**: the COM automation creates tables, inserts +rows and embeds the cabinet, which means the release script can produce the +MSI on a stock runner the same way it produces the zip — it did, on +2026-09-18. `MsiDb`, `MsiFiler` and `Orca` remain what they are, tools to +inspect one. What the probe did not do and the real package does: carry +versioned files with `VERSIONINFO`, and pass ICE validation (`MsiVal2`, from +the SDK, 102 evaluators, none tolerated). The choice is closed: **MSI, authored from PowerShell through Windows -Installer's automation, per-user by the summary bit.** +Installer's automation, per-user by the summary bit and the two +properties together.** Both candidates can offer per-user *or* per-machine from one installer, but **this program has no per-machine story**: the logon task, the configuration diff --git a/docs/design/13-updating.md b/docs/design/13-updating.md index e8242a9..28e0d94 100644 --- a/docs/design/13-updating.md +++ b/docs/design/13-updating.md @@ -1,10 +1,12 @@ # Lot 13 — Updating -**Status: proposed.** Decided 2026-09-17 to be a lot of its own rather than -a tail of [Lot 8](08-distribution.md): updating touches the "no network" -non-goal, the tray menu and the running process, and each of those deserves -its own measurement. Nothing here is built. It needs Lot 8 first — there is -nothing to update to until a release exists. +**Status: proposed, measured against a real release on 2026-09-18.** Decided +2026-09-17 to be a lot of its own rather than a tail of +[Lot 8](08-distribution.md): updating touches the "no network" non-goal, the +tray menu and the running process, and each of those deserves its own +measurement. Nothing here is built. Lot 8 is done and `v0.1.0` exists, so +there is now something to update from, and what a release actually answers +is recorded below rather than assumed. **Goal.** A user who wants the newer version gets it from the notification icon, without a browser, without an administrator prompt, and without the @@ -15,50 +17,148 @@ says what it found, installs it on request while no game is running, and the watcher comes back on the new version — verified in the field across a real release pair. -## What Lot 8 already settled +## What Lot 8 settled, and what it then did for this lot **The installer is the updater.** An updater that swaps files under an installer is the wrong shape, for reasons the distribution page keeps in its table of Windows Installer's four moments. So the updater downloads the -new package, verifies it, and runs it silently: `msiexec /i new.msi -/passive`, per-user, no UAC. The **zip** is the opposite case — nothing else -owns the files, so there the updater replaces them itself. -`MsiEnumRelatedProducts` on the package's UpgradeCode tells the two -installations apart. +new package, verifies it, and runs it silently: `msiexec /i new.msi`, +per-user, no UAC. `MsiEnumRelatedProducts` on the package's UpgradeCode — +already in `purge` — tells an installed copy from an unpacked one. -## What is decided, ahead of building it +**The package now stops and restarts the watcher itself.** This page first +proposed that the watcher quit before launching the installer and hand its +own relaunch to a detached shell, because Windows Installer's Restart +Manager would otherwise put up a files-in-use dialog. Lot 8 met that dialog +on its first uninstall and answered it in the package: an immediate action +runs `stop` before `InstallValidate`, and `install-task` at the end starts +the watcher through its task. Measured on a real upgrade on 2026-09-18: +700 ms from *Stopped* to *starting*, no dialog, one product listed. So the +updater has less to do than planned — start the installer detached and let +the package close the process that started it; the new version comes back +by the package's own doing. What the updater still owns is the failure +path: if the install fails after the watcher was stopped, nothing restarts +it until the next logon, so something must wait for `msiexec` and run the +task again when it exits non-zero. The same idiom as `purge`'s after-exit +shell: hidden Windows PowerShell, `Wait-Process`, then `schtasks /Run`. + +## What the release answers, measured 2026-09-18 against `v0.1.0` + +| Request | Answer | +| --- | --- | +| `HEAD github.com/{owner}/{repo}/releases/latest` | `302`, `Location: …/releases/tag/v0.1.0` — the tag, and nothing to parse but a URL | +| `HEAD …/releases/latest/download/SHA256SUMS.txt` | `302` to `…/releases/download/v0.1.0/SHA256SUMS.txt`, then the file: one line per asset, ` ` | +| `…/releases/download/v0.1.0/GameModeExecutor-0.1.0.msi` | `302` to a signed `release-assets.githubusercontent.com` URL valid for about an hour, then `200`, `Content-Length: 1462272`, `application/octet-stream` | +| `GET api.github.com/repos/{owner}/{repo}/releases/latest` | JSON: `tag_name`, `name`, `draft`, `prerelease`, `published_at`, `body`, and per asset `name`, `size`, `browser_download_url` and `digest: sha256:…`; `X-RateLimit-Limit: 60` per IP unauthenticated, `Cache-Control: max-age=60`, an `ETag` | + +Two witnesses to the same hash: the workflow's `SHA256SUMS.txt`, computed +on the runner from the files it built, and GitHub's own `digest` on each +asset, computed on upload. For the `.msi` they agreed, +`85d6178b…d0d5d1`. Neither defends against a compromised account, only +against a corrupted or truncated download, and the record says so. + +**What the program already knows without connecting:** its own version, +`build_info::VERSION`; whether Windows Installer owns it and under which +product code, `purge::installed_product()`; the installed product's +version, `MsiGetProductInfoW` with `VersionString` — which is the number +*Programs and Features* shows and the one an upgrade must beat; whether a +game is running; whether the logon task exists. + +## The shape, decided ahead of building it - **Never a silent poll.** An automatic release check breaks the "no - network" non-goal. The compatible shape is a *Check for updates…* entry - that connects only when clicked, or an explicit opt-in in the - configuration; nothing else ever opens a connection. -- **Over WinHTTP**, a Microsoft library using the system certificate store. - No HTTP crate. -- **The download verified against a SHA-256 published with the release.** - That guards against corruption, not against a compromised account, and - the record says so. -- **GitHub provides for the check with no key.** Either the REST API — - `GET /repos/{owner}/{repo}/releases/latest`, 60 requests an hour per IP - unauthenticated, `User-Agent` mandatory — or no API at all: - `github.com/{owner}/{repo}/releases/latest` answers 302 with the tag in - `Location`, and `…/releases/latest/download/{asset}` serves the latest - asset through a redirect to `objects.githubusercontent.com`. The second - suffices: a `HEAD`, a `Location`, a tag compared with - `build_info::VERSION`, nothing to parse. A token would matter only for a - private repository, and embedding one in a public executable would be a - fault. -- **The running executable.** The watcher holds its own `.exe`; Windows - Installer's Restart Manager would show a files-in-use dialog even under - `/passive`. So: refuse to update while a game is on — the same rule as the - purge — then launch the installer *and quit*, with the relaunch handed to - a detached shell (`msiexec … & schtasks /Run Watcher`). No custom action. + network" non-goal. *Check for updates…* connects when clicked and at no + other time. An opt-in check at start is not offered in this lot; if it + ever is, it is a configuration key that defaults to off, at most once a + day, and the record says what it sends. +- **The check is one request and no API.** `HEAD …/releases/latest` with + redirects disabled, the tag read from `Location`, `x.y.z` parsed from it + and compared with the running version as three numbers. A tag that does + not parse as exactly `vX.Y.Z` is "a release this version does not + understand", shown as such, never guessed at. No rate limit to think + about, no JSON, no `User-Agent` contract. The API stays in reserve for + the release notes, should the menu ever show them. +- **Every later request names the tag, not `latest`.** The checksum file + and the package are fetched from `…/releases/download//…`, so a + release published between the check and the download cannot mix one + version's hash with another's file. +- **Verified against `SHA256SUMS.txt`**, the line for the package's exact + file name, with the hash computed through BCrypt — a Microsoft library, + no crate. A mismatch deletes the file and says so; nothing is ever run + unverified. +- **Over WinHTTP**, a Microsoft library using the system certificate store + and the system proxy (`WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY`). No HTTP + crate, no relaxed certificate flag. It follows `https` → `https` + redirects by default, which the asset chain needs, and can be told not + to for the one request whose redirect *is* the answer. +- **Downloaded to `%LOCALAPPDATA%\GameModeExecutor\updates\`**, local and + disposable like the log; the watcher empties that folder when it starts, + so a package is kept only until the version it carries is running. + Windows Installer caches its own copy of every package it installs, so + deleting the download costs a later repair nothing. +- **Installed with `msiexec /i /qn /l*v \install.log`**, + started detached — not a child that shares the watcher's fate — and the + package's own actions stop this process and start the new one. `/qn` + rather than `/passive`, provisionally: `/passive` shows Windows + Installer's progress window, and the program's rule is no windows; the + icon going and coming back is the visible part, as it is for the + installer run by hand, and the version in the tooltip afterwards is the + confirmation. To be measured on screen before it is settled. +- **Refused while a game is running**, for the purge's reason: the stop + commands would fire mid-game and the new watcher would fire the start + commands seconds later. The menu says so; the user quits the game and + clicks again. +- **The zip copy is told, not updated.** Nothing owns its files but the + user, and replacing two executables under a running logon task from a + hidden shell is exactly the file-swapping shape this lot exists to avoid. + A hand-installed copy gets the same check, and the menu entry then + opens the release page. The person who chose no installer keeps their + files in their hands. +- **The menu is the whole interface**, as everywhere else: the entry reads + *Check for updates…*, then *Up to date (0.1.0)* greyed, or *Update to + 0.2.0…*, or *Could not check: offline* greyed; the tooltip mirrors it. + No balloon, no dialog. The log carries the same lines under `setup`, + with the URL, the size and the hash, so an update is as readable + afterwards as an install. +- **A downgrade is never offered.** The package refuses one anyway + (`NEWERVERSIONDETECTED`), and the comparison makes it unreachable. + +## What it does not defend against, said plainly + +- **A compromised release or account.** The hash proves the file is the one + the workflow published, not that the workflow was honest. Authenticode + was declined in Lot 8 for want of a certificate anyone else's Windows + trusts; that decision, not this lot, is where the line moves if it ever + does. +- **SmartScreen does not see it.** A file fetched through WinHTTP carries + no Mark of the Web — browsers and Explorer write it, libraries do not — + so `msiexec` runs the package without the warning a person downloading + the same file would meet. Convenient, and worth knowing: the program is + the one vouching for the file, through the hash and nothing else. +- **A stale mirror or a captive portal.** A `302` to somewhere that is not + GitHub, or a `200` that is an HTML page, must fail the parse and be + shown as "could not check", never as "up to date". ## To measure, when the lot is taken -1. The redirect chain of `releases/latest` and `releases/latest/download` - through WinHTTP, and what a rate-limited or offline answer looks like - from the menu. -2. A `/passive` upgrade launched by the watcher itself, with the watcher - gone by the time Windows Installer looks for files in use. -3. The zip path: replacing two executables under a running logon task, and - what happens when the task fires in the middle of it. +1. WinHTTP against the four requests above: reading `Location` with + redirects disabled, following the asset chain to the signed URL with + them enabled, a proxy, an offline machine and a DNS failure, each as + seen from the menu and the log. +2. The watcher launching its own upgrade: `msiexec /qn` detached, + `StopWatcher` closing the process that started it, `RegisterTask` + bringing the new version back — and the failure path, with a package + built to fail after `InstallValidate`, restarting the old one. +3. `/qn` against `/passive`, on screen, success and failure. +4. The updates folder emptied at start while Windows Installer's cache + still serves a repair. +5. The zip path: the check, the notice, the page opening, and nothing else + happening. + +## Size + +A module of a few hundred lines — the requests, the hash, the version +comparison, the launch — with the comparison and the `SHA256SUMS.txt` parse +under unit tests, one menu entry and one tooltip state in the tray, and a +`setup` line for each step. Verifying it needs a real release pair: it is +built against `v0.1.0` and proved by installing whatever `v0.1.1` becomes. diff --git a/docs/design/README.md b/docs/design/README.md index 406a3f5..83d6a51 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -19,7 +19,7 @@ session rather than when the code compiles. Each has its own page. | 5 | [A Windows program with no window](05-windowless-watcher.md) | done | | 6 | [Notification area icon](06-notification-icon.md) | done | | 7 | [Icon, tooltip and menu as one state](07-tray-state.md) | done | -| 8 | [Distribution](08-distribution.md) | in progress | +| 8 | [Distribution](08-distribution.md) | done | | 9 | [Robustness](09-robustness.md) | partly done | | 10 | [Configuration window](10-configuration-window.md) | proposed | | 11 | [Documentation for the people who use it](11-user-documentation.md) | done | @@ -55,7 +55,7 @@ Recorded so they stop coming back. - **No allow-list of game executables, and no heuristics that guess at what a game is.** Detection is Windows' verdict, read from Windows. - **No telemetry, no network access.** A future update check must be manual or - opt-in, or this stops being true — see [Lot 8](08-distribution.md). + opt-in, or this stops being true — see [Lot 13](13-updating.md). - **No elevation.** The watcher runs as the user, on purpose. Programs that need administrator rights are reached through a scheduled task, never by elevating the watcher — see [Lot 1](01-console-watcher.md). diff --git a/docs/getting-started.md b/docs/getting-started.md index 2d835c4..bfe66f8 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -170,8 +170,8 @@ The log keeps the history of every session. It lives in It starts earlier than the first session: the lines marked `setup` say when the configuration was written and the logon task registered, whether you -typed the command or the installer did it. `init`, `install-task` and -`uninstall-task` print those same lines as they run. +typed the command or the installer did it. `init`, `install-task`, +`uninstall-task` and `stop` print those same lines as they run. ## Using the game's name in your command @@ -260,7 +260,9 @@ gamemode-executor uninstall-task ``` The first stops the one running now; the second removes the logon task, so -nothing starts at the next logon. +nothing starts at the next logon. To take the program off the machine, see +[Removing it](how-it-works.md#removing-it): *Programs and Features* for the +installer's copy, or `purge` for every trace whichever way it came. ## Everything else diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 062b3f0..c78fadc 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -224,7 +224,7 @@ added. | What | Where | | --- | --- | -| The program | wherever you put it. `%LOCALAPPDATA%\Programs\GameModeExecutor` is the Windows convention for a per-user install, and it stays writable, which `C:\Program Files` would not | +| The program | `%LOCALAPPDATA%\Programs\GameModeExecutor` from the installer — the Windows convention for a per-user install, and it stays writable, which `C:\Program Files` would not. From the zip, wherever you put it | | Configuration | `config.toml` next to the executable if there is one, otherwise `%APPDATA%\GameModeExecutor\config.toml` | | Log | `%LOCALAPPDATA%\GameModeExecutor\logs\gamemode-executor.log`, one file, local timestamps | | Scheduled tasks | a `GameModeExecutor` folder in Task Scheduler, holding `Watcher` and anything a recipe added | diff --git a/docs/recipes/fancontrol-fan-profiles/README.md b/docs/recipes/fancontrol-fan-profiles/README.md index fc8f3b5..6cdc29d 100644 --- a/docs/recipes/fancontrol-fan-profiles/README.md +++ b/docs/recipes/fancontrol-fan-profiles/README.md @@ -307,12 +307,14 @@ FanControl records the active configuration in a file called `CACHE`, in its ConvertFrom-Json).CurrentConfigFileName ``` -When both work: +When both work, restart the watcher so it reads the file: ```bash +gamemode-executor stop gamemode-executor install-task ``` +From the zip, the second line also registers the logon task, the first time. Done. Play a game and the fans follow. ## Worth knowing diff --git a/docs/recipes/windows-power-plan/README.md b/docs/recipes/windows-power-plan/README.md index c0a5124..1a6b322 100644 --- a/docs/recipes/windows-power-plan/README.md +++ b/docs/recipes/windows-power-plan/README.md @@ -81,12 +81,15 @@ powercfg /getactivescheme gamemode-executor trigger stop ``` -When both work: +When both work, restart the watcher so it reads the file: ```bash +gamemode-executor stop gamemode-executor install-task ``` +From the zip, the second line also registers the logon task, the first time. + ## Worth knowing **On a laptop, Windows may override you.** Some machines switch plans by diff --git a/scripts/msi.ps1 b/scripts/msi.ps1 index 1a61dff..adf2047 100644 --- a/scripts/msi.ps1 +++ b/scripts/msi.ps1 @@ -3,11 +3,15 @@ # Per-user, no elevation, no UI: the two executables and the license go to # %LOCALAPPDATA%\Programs\GameModeExecutor. The user's configuration, log, # marker and scheduled tasks are not components, so no repair, upgrade or -# uninstall reaches them. Two custom actions, both the program's own -# commands and both idempotent, finish an install or upgrade: `init`, which -# writes a 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. +# uninstall reaches them. Four custom actions, all the program's own +# commands and all idempotent, run through the windowless executable: +# `stop` before an uninstall or upgrade touches the files, so the Restart +# Manager never has to ask; `init`, which writes a 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 -- to finish an install or upgrade; and +# `uninstall-task` on an uninstall, since the task is the package's to take +# down. # # Written with nothing but Windows Installer's own COM automation and # makecab, so a stock runner can build it -- docs/design/08-distribution.md diff --git a/src/bin/gamemode-executorw.rs b/src/bin/gamemode-executorw.rs index c49bb99..e0dda5e 100644 --- a/src/bin/gamemode-executorw.rs +++ b/src/bin/gamemode-executorw.rs @@ -12,11 +12,12 @@ //! nothing this binary prints goes anywhere. That is why people type the //! console one and why the documentation only ever names it. This one is for //! the two callers that have no console to give: the logon task, which runs -//! the watcher, and the installer, which runs `init` and `install-task` -- -//! Windows Installer starts an executable action without hiding its console, -//! and the console binary flashed a window twice at the end of every install, -//! seen on 2026-09-18. The commands log what they did, so nothing is lost by -//! not printing it; the exit code is what the caller records. +//! the watcher, and the installer, which runs `stop`, `init`, `install-task` +//! and `uninstall-task` -- Windows Installer starts an executable action +//! without hiding its console, and the console binary flashed a window twice +//! at the end of every install, seen on 2026-09-18. The commands log what +//! they did, so nothing is lost by not printing it; the exit code is what the +//! caller records. use clap::Parser; diff --git a/src/task.rs b/src/task.rs index 2638b16..88d403f 100644 --- a/src/task.rs +++ b/src/task.rs @@ -20,8 +20,9 @@ pub const TASK_FOLDER: &str = "GameModeExecutor"; /// the folder's name, so it reads as `GameModeExecutor \ Watcher` in the tree. pub const TASK_NAME: &str = "GameModeExecutor\\Watcher"; -/// The windowless twin this task is meant to run. Sits beside the console -/// binary, which is the one the user types and therefore the one running now. +/// The windowless twin this task is meant to run. Sits beside whichever +/// binary is running this command: the console one a person typed, or the +/// twin itself when the installer runs it. const WATCHER_EXE: &str = "gamemode-executorw.exe"; /// What `install` did about the task. @@ -344,10 +345,11 @@ mod tests { assert!(xml.contains(&format!(r"\{TASK_NAME}")), "{xml}"); } - /// The task must run the windowless binary with no subcommand. `run` and - /// `--hidden` belong to the console binary, and passing either here would - /// make the task fail at every logon with an argument error nobody sees, - /// because there is no console to see it in. + /// The task runs the windowless binary with the configuration and nothing + /// else. `run` is the default command and `--hidden` is a leftover kept + /// for tasks registered before the twin existed; a definition that + /// names neither keeps working whatever later versions do with them, + /// and a task that fails at logon fails where nobody sees it. #[test] fn the_task_passes_only_the_configuration() { let xml = definition(