Windows port, part 3a: desktop app build, Task Scheduler task and installer (W3 part 1) #32

Manually merged
SkyfaR merged 53 commits from windows-w3 into main 2026-10-05 21:57:58 +02:00
Owner

Windows port, phase W3, part 1: the desktop app builds and starts on Windows, the background re-compression runs from the Task Scheduler, and there is a per-user installer and a portable zip. It builds on #31 (W2). See plan sections 1.3-1.6, 5, 6 and 7.

Desktop app on Windows

  • ogc-gui.exe is built for x86_64-pc-windows-gnullvm against MSYS2 CLANG64 (UCRT; never msvcrt).
  • Sysroot: packaging/windows/msys2.lock pins 82 packages by version and SHA-256 (gtk4 4.24.1, libadwaita 1.10.0, pango 1.58.2).
    • msys2-lock.py writes the lock only from a clang64.db whose signature gpgv accepts against MSYS2's packaging keys, which are pinned by fingerprint in packaging/windows/msys2-keyring.asc and msys2-keys.txt.
    • fetch-msys2.sh checks every package's hash. It takes a lock and unpacks atomically, so parallel runs are safe.
  • Bundle (build-gui.sh, bundle-gui.py), 71 MiB:
    • ogc.exe, ogc-gui.exe and ogc-background.exe with their 63 DLLs;
    • compiled schemas, a trimmed Adwaita theme, the SVG pixbuf loader, fontconfig and the app's fonts.
  • Import check: scripts/check-bundle-windows.sh walks every import. It fails on any DLL that is not in the bundle or Windows' own, on msvcrt, and on any API set not listed by exact name. check.sh and dist.sh apply the same exact list to ogc.exe and ogc-background.exe.
  • Hidden on Windows, by view-model flags (gui::system::FEATURES): migration pages, helper detection, dedupe, the No_COW and snapshot hints, and the Proton-prefix switch. The timer settings are wired to the Task Scheduler but stay hidden until the next round, because their rows are systemd's.
  • Under Wine: the app starts with GSK_RENDERER=cairo and stays up with no critical errors (gui-smoke.sh). Its Windows parts are tested under Wine (gui-test.sh). The Linux GUI is unchanged.

Background re-compression (scheduler, ogc-background.exe)

  • The task: a per-user Task Scheduler task \OpenGameCompressor Auto-<SID> in the library root.
    • Never elevated: LeastPrivilege, InteractiveToken.
    • Runs hourly with a random delay, and after logon.
    • Does not start on battery and stops when the PC goes on battery.
    • PT7H time limit as a backstop.
    • The XML is generated in code and has a snapshot test. schtasks is called through a replaceable runner.
    • A missing task is told apart from other errors by schtasks' own not-found text; the listing is only a fallback.
  • Commands: ogc timer enable/disable/status work on Windows. TimerStatus is shared with systemd; Linux output is unchanged.
  • ogc-background.exe (GUI subsystem, no console window):
    • Before it starts ogc.exe auto, it checks the lock, battery, running games and SHQueryUserNotificationState (fullscreen, busy, presentation).
    • It starts ogc.exe rather than compressing itself, because only a console program is told about logoff and shutdown.
    • It re-checks every 5 s with one reusable game look.
    • It asks the run to stop at 5 h 45 min, counted from the run's own start.
    • It watches a run it began earlier, recorded in background-run.json, and never a hand-started ogc auto.
    • A run the Task Scheduler ended (0x41306) is not reported as failed.

Installer and portable zip (packaging/windows/)

  • NSIS (installer.sh, built with makensis in the container):
    • Per-user, RequestExecutionLevel user, so no UAC prompt. Installs to %LOCALAPPDATA%\Programs\OpenGameCompressor.
    • Start-menu shortcut with AppUserModelID LevelXStudios.OpenGameCompressor, set through the shell's IPropertyStore with NSIS's own System plugin.
    • Uninstall entry under HKCU.
    • An optional, default-off "Hintergrund-Aufgabe".
    • Upgrade in place; refuses while a program runs.
    • The uninstaller removes exactly what was installed. It removes the user's data only after asking (default no) or with /REMOVEDATA.
    • An install folder that overlaps ogc's data or the profile is refused, comparing long names, also for folders not made yet.
    • ogc.exe is started without cmd.exe, and its UTF-8 output is shown correctly.
    • 13 languages; NSIS's German is reworded impersonally.
  • dist.sh builds OpenGameCompressor-<ver>-x64-portable.zip (28 MiB) and -setup.exe (18 MiB) from the bundle, plus the test package. The test package's run-tests.ps1 gains part 3 for the next real-PC run: install, read the shortcut's AppUserModelID, uninstall.
  • in-container.sh now passes the scripts' OGC_* settings into the container.

