0.3.23: the installer runs ITS OWN stop step, with the install dir (PC 2, 0.3.23 take 1, 22:07 BST: PrepareToInstall preferred the installed 0.3.21 stop-igneum.ps1, which only asked the engine to quit; the window host lived on, Inno could not replace it, the host's restart ladder started the old engine 45 s later and every file stayed 0.3.21, while the crate's text tests passed). Igneum-Miner.iss: always ExtractTemporaryFile and run the payload's script with -Install "{app}" (and once more for an old admin dir), Log lines for the step and its exit (3 = a file stayed locked, CloseApplications is the fallback); InitializeSetup refuses an installer whose file name carries another version than it installs, and clears a stale install-running.flag with a line. stop-igneum.ps1: param Install (its own folder only the default, since the installer runs it from {tmp}), exit 3 when a file stays locked. tools/ci/installer-stop-check.sh (pre-push, self-test on tonight's shape, known-failed first on the tree): payload script, -Install, host by path before api/quit, unlock wait. Release rule 14 and the CI README row: an installer is tested by running it end to end on a Windows box over a running older app (installed versions, process set, first upload); a text test is a lint

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 21:31:34 +00:00
parent 67d53ca5a4
commit 526db5f78e
6 changed files with 108 additions and 10 deletions

View file

