Skip to content

Self Updater Guide

Mikhail Deynekin edited this page Oct 16, 2026 · 4 revisions

Self-Updater Guide

TrayIconGuard updates itself from a JSON feed hosted in its own Git repository: the program fetches the feed over HTTPS, compares versions, and — only after the user confirms — downloads the new executable, verifies its SHA-256 against the feed's pin, replaces itself through a rename chain and relaunches. This page documents the contract (Updater_SKILL.MD in the repository), the security properties, the failure matrix, an end-to-end test recipe, and one provenance caveat every release engineer should know about the 8.4.0 publication.


Contents


What the program does at update time

  1. Resolve the feed URL: settings.updates.feedUrl if set, else the built-in default https://raw.githubusercontent.com/paulmann/windows-show-all-tray-icons/main/update/stable.json. The allow-list accepts https:// URLs plus plain HTTP only for 127.0.0.1 / localhost (local QA servers).
  2. GET the feed with WinHTTP (user agent TrayIconGuard/<version>; resolve/connect 8 s, send 10 s, receive 60 s timeouts).
  3. Parse and validate: version, url and a 64-hex sha256 are mandatory; missing or malformed fields fail the check with feed missing version/url/sha256(64 hex).
  4. Compare version with the running version (4-component numeric compare, v prefix tolerated). Not newer → “up to date”.
  5. On confirmation (GUI) download url to %TEMP%\TrayIconGuard-update.exe.
  6. SHA-256 the download with BCrypt; mismatch → delete the file and report Update rejected: checksum mismatch.
  7. Apply: rename the running exe to TrayIconGuard.exe.old (renaming a running image is permitted), move the new file into place, schedule .old for deletion at reboot (MOVEFILE_DELAY_UNTIL_REBOOT), relaunch and exit. A failed move rolls the .old file back into place.
  8. The opt-in automatic check (About toggle) runs at most once per 24 h, shows a balloon only, and never downloads or applies anything by itself.

The legacy path — if the feed cannot be fetched or parsed — probes the GitHub API releases/latest endpoint and compares tag names; it can tell you that something newer exists but cannot pin hashes, so applying from that path is not offered.

The feed file

update/stable.json at the repository root (served through raw.githubusercontent.com); update/beta.json for a beta channel, selected only via the feedUrl override:

{
  "schema": 1,
  "channel": "stable",
  "version": "8.6.3",
  "released": "2026-10-16",
  "minBuild": 15063,
  "url": "https://github.com/paulmann/windows-show-all-tray-icons/releases/download/v8.6.3/TrayIconGuard.exe",
  "sha256": "10ea493785629761be0430baff3288f474b47d3720201c2197a1b6fa5bc5fc03",
  "zipUrl": "https://github.com/paulmann/windows-show-all-tray-icons/releases/download/v8.6.3/TrayIconGuard-8.6.3-win-x64.zip",
  "zipSha256": "4fadcc253d259974728583202b5e0c5fd3b220375cc3e077b6734713e9028436",
  "notes": "One paragraph shown in About → update panel."
}

Rules from the contract: versions must strictly increase; sha256 must match the byte-identical served executable (compute it on the final stripped — and signed, if you sign — binary); binaries belong in release assets, not raw git URLs; published feeds are never rewritten, a new version is published instead.

Publication checklist

From Updater_SKILL.MD §3, annotated with what the 8.4.0 release actually did (see the caveat below):

  1. ./scripts/build.sh — gates: zero warnings + manifest well-formedness.
  2. Bump the version in one place — src/core/version.h — then mirror to resources/resources.rc, resources/app.manifest, src/core/installer.cpp (DisplayVersion) and scripts/package.sh (VER).
  3. ./scripts/package.sh → zip + SHA256SUMS.
  4. Write update/stable.json with the new hashes and notes.
  5. Commit, tag v<ver>, push; create the GitHub release with the three assets named exactly as the feed URLs.
  6. Verify: --check-update prints newer=yes before and newer=no after updating.
  7. Optional loopback E2E (below).

Security properties

  • HTTPS-only feeds and downloads; loopback HTTP solely for QA.
  • Mandatory SHA-256 pin; no apply without an exact match.
  • No silent auto-apply: a confirmation dialog always precedes download+apply in the GUI; the CLI verb --update-now is an explicit operator action.
  • No telemetry: the only network requests are the feed GET and the binary GET.
  • Rename-chain apply with rollback on move failure; old image deleted at reboot.

Important

Understand the trust boundary: the SHA-256 pin proves the download matches the feed, and TLS proves both came from the feed's host — it does not authenticate the publisher beyond that, because there is no code signature and no signed manifest. Anyone who can publish to the feed URL can publish a matching hash for their own binary. For fleets, the documented answer is code-signing the executable and re-hashing the feed afterwards.

Failure matrix

Symptom Cause Action
update check failed: HTTP 404 feed path/branch wrong check update/stable.json exists on the default branch
sha256 mismatch (feed=… file=…) exe re-uploaded or re-signed after the feed was written republish the feed with the hash of the served bytes
only https (or loopback http for tests) feeds are allowed non-loopback http feed URL use https
cannot rename running exe (…) AV/EDR holding the file retry; add an exclusion for the folder
update applied but old UI still running relaunch blocked start manually; .old disappears at reboot
bad feed JSON: … malformed feed fix JSON; the client also falls back to the releases probe for the check

End-to-end test on a loopback feed

# serve a folder containing stable.json (loopback url) + the candidate exe
python3 -m http.server 8000
# point one installation at it (settings.json):  "updates": { "feedUrl": "http://127.0.0.1:8000/stable.json" }
TrayIconGuard.exe --update-now     # downloads, verifies, applies, relaunches
TrayIconGuard.exe --check-update   # now prints newer=no

The repository's v8.3.0 development log records this exact loop verified under Wine (download → sha → rename → .old → relaunch, exit 0).

Provenance caveat: the 8.4.0 publication (resolved)

The published v8.4.0 assets do not match what the tagged source would produce:

  • the v8.4.0 tag changed only update/stable.json (plus script execute bits) relative to v8.3.0; src/core/version.h at the tag still reads 8.3.0;
  • the released executable's version resource says 8.4.0.0 and its --version prints 8.4.0, so it was built from a tree whose version bump was never pushed;
  • its version resource copyright still says MIT although the repository license changed to PolyForm Noncommercial at 8.3.0;
  • the feed's release notes advertise features (all-users install scope, About License/Wiki buttons) that exist neither in the published source nor in the released binary's strings.

Practical consequence for operators: updating an 8.3.0 installation to the feed's 8.4.0 is safe and terminates (the delivered binary reports 8.4.0, so the next check says newer=no); but a build produced from the public tag reports 8.3.0 and would keep offering the same update. Release engineers should treat checklist step 2 as mandatory and commit the bump with the tag. Full drift table: Changelog and Versioning.

Resolution (2026-10-16). The gap was closed: the missing 8.4.0 sources were restored and published as v8.4.1, and 8.6.0 and everything after it followed from the repository itself. Since 8.6.0 the CI release job uploads the assets and runs scripts/make_feed.py, which pins update/stable.json from the bytes the release actually serves — so the feed can no longer disagree with the assets. The obsolete 8.3.0/8.4.0/8.4.1/8.6.0/8.6.1 releases and tags were removed on 2026-10-16; v8.6.3 is the only 8.x release, and the legacy v6.2 / v7.0.3 releases remain as history for the script editions (Legacy Script Editions).


See also

Clone this wiki locally