Tests

  • Linux: fmt, both clippy runs, OGC_TEST_REQUIRE_BTRFS=1 cargo test, and the Broadway suite.
  • Windows (container): check.sh and wine-test.sh (lib 362, ogc 77, ogc-background 10, plus the timer smoke with a fake schtasks).
  • App: check-gui.sh (including the Python tests for the lock, its signature check and the bundle check), build-gui.sh, gui-smoke.sh, gui-test.sh.
  • Installer: packaging/windows/test/wine-test.sh covers install, upgrade, running programs, failing task setup, uninstall and data removal.
  • Packages: dist.sh with the app installs, upgrades and uninstalls the real bundle under Wine.
  • Each piece had a review with adversarial verification, followed by two fix rounds.

Not verified yet (needs a real Windows PC)

  • H23: whether real schtasks accepts the task and its name for a standard user, and what it prints in other languages.
  • H19: what SHQueryUserNotificationState reports during a game.
  • Logoff during a run.
  • The shortcut's AppUserModelID (Wine has no property store).
  • Pango with the app's own fonts: under Wine, Chakra Petch loses letters.
  • Dark mode, and the GL and Vulkan renderers.

Known gaps for the next rounds

  • AccessKit (release blocker per plan §1.4): MSYS2 builds GTK 4.24.1 without AccessKit, so Narrator reads nothing. A GTK of our own with -Daccesskit=enabled is needed before release.
  • W3 part 2:
    • compress and decompress from the app on Windows (today ogc compress);
    • the algorithm combo row;
    • the timer settings;
    • Windows card states;
    • single instance;
    • toasts;
    • the "BTRFS · ZSTD:3" sidebar label.
  • The Windows GUI build and the installer are not in CI yet.