@ -18,3 +18,4 @@ Every cut of the Igneum Miner app and its node runs under these. The dated plan
11. **Kill by pid, never by name,** on the shared Mac; a merge worktree never checks out master.
12. **The Discord card only when every platform is live.** Live manifest changes beyond the binaries (a moved consensus floor) go out only on the project lead's explicit word, staged beside the release with their digest and a one-line diff.
13. **Ship on green:** no calendar waits; when the gates are green, publish and state the clock time (UK). Checkpoints are for slips, not for waiting.
14. **An installer or updater is tested by running it (main, 7 October 2026, after 0.3.23 take 1 on PC 2).** Every change to the install path (Igneum-Miner.iss, stop-igneum.ps1, ota-apply.ps1, the host's restart ladder) is tested END TO END on a Windows box over a RUNNING older app: by job on a fresh user account on PC 2, or on the Windows gate box when it exists. The test asserts three things after the run: the installed versions (every exe under the install folder answers the new version), the running process set (the new host and engine up, nothing of the old version alive), and the first upload afterwards (the read-back line `update-return: app <v> up after the update from <from>` reaches intake). A test that reads a script's text is a lint, not a test of the installer: 0.3.23 take 1's text tests passed while the installed 0.3.21 stop script ran instead of the payload's, the host never stopped and every file stayed 0.3.21. The install job passes `-Version` and the installer's name must carry it (InitializeSetup refuses otherwise); a stale `install-running.flag` is cleared by the installer itself at its start.

View file

@ -148,6 +148,31 @@ begin
if CurStep = ssDone then ClearMarker;
end;
// Two refusals before anything runs (0.3.23 take 1, PC 2, 22:07 BST): the installer's file name must carry ITS version
// (a 0.3.21 kit once shipped a 0.3.22.0 host; a job that passes -Version reads this name), and a marker an earlier
// installer left behind (older than 15 min: it died) is cleared with a line, so nothing holds on it.
function InitializeSetup: Boolean;
var
Name: String;
Age: TDateTime;
begin
Result := True;
Name := ExtractFileName(ExpandConstant('{srcexe}'));
if (Pos('Setup-', Name) > 0) and (Pos('Setup-{#AppVersion}', Name) = 0) then
begin
Log('REFUSED: this installer is {#AppVersion} but its file is named ' + Name + ' (the name must carry the version it installs)');
if not WizardSilent then
MsgBox('This installer is version {#AppVersion} but its file is named ' + Name + '. Download the installer again.', mbError, MB_OK);
Result := False;
exit;
end;
if FileExists(MarkerPath) then
begin
Log('a stale install marker was found at ' + MarkerPath + ' (an earlier installer did not finish); replaced by this run');
ClearMarker;
end;
end;
procedure DeinitializeSetup;
begin
ClearMarker;
@ -161,15 +186,19 @@ begin
Result := '';
SetMarker;
OldDir := OldAdminInstallDir;
StopScript := ExpandConstant('{app}\stop-igneum.ps1');
if (not FileExists(StopScript)) and (OldDir <> '') then
StopScript := OldDir + '\stop-igneum.ps1';
if not FileExists(StopScript) then
StopScript := ExpandConstant('{tmp}\stop-igneum.ps1');
if not FileExists(StopScript) then
ExtractTemporaryFile('stop-igneum.ps1');
if FileExists(StopScript) then
Exec('powershell.exe', '-NoProfile -ExecutionPolicy Bypass -File "' + StopScript + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
// Always THIS installer's stop step, never the installed one: PC 2, 0.3.23 take 1 (7 October 2026, 22:07 BST) ran the
// installed 0.3.21 script, which only asked the engine to quit; the window host lived on, Inno could not replace it,
// and the host started the old engine again 45 s later. The payload's script ends the host first, by its path under
// the install dir it is given, then the engine, then waits for every file to unlock (tools/ci/installer-stop-check.sh).
ExtractTemporaryFile('stop-igneum.ps1');
StopScript := ExpandConstant('{tmp}\stop-igneum.ps1');
Log('stop step: ' + StopScript + ' -Install "' + ExpandConstant('{app}') + '"');
if Exec('powershell.exe', '-NoProfile -ExecutionPolicy Bypass -File "' + StopScript + '" -Install "' + ExpandConstant('{app}') + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
Log('stop step: exit ' + IntToStr(ResultCode) + ' (0 = every file of ours is free; 3 = a file stayed locked, CloseApplications is the fallback)')
else
Log('stop step: powershell did not start (' + SysErrorMessage(ResultCode) + '); CloseApplications is the fallback');
if (OldDir <> '') and FileExists(OldDir + '\stop-igneum.ps1') then
Exec('powershell.exe', '-NoProfile -ExecutionPolicy Bypass -File "' + StopScript + '" -Install "' + OldDir + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
if (OldDir <> '') and (not WizardSilent) and FileExists(OldDir + '\unins000.exe') then
begin
if MsgBox('Igneum Miner now installs in your user folder, so updates need no administrator prompt.' + #13#10#13#10 +

View file

@ -3,8 +3,13 @@
# restart ladder started the old engine again 10 s later, the host stayed locked and no file was replaced): the window
# host goes FIRST, by its path under the install folder, so nothing can restart the engine; then the engine is asked to
# quit through its local API (miners first, then the node), waited for, and whatever of ours is left is ended by path.
# -Install <dir>: the install folder whose processes to end. The installer passes its {app} (this script then runs from
# the installer's temp folder, so its own location is not the install dir); the Start Menu entry and uninstall run it
# from the install folder itself and give nothing.
param([string]$Install = '')
$ErrorActionPreference = 'Continue'
$install = Split-Path -Parent $MyInvocation.MyCommand.Path
$install = if ($Install) { $Install.TrimEnd('\') } else { Split-Path -Parent $MyInvocation.MyCommand.Path }
Write-Host "stopping Igneum Miner under $install"
function Ours { @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object { $_.ExecutablePath -and $_.ExecutablePath.StartsWith($install, [StringComparison]::OrdinalIgnoreCase) }) }
foreach ($h in (Ours | Where-Object { $_.Name -eq 'Igneum Miner.exe' })) {
Write-Host "ending the window host (pid $($h.ProcessId)) first, so it cannot start the engine again"
@ -50,3 +55,4 @@ do {
} while ((Get-Date) -lt $deadline)
if ($locked.Count) { Write-Host ("STILL LOCKED after 30 s: " + ($locked -join ', ') + " (the installer's close-applications step is the fallback; a locked file is why an install leaves the old version in place)") } else { Write-Host 'every file of ours is free' }
Write-Host 'Igneum Miner is stopped.'
if ($locked.Count) { exit 3 }

View file

@ -3,4 +3,5 @@
| Check | What it fails | Since |
|---|---|---|
| installer-stop (`tools/ci/installer-stop-check.sh`): the .iss install path extracts and runs the PAYLOAD's stop-igneum.ps1 with `-Install "{app}"`, the script ends the window host by path before api/quit and waits for every exe to unlock with a STILL LOCKED line. This is a lint on the text; the test of the installer is release rule 14 (docs/plans/release-rules.md): run it end to end on a Windows box over a running older app and assert installed versions, the process set and the first upload. | 7 October 2026, 22:07 BST, PC 2, 0.3.23 take 1: PrepareToInstall preferred the installed 0.3.21 stop script (quit only), the host lived on, Inno could not replace it, the host restarted the old engine 45 s later, every file stayed 0.3.21; the crate's text tests had passed. |
| kill by exact command or pid file (owed as a check) | 6 October 2026, 21:09Z: a Mac-side `pkill -f <log file name>` matched nothing (the log name was a redirect, not part of the command line), the roll-everything script lived on and wiped a box it had been told to hold. Rule: a job is stopped by its pid file (`tools/fleet/fleet-bg.sh start|stop <name>`) or by a pattern anchored on its exact command line (`^python3 -u /root/fleet/in/box-prover.py`), never by a word that may or may not appear in it. The check that flags a `pkill -f`/`pgrep -f` whose literal is a path or a name that never starts a command line is owed to the CI lane |

View file

@ -0,0 +1,60 @@
#!/usr/bin/env bash
# The installer's own stop step ends the running app before Inno copies (install-close-23, 7 October 2026).
#
# PC 2, 0.3.23 take 1 at 22:07 BST: PrepareToInstall preferred the INSTALLED stop-igneum.ps1 ({app}, the 0.3.21 one that
# only asked the engine to quit), so the window host never stopped, Inno could not replace it, the host's restart ladder
# started the old 0.3.21 engine 45 s later and every file stayed 0.3.21. And the payload's script, when it did run from
# {tmp}, took its own folder as the install dir and so matched no process of ours.
#
# Rule, as this check reads packaging/windows/Igneum-Miner.iss and packaging/windows/stop-igneum.ps1:
# 1. PrepareToInstall extracts the PAYLOAD's stop-igneum.ps1 and runs that one ({tmp}); it never Execs {app}\stop-igneum.ps1
# or an older admin copy (an installed script is the OLD version's idea of how to stop).
# 2. The Exec passes the install dir: -Install "{app}".
# 3. stop-igneum.ps1 takes a param Install (its own folder is only the default) and ends 'Igneum Miner.exe' by path BEFORE
# it asks the engine to quit (the host is what restarts the engine).
# 4. The script waits for every exe of ours to open for write and names the one that stays locked ("STILL LOCKED").
#
# tools/ci/installer-stop-check.sh # exit 1 with the failing rule
# tools/ci/installer-stop-check.sh --self-test # fires on tonight's shape (the 0.3.21-style .iss), passes the fixed one
set -euo pipefail
cd "$(dirname "$0")/../.."
check() { # <iss file> <ps1 file> -> prints each broken rule, returns 1 when any is broken
local iss="$1" ps1="$2" bad=0 body
body="$(sed -n '/^function PrepareToInstall/,/^end;/p' "$iss")"
if ! grep -q "ExtractTemporaryFile('stop-igneum.ps1')" <<<"$body"; then echo "rule 1: PrepareToInstall does not extract the payload's stop-igneum.ps1"; bad=1; fi
if grep -Eq "Exec\(.*(\{app\}|OldDir).*stop-igneum" <<<"$body" || grep -Eq "StopScript := (ExpandConstant\('\{app\}|OldDir)" <<<"$body"; then echo "rule 1: PrepareToInstall runs an INSTALLED stop-igneum.ps1 (the old version's), not the payload's"; bad=1; fi
if ! grep -Fq -- '-Install ""{app}""' <<<"$body" && ! grep -Fq -- "-Install \"' + ExpandConstant('{app}')" <<<"$body"; then echo 'rule 2: the stop step is not given the install dir (-Install "{app}")'; bad=1; fi
if ! grep -Eq '^\s*param\s*\(' "$ps1" || ! grep -Eq '\$Install' "$ps1"; then echo "rule 3: stop-igneum.ps1 has no Install parameter"; bad=1; fi
local host quit
host="$(grep -n "Igneum Miner.exe" "$ps1" | grep -v '^\s*#' | head -1 | cut -d: -f1 || true)"
quit="$(grep -n "api/quit" "$ps1" | head -1 | cut -d: -f1 || true)"
if [[ -z "$host" || -z "$quit" || "$host" -gt "$quit" ]]; then echo "rule 3: the window host is not ended before the engine is asked to quit"; bad=1; fi
if ! grep -q "STILL LOCKED" "$ps1" || ! grep -q "FileShare\]::None" "$ps1"; then echo "rule 4: no unlock wait with a STILL LOCKED line"; bad=1; fi
return $bad
}
if [[ "${1:-}" == "--self-test" ]]; then
t="$(mktemp -d)"; trap 'rm -rf "$t"' EXIT
# tonight's shape: the installed script preferred, no -Install, a script that only quits the engine
cat > "$t/old.iss" <<'ISS'
function PrepareToInstall(var NeedsRestart: Boolean): String;
begin
StopScript := ExpandConstant('{app}\stop-igneum.ps1');
if not FileExists(StopScript) then
ExtractTemporaryFile('stop-igneum.ps1');
if FileExists(StopScript) then
Exec('powershell.exe', '-NoProfile -ExecutionPolicy Bypass -File "' + StopScript + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
ISS
printf '%s\n' '$install = Split-Path -Parent $MyInvocation.MyCommand.Path' 'Invoke-WebRequest -Uri ($url + "api/quit")' 'Stop-Process -Id $p.ProcessId' > "$t/old.ps1"
if check "$t/old.iss" "$t/old.ps1" >/dev/null; then echo "self-test: tonight's shape passed, it must fail"; exit 1; fi
if ! check packaging/windows/Igneum-Miner.iss packaging/windows/stop-igneum.ps1 >/dev/null; then echo "self-test: the tree's own files fail"; check packaging/windows/Igneum-Miner.iss packaging/windows/stop-igneum.ps1 || true; exit 1; fi
echo "installer-stop self-test: tonight's shape is caught, the tree's files pass"
exit 0
fi
if check packaging/windows/Igneum-Miner.iss packaging/windows/stop-igneum.ps1; then
echo "installer-stop: the installer runs the payload's stop step with the install dir, host first, unlock wait"
else
exit 1
fi

View file

@ -73,6 +73,7 @@ tree_checks() {
run "override params files parse with no duplicate key" bash tools/ci/override-json-check.sh
run "second-engine playbooks log to a file and end their tree (C35)" bash tools/ci/second-engine-check.sh
run "no playbook quits, pauses or resumes the installed app" bash -c 'bash tools/ci/playbook-quit-check.sh --self-test && bash tools/ci/playbook-quit-check.sh'
run "the installer runs its own stop step: payload script, install dir, host first, unlock wait" bash -c 'bash tools/ci/installer-stop-check.sh --self-test && bash tools/ci/installer-stop-check.sh'
run "no script writes into another worktree or walks Projects" bash -c 'bash tools/ci/no-foreign-tree-writes.sh --self-test && bash tools/ci/no-foreign-tree-writes.sh'
run "the signer is never piped into head" bash tools/ci/signer-pipe-check.sh
run "bash bodies in PowerShell job scripts pass bash -n" bash -c 'bash tools/ci/bash-body-check.sh --self-test && bash tools/ci/bash-body-check.sh'