Windows port, phase W3, part 1: the desktop app builds and starts on Windows, the background re-compression runs from the Task Scheduler, and there is a per-user installer and a portable zip. It builds on #31 (W2). See plan sections 1.3-1.6, 5, 6 and 7. ## Desktop app on Windows - `ogc-gui.exe` is built for `x86_64-pc-windows-gnullvm` against MSYS2 CLANG64 (UCRT; never msvcrt). - **Sysroot:** `packaging/windows/msys2.lock` pins 82 packages by version and SHA-256 (gtk4 4.24.1, libadwaita 1.10.0, pango 1.58.2). - `msys2-lock.py` writes the lock only from a `clang64.db` whose signature `gpgv` accepts against MSYS2's packaging keys, which are pinned by fingerprint in `packaging/windows/msys2-keyring.asc` and `msys2-keys.txt`. - `fetch-msys2.sh` checks every package's hash. It takes a lock and unpacks atomically, so parallel runs are safe. - **Bundle** (`build-gui.sh`, `bundle-gui.py`), 71 MiB: - `ogc.exe`, `ogc-gui.exe` and `ogc-background.exe` with their 63 DLLs; - compiled schemas, a trimmed Adwaita theme, the SVG pixbuf loader, fontconfig and the app's fonts. - **Import check:** `scripts/check-bundle-windows.sh` walks every import. It fails on any DLL that is not in the bundle or Windows' own, on msvcrt, and on any API set not listed by exact name. `check.sh` and `dist.sh` apply the same exact list to `ogc.exe` and `ogc-background.exe`. - **Hidden on Windows**, by view-model flags (`gui::system::FEATURES`): migration pages, helper detection, dedupe, the No_COW and snapshot hints, and the Proton-prefix switch. The timer settings are wired to the Task Scheduler but stay hidden until the next round, because their rows are systemd's. - **Under Wine:** the app starts with `GSK_RENDERER=cairo` and stays up with no critical errors (`gui-smoke.sh`). Its Windows parts are tested under Wine (`gui-test.sh`). The Linux GUI is unchanged. ## Background re-compression (`scheduler`, `ogc-background.exe`) - **The task:** a per-user Task Scheduler task `\OpenGameCompressor Auto-<SID>` in the library root. - Never elevated: `LeastPrivilege`, `InteractiveToken`. - Runs hourly with a random delay, and after logon. - Does not start on battery and stops when the PC goes on battery. - PT7H time limit as a backstop. - The XML is generated in code and has a snapshot test. `schtasks` is called through a replaceable runner. - A missing task is told apart from other errors by schtasks' own not-found text; the listing is only a fallback. - **Commands:** `ogc timer enable/disable/status` work on Windows. `TimerStatus` is shared with systemd; Linux output is unchanged. - **`ogc-background.exe`** (GUI subsystem, no console window): - Before it starts `ogc.exe auto`, it checks the lock, battery, running games and `SHQueryUserNotificationState` (fullscreen, busy, presentation). - It starts `ogc.exe` rather than compressing itself, because only a console program is told about logoff and shutdown. - It re-checks every 5 s with one reusable game look. - It asks the run to stop at 5 h 45 min, counted from the run's own start. - It watches a run it began earlier, recorded in `background-run.json`, and never a hand-started `ogc auto`. - A run the Task Scheduler ended (0x41306) is not reported as failed. ## Installer and portable zip (`packaging/windows/`) - **NSIS** (`installer.sh`, built with makensis in the container): - Per-user, `RequestExecutionLevel user`, so no UAC prompt. Installs to `%LOCALAPPDATA%\Programs\OpenGameCompressor`. - Start-menu shortcut with AppUserModelID `LevelXStudios.OpenGameCompressor`, set through the shell's `IPropertyStore` with NSIS's own System plugin. - Uninstall entry under HKCU. - An optional, default-off "Hintergrund-Aufgabe". - Upgrade in place; refuses while a program runs. - The uninstaller removes exactly what was installed. It removes the user's data only after asking (default no) or with `/REMOVEDATA`. - An install folder that overlaps ogc's data or the profile is refused, comparing long names, also for folders not made yet. - `ogc.exe` is started without `cmd.exe`, and its UTF-8 output is shown correctly. - 13 languages; NSIS's German is reworded impersonally. - **`dist.sh`** builds `OpenGameCompressor-<ver>-x64-portable.zip` (28 MiB) and `-setup.exe` (18 MiB) from the bundle, plus the test package. The test package's `run-tests.ps1` gains part 3 for the next real-PC run: install, read the shortcut's AppUserModelID, uninstall. - `in-container.sh` now passes the scripts' `OGC_*` settings into the container. ## Tests - **Linux:** fmt, both clippy runs, `OGC_TEST_REQUIRE_BTRFS=1 cargo test`, and the Broadway suite. - **Windows (container):** `check.sh` and `wine-test.sh` (lib 362, ogc 77, ogc-background 10, plus the timer smoke with a fake schtasks). - **App:** `check-gui.sh` (including the Python tests for the lock, its signature check and the bundle check), `build-gui.sh`, `gui-smoke.sh`, `gui-test.sh`. - **Installer:** `packaging/windows/test/wine-test.sh` covers install, upgrade, running programs, failing task setup, uninstall and data removal. - **Packages:** `dist.sh` with the app installs, upgrades and uninstalls the real bundle under Wine. - Each piece had a review with adversarial verification, followed by two fix rounds. ## Not verified yet (needs a real Windows PC) - **H23:** whether real `schtasks` accepts the task and its name for a standard user, and what it prints in other languages. - **H19:** what `SHQueryUserNotificationState` reports during a game. - Logoff during a run. - The shortcut's AppUserModelID (Wine has no property store). - Pango with the app's own fonts: under Wine, Chakra Petch loses letters. - Dark mode, and the GL and Vulkan renderers. ## Known gaps for the next rounds - **AccessKit (release blocker per plan §1.4):** MSYS2 builds GTK 4.24.1 without AccessKit, so Narrator reads nothing. A GTK of our own with `-Daccesskit=enabled` is needed before release. - **W3 part 2:** - compress and decompress from the app on Windows (today `ogc compress`); - the algorithm combo row; - the timer settings; - Windows card states; - single instance; - toasts; - the "BTRFS · ZSTD:3" sidebar label. - The Windows GUI build and the installer are not in CI yet.
TimerStatus and Trouble move from systemd/status.rs to auto, next to
TimerConfig, so that the Task Scheduler's task on Windows fills the same
fields; systemd re-exports them under their old names. new_level is a
Codec now, a zstd level on Linux as before: the unit is parsed alike,
and the CLI, the settings and the debug text show the same as before.
ogc timer enable, disable and status set up, remove and read the
per-user task \OpenGameCompressor\Auto through schtasks (plan section 5):
InteractiveToken and LeastPrivilege, so no administrator rights and
nothing elevated; hourly with a random delay of 10 minutes and 15 minutes
after signing in; not on battery, never twice at once, priority 7, at
most 6 hours. It starts ogc-background.exe auto next to ogc.exe.

scheduler writes the task's XML (checked against a snapshot), reads it
and schtasks' CSV back into the shared TimerStatus, and runs schtasks
through a Runner that the tests replace with a scheduler in memory. It is
built and tested on every system; the real schtasks (by its full path,
without a console window) and the user's SID are Windows'. ogc timer is
visible on Windows now, with help texts of its own, in all languages.
A program of the Windows subsystem, so that no console window opens every
hour. It checks the run guards in order: the compression lock is free,
the computer is not on battery, no game runs, and
SHQueryUserNotificationState reports neither exclusive full screen nor a
presentation nor a busy user. Then it starts ogc.exe with its arguments,
without a console window and with the output in background.log, looks
every 5 seconds for a game or full screen, and asks the run to stop after
the files in progress through run.json's cancel, as the app does. The
work stays ogc.exe's, which Windows tells of logoff and shutdown.

The guards and the watch are tested with made-up answers. On Linux the
binary says that it is for Windows and exits with 2. check.sh reviews
ogc-background.exe's imports against a list of its own (shell32.dll
added; ogc.exe's is unchanged) and checks its subsystem.
Wine's schtasks.exe cannot query a task, so a made-up one replaces it in
the test's prefix only: ogc timer enable, status (also of a failed run)
and disable go through it, and the task's XML it got is checked. Then
ogc-background.exe runs without and with auto on the made-up Steam
installation; on a host on battery the guard keeps the run from
starting, and nothing is noted.
The desktop app reads it on every system, and Windows has no systemd.
The views ask system::FEATURES whether this system has the migration,
sharing identical files and the timer; the code behind them is reached
through crate::system, which is the library's modules on Linux and
stand-ins of the same API on Windows. Large modules are split so that
each stays below 500 lines. Linux keeps every part.
packaging/windows/msys2.lock pins the dependency closure of GTK 4,
libadwaita and Pango by version and SHA-256; fetch-msys2.sh downloads,
checks and unpacks it once per lock, and sysroot-env.sh points
pkg-config at it. check-gui.sh runs clippy for Windows with every
feature.
build-gui.sh builds ogc.exe and ogc-gui.exe against the sysroot,
bundle-gui.py gathers the DLL closure, schemas, a trimmed Adwaita icon
theme, the pixbuf loader and the fonts, and check-bundle-windows.sh
fails on a DLL neither bundled nor Windows' own, and on msvcrt.dll.
installer.sh builds OpenGameCompressor-<version>-x64-setup.exe from a
bundle folder with makensis. It installs without administrator rights
into %LOCALAPPDATA%\Programs\OpenGameCompressor, creates a Start-menu
shortcut with the AppUserModelID LevelXStudios.OpenGameCompressor, an
uninstall entry under HKCU and, optionally, the background task. An
upgrade removes the files the earlier version listed; the uninstaller
removes the task, the shortcut, the files and the entry, and asks
whether to remove the user's data.
# Conflicts:
#	src/auto.rs
#	src/auto/timer_status.rs
#	src/gui/settings/timer.rs
system::timer is opengamecompressor::scheduler there, with the API of
systemd's timer. The settings still leave the timer out on Windows, as
their rows are systemd's (a zstd level for new games, prefixes,
notifications), so the app neither reads nor changes the task until the
algorithm's row replaces the level.
The Task Scheduler's task starts it beside ogc.exe. The bundle check
also makes sure that it and ogc-gui.exe open no console window.
Without the app, the check builds an installer of its own with a
stand-in for ogc-gui.exe, so that the real ogc timer enable and disable
run through the installer before the GUI bundle exists.
Right after the fork the child has the name of the test's thread, so a
Running taken then did not match it once it ran the test program, and
runstatus' stale-status test failed now and then.
Their Windows stand-ins are chosen with cfg_attr paths, and the
sidebar's pages of the system move to shortcuts.rs, beside the
accelerators that follow them.
# Conflicts:
#	docs/ARCHITECTURE.md
#	scripts/windows/Containerfile
Windows' engine compresses no Proton prefixes, yet the compression group
still showed the switch. system::Features gets a prefixes flag (Linux
true, Windows false), and the settings add the row only when it is set,
as they already do for the dedupe rows. A new test checks that the
prefix, dedupe and auto-recompress switches are shown exactly where
FEATURES says; gui-test.sh runs it under Wine too.
check-bundle-windows.sh and bundle-gui.py let every api-ms-win-* and
ext-ms-win-* import through, so a newer API-set contract than Windows
10 22H2 has, or an optional extension set, would have passed the check
and failed to load there. system-dlls.txt now lists the sets the bundle
imports by name and version: the UCRT's fifteen api-ms-win-crt-*-l1-1-0
and api-ms-win-core-synch-l1-2-0. Any other set fails like an unknown
DLL until it has been checked and added.

bundle.tests.py checks the list and runs check-bundle-windows.sh on a
made-up bundle whose programs llvm-mingw builds with the imports each
test names; check-gui.sh runs it.
Task Scheduler names are the computer's: with the one name
\OpenGameCompressor\Auto a second account's 'ogc timer enable' was
refused, or, elevated, took the first account's task over. The task is
now \OpenGameCompressor Auto-<SID>, as per-user updaters name theirs,
and lies in the library's root, since making a folder of its own may
need administrator rights (plan H23, not measured yet).

A failed /Query or /Delete was always 'no task'. schtasks tells a
missing task only in the user's language, so the list of the tasks
(/Query /FO CSV /NH) now decides; where it names the task or cannot be
listed, what schtasks said is the error. The made-up schtasks.exe of
the Wine smoke test keeps the task by its name, lists it next to
another account's and refuses a task in a folder.
When the Task Scheduler ends a run (unplugged, at its time limit or by
hand) it keeps SCHED_S_TASK_TERMINATED (0x41306) as the last result,
which 'ogc timer status' told as a failed run until a later run on AC
power finished. Linux reports an unplugged run as such, not as a
failure.
pause::running_game builds a new Lookout on every call, which
canonicalizes every game folder again and searches the processes
again while the game it found still runs. GameLook keeps one Lookout,
as GameWatch does, for ogc-background.exe's look every 5 seconds.
The Task Scheduler's time limit, like a manual End, ends only
ogc-background.exe; ogc.exe went on without a bound and without the
full-screen guard, and every later start only noted that it was busy.
ogc-background.exe now asks the run to stop after 5 h 45 min itself,
with ExecutionTimeLimit PT7H as a backstop, and a start that finds the
lock held by a live run of the timer watches that run with the same
guards and limit until it ends.

The guards' 5 s look keeps one GameLook for the whole start instead of
building a new Lookout every time.
fetch-msys2.sh assumed a single writer: two first runs wiped and
interleaved each other's unpack into one shared .partial folder, shared
the download's .part file, and every run deleted all other sysroots,
including one a concurrent build was compiling against or one of
another lock. A run could even mark a sysroot complete that missed
packages.

Now a run takes flock on <cache>/.lock after the fast path and looks
for the complete sysroot again, unpacks into a mktemp folder of its own
that is renamed into place once complete, downloads to a per-process
.part file, and removes only what a stopped run left. Sysroots of other
locks are never deleted. OGC_MSYS2_LOCK lets msys2.tests.py fetch
made-up packages from a file:// mirror: four runs at once unpack once,
a second run needs no mirror, other sysroots stay, a wrong hash leaves
nothing behind. check-gui.sh runs the tests.
msys2-lock.py copied each package's SHA-256 from whatever clang64.db it
was served or given, so trust rested on TLS at the moment the lock was
written. It now reads the database only after gpgv has found
clang64.db.sig good (GOODSIG, so not by an expired or revoked key) and
made by a key that packaging/windows/msys2-keys.txt pins by its primary
fingerprint. The keys are msys2.gpg of MSYS2's msys2-keyring package
1~20260814-1, kept verbatim as msys2-keyring.asc and byte for byte the
file in github.com/msys2/MSYS2-keyring; the pins are its keys less the
four msys2-revoked lists, and msys2-keys.txt says how to update them.
--db now needs <db>.sig beside it.

The lock's header names the database's mirror, SHA-256, signing key and
date. The lock is written anew from today's database, which is the one
the packages were pinned from (same SHA-256): only the header changes.

msys2.tests.py signs made-up databases with a key made for the test in
a GnuPG home of its own: a pinned signature is taken, a changed
database, an unpinned key and a missing signature are refused; it also
checks that every pinned key is in the keyring. The image gets gpgv,
which trixie no longer installs with apt.
ogc writes UTF-8 to anything that is not a console, but the Unicode
nsExec decodes a program's output in the ANSI code page. So the dialog
that explains why the background task could not be set up or removed
showed a German schtasks error or a profile folder like C:\Users\Jürgen
garbled.

run.nsh's RunProgram runs the program through cmd.exe with stdout and
stderr redirected into a file in $PLUGINSDIR and decodes the file with
MultiByteToWideChar(CP_UTF8). The program's path and the file's reach
cmd.exe through environment variables, so that "%" or "&" in a folder
name stays literal.

wine-test.sh runs RunProgram through test/run-test.nsi against a fake
ogc that writes Cyrillic, Japanese and German quotes as raw UTF-8 from a
folder named "a b&c %OS%", and checks the text and the exit code; the
failing background task in the install test now fails with that text.
Without _?=, an NSIS uninstaller copies itself to %TEMP%, starts the
copy and ends at once with 0, so the tests' checks of its exit code
passed even when the uninstall failed. The tests now run it with
"_?=<install folder>": it works in place, its exit code is the
uninstall's own, and it cannot remove itself, so the tests check that
only uninstall.exe is left and remove it themselves.
The folder page's Browse appends "OpenGameCompressor" to the folder
picked, so picking AppData\Local gave exactly the folder of the app's
state, data, cache and run files, %LOCALAPPDATA%\OpenGameCompressor,
and /D could name it or %APPDATA%\OpenGameCompressor directly. An
uninstall that removes the data (a "yes" or /REMOVEDATA) then deleted
the install folder with everything the user had put there.

folders.nsh compares full paths (case, "..", "/" and an existing
folder's 8.3 name do not matter). The installer refuses a folder that
is, holds or lies inside one of the two data folders, or that is or
holds the profile folder: the folder page shows why and stays, and /D
or a stored folder ends the setup in .onInit with the message (exit
code 2 when silent) before anything is installed. The uninstaller, as
a second guard, does not remove a data folder that is its own folder or
holds it. The message is in all 13 languages.

wine-test.sh checks the refusal for each kind of overlap and another
spelling, that a folder which only starts like a data folder is
accepted (and the plain uninstall without _?= removes it), and that an
uninstaller started in or below a data folder keeps that one.
wine-install-check.sh let bin/ogc-gui.exe differ in every case. With a
stand-in installer the unpacked bundle has no ogc-gui.exe, so the
exception only took effect when dist.sh built a real installer, exactly
where the installed app must equal the bundle's. The check now compares
every file with the folder the setup was built from: the unpacked
bundle, or the stand-in's bundle with its fake ogc-gui.exe.
Wine's shell link keeps no property store, so wine-test.sh cannot see
the AppUserModelID that shortcut.nsh sets on the Start-menu shortcut,
and nothing that runs on Windows covered the installer: the System
plugin's COM sequence was never checked anywhere.

run-tests.ps1 gets a part 3. It runs when the setup lies next to the
script, with -NurInstallation (part 3 alone) or with
OGC_TEST_REQUIRE_AUMID=1, which also makes a part 3 that cannot run
fail. After a question it installs with /S (no background task), reads
the shortcut with lnk-aumid.exe, which the test package now carries,
requires the target bin\ogc-gui.exe and the ID
LevelXStudios.OpenGameCompressor, then uninstalls with /S _?=<folder>
and requires exit codes 0 and nothing left. When OpenGameCompressor is
installed already, it leaves part 3 out rather than remove the tester's
install. ANLEITUNG.txt describes the step for the next run on a real PC;
run-tests.tests.ps1 covers it with fakes.

wine-test.sh now says that it does not check the ID, instead of
reporting the missing ID as expected.
PowerShell reads „ “ ” as double quotes, so in a "..." string they end
it: the missing-drive message came out as "Laufwerk  Q: nicht
gefunden", and the one for a game that is no App-ID lost its name the
same way. Both now use a single-quoted format string, as part 3's
messages do. The tests check both texts and that no double-quoted
string in the script holds a German quote.
ARCHITECTURE.md describes the refused install folders and the
uninstaller's guard, how the installer reads ogc's output, the
uninstall with _?=, run-tests.ps1's part 3 and lnk-aumid.exe in the
test package, and no longer says that the AppUserModelID is required on
Windows: nothing has checked it on a real PC yet.
When /Query /TN or /Delete failed, the task counted as missing only if
the full list of the Task Scheduler library did not name it. That list
fails on machines with one unreadable task elsewhere, so a user without
the task got an error from `ogc timer status` and every uninstall failed
to turn the timer off.

Compare what schtasks said with Windows' own texts for a missing file
and path in the user's language first, which a Runner now supplies, and
keep the list only as the last resort. Access denied stays an error.
The Wine smoke test's fake schtasks prints Windows' text and can fail
its listing, and checks that status and disable work without it.
Running already keeps the process's start time; age() gives the time
since then, or none where Windows did not tell the start.
A start of ogc-background.exe that found the lock held watched any
`ogc auto` that run.json told as the timer's, also one started by hand,
and could ask it to stop for full screen or after its time. Each start
now notes the process of the run it began in background-run.json, a
Windows-only file in the runtime folder, and a later start watches only
a run whose run.json names that process. run.json is unchanged.

The time limit of a watched run counted from when it was found, so a
run orphaned for hours went on far past it and the log's "almost six
hours" was wrong. It now counts from the run's own start, from when it
was found only where Windows does not tell the start.

Test the Windows side under Wine: picking the run, the note, watching
a run by its run.json and asking it to stop.
check.sh and dist.sh accepted any api-ms-win-crt-* and api-ms-win-core-*
import by its prefix, so a newer contract version that Windows 10 22H2
does not have, such as api-ms-win-core-synch-l1-3-0, would have passed
for ogc.exe, ogc-background.exe, the test program and lnk-aumid.exe.
Both now take API sets only by the exact names in system-dlls.txt, the
list the desktop bundle's check reads, through a shared imports.sh;
check.sh keeps its narrower rule of the C runtime's and the core's sets.
check.sh first runs the check on made-up programs: the listed version
passes, a newer one and a shell set fail.
run.nsh sent timer enable and disable through cmd.exe to catch ogc's
UTF-8 output in a file. cmd.exe reads "!" in the install path as its
own where delayed expansion is on in the registry, and refuses to run
at all under the policy that disables the command prompt, which left
the uninstaller's timer disable undone. RunProgram now starts ogc.exe
itself with CreateProcessW, an inheritable handle of the output file
as its stdout and stderr and no window, waits for it and reads its
exit code; a program that cannot start gives "error" and Windows'
message. The fake ogc names the program that started it, so the Wine
test sees that no cmd.exe comes between, also from a folder with "!".
GetLongPathNameW fails for a path that does not exist, and the data
folders do not exist before the app's first start. So /D with an 8.3
name, such as C:\Users\KIMCO~1\AppData\Local\OpenGameCompressor for a
profile "Kim & Co", kept its short spelling, did not compare equal to
the data folder and was accepted. NormalizeFolder now long-names the
deepest folder above it that exists and appends the names below it.
folder-test.nsi runs the comparison under Wine, which gives real 8.3
names, and the installer test refuses /D with such a name after moving
%LOCALAPPDATA% into a folder that has one.
Pass the scripts' OGC_* settings into the Windows container
All checks were successful
CI / Windows, command line (clippy with tests) (pull_request) Successful in 40s
CI / Windows, command line (tests under Wine) (pull_request) Successful in 1m36s
CI / Format, lint and test (pull_request) Successful in 5m31s
CI / Bundle programs, command line and helper (pull_request) Successful in 2m12s
CI / Bundle programs, desktop app (pull_request) Successful in 3m0s
CI / Arch package (pull_request) Successful in 3m3s
CI / Bundle for any distribution (pull_request) Successful in 41s
CI / Release (pull_request) Has been skipped
VM test / Migrations in a VM (pull_request) Successful in 18m11s
e467e43123
dist.sh's OGC_GUI_BUNDLE, the smoke test's OGC_SMOKE_SECONDS and the
other OGC_* settings never reached the scripts in the container, so
dist.sh built no installer even when given the app's bundle.
SkyfaR manually merged commit 05e924ed76 into main 2026-10-05 21:57:58 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
LevelXStudios/OpenGameCompressor!32
No description provided.