From fa00b67bbd4ef63ce223e14cb258ef69785d64c3 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:00:44 +0000 Subject: [PATCH] 0.3.25: an installer started by the app's job runner detaches itself from the job's process tree (PC 2, 0.3.24 take 2, 8 October 2026, 09:24Z: the stop step ended the host and quit the engine, the engine's job runner ended the job's whole process tree on its way out (src/jobrun.rs kill_tree, taskkill /T) and the installer, the job script's child, died between PrepareToInstall and the copy: every exe still 0.3.21, install-running.flag left behind, no ssDone, no DeinitializeSetup). Igneum-Miner.iss: under a job (IGNEUM_JOB_ID set, no /IGDETACHED=1) InitializeSetup copies the installer to \igneum\app\updates\, registers and runs a one-shot scheduled task (a child of the Task Scheduler, outside the tree) with the same switches plus /IGDETACHED=1 and /LOG=install--detached.log, and exits before any marker or stop step; the detached run installs and deletes its task at ssDone; if the detached start fails it installs in place and says so in the log. The job's proof is the --version wait and the detached log, not this process's exit code (1). installer-stop-check rule 6 reads the path, the order (InitializeSetup first) and Inno's declare-before-use (RemoveDetachTask above CurStepChanged); self-test known-failed on 78dfe65e's installer Co-Authored-By: Claude Fable 5.1 --- packaging/windows/Igneum-Miner.iss | 81 +++++++++++++++++++++++++++++- tools/ci/installer-stop-check.sh | 16 ++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/packaging/windows/Igneum-Miner.iss b/packaging/windows/Igneum-Miner.iss index 87737b421..5f9c46244 100644 --- a/packaging/windows/Igneum-Miner.iss +++ b/packaging/windows/Igneum-Miner.iss @@ -143,19 +143,98 @@ begin DeleteFile(MarkerPath); end; +// An installer started by the app's own job runner is a CHILD of the engine; the stop step then quits the engine, and +// the engine ends the job's whole process tree on its way out (src/jobrun.rs kill_tree: taskkill /T), the installer with +// it, between PrepareToInstall and the copy (PC 2, 0.3.24 take 2, 8 October 2026, 09:24Z: the host gone, no file copied, +// install-running.flag left behind, no ssDone and no DeinitializeSetup). So under a job (IGNEUM_JOB_ID set) this installer +// copies itself to the app's updates folder and runs that copy DETACHED through a one-shot scheduled task (a child of +// the Task Scheduler, outside the job's tree), with the same switches plus /IGDETACHED=1 and its own log, then exits +// without touching the app; the detached run installs and removes the task at its end. The job's proof is the --version +// wait and the detached log, not this process's exit code (1: "Setup failed to initialize"). +const + DetachTask = 'Igneum Miner install'; + +function DetachedRun: Boolean; +begin + Result := ExpandConstant('{param:IGDETACHED|0}') = '1'; +end; + +function UnderAppJob: Boolean; +begin + Result := (GetEnv('IGNEUM_JOB_ID') <> '') and (not DetachedRun); +end; + +function DetachLogPath: String; +begin + Result := ExpandConstant('{localappdata}\igneum\app\updates\install-{#AppVersion}-detached.log'); +end; + +// Copies this installer beside the app's data and starts the copy through a one-shot task. True when the task started. +function RelaunchDetached: Boolean; +var + Dir, Copy, Args, Cmd: String; + ResultCode: Integer; +begin + Result := False; + Dir := ExpandConstant('{localappdata}\igneum\app\updates'); + ForceDirectories(Dir); + Copy := Dir + '\Igneum-Miner-Setup-{#AppVersion}.exe'; + if not FileCopy(ExpandConstant('{srcexe}'), Copy, False) then + begin + Log('detach: could not copy ' + ExpandConstant('{srcexe}') + ' to ' + Copy); + exit; + end; + Args := '/VERYSILENT /NORESTART /SUPPRESSMSGBOXES /IGNOTA=' + ExpandConstant('{param:IGNOTA|0}') + ' /IGDETACHED=1 /LOG="' + DetachLogPath + '"'; + Cmd := '/Create /F /SC ONCE /ST 23:59 /RL LIMITED /TN "' + DetachTask + '" /TR "\"' + Copy + '\" ' + Args + '"'; + if not Exec('schtasks.exe', Cmd, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) or (ResultCode <> 0) then + begin + Log('detach: schtasks /Create returned ' + IntToStr(ResultCode)); + exit; + end; + if not Exec('schtasks.exe', '/Run /TN "' + DetachTask + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) or (ResultCode <> 0) then + begin + Log('detach: schtasks /Run returned ' + IntToStr(ResultCode)); + Exec('schtasks.exe', '/Delete /F /TN "' + DetachTask + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + exit; + end; + Log('detach: this installer runs under the app''s job ' + GetEnv('IGNEUM_JOB_ID') + '; the copy at ' + Copy + ' installs detached, log ' + DetachLogPath); + Result := True; +end; + +procedure RemoveDetachTask; +var + ResultCode: Integer; +begin + if DetachedRun then + Exec('schtasks.exe', '/Delete /F /TN "' + DetachTask + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); +end; + procedure CurStepChanged(CurStep: TSetupStep); begin - if CurStep = ssDone then ClearMarker; + if CurStep = ssDone then + begin + ClearMarker; + RemoveDetachTask; + end; 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; begin Result := True; + if UnderAppJob then + begin + // the detached copy does the install; this process ends here, before any marker or stop step, so the job's tree + // teardown finds nothing of the install to kill + Result := not RelaunchDetached; + if not Result then exit; + Log('detach: the detached start failed; installing in place under the job (the stop step will end this process with the engine)'); + end; Name := ExtractFileName(ExpandConstant('{srcexe}')); if (Pos('Setup-', Name) > 0) and (Pos('Setup-{#AppVersion}', Name) = 0) then begin diff --git a/tools/ci/installer-stop-check.sh b/tools/ci/installer-stop-check.sh index 98fcfa3fc..7a8d6129c 100755 --- a/tools/ci/installer-stop-check.sh +++ b/tools/ci/installer-stop-check.sh @@ -31,6 +31,18 @@ check() { # -> prints each broken rule, returns 1 when 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 + # rule 6 (PC 2, 0.3.24 take 2, 8 October 2026, 09:24Z): an installer started by the app's job runner is the engine's child and + # dies with the job's tree when the stop step quits the engine; under a job (IGNEUM_JOB_ID) it must re-run itself detached + # (a copy through a one-shot scheduled task, /IGDETACHED=1) and exit before the marker and the stop step + if ! grep -q "GetEnv('IGNEUM_JOB_ID')" "$iss" || ! grep -q "IGDETACHED" "$iss" || ! grep -q "schtasks.exe', '/Create" "$iss" || ! grep -q "schtasks.exe', '/Run" "$iss"; then echo "rule 6: the installer does not detach itself from the app's job tree (IGNEUM_JOB_ID, a scheduled-task relaunch with /IGDETACHED=1)"; bad=1; fi + local init + init="$(sed -n '/^function InitializeSetup/,/^end;/p' "$iss")" + if ! grep -q "UnderAppJob" <<<"$init" || ! grep -q "RelaunchDetached" <<<"$init"; then echo "rule 6: InitializeSetup does not run the detach before anything else"; bad=1; fi + if ! grep -q "RemoveDetachTask" "$iss"; then echo "rule 6: the detached run does not remove its task at ssDone"; bad=1; fi + # Inno's Pascal: a routine must be declared before the line that calls it (CurStepChanged calls RemoveDetachTask) + local def use + def="$(grep -n '^procedure RemoveDetachTask' "$iss" | cut -d: -f1 || true)"; use="$(grep -n '^procedure CurStepChanged' "$iss" | cut -d: -f1 || true)" + if [[ -n "$def" && -n "$use" && "$def" -gt "$use" ]]; then echo "rule 6: RemoveDetachTask is declared after CurStepChanged, which calls it (Inno compiles top down)"; bad=1; fi return $bad } @@ -50,6 +62,10 @@ 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 + # the job-tree shape (0.3.24 take 2): an installer with no detach path; known-failed first on release-0.3.25 9b93e649's .iss + grep -v "IGNEUM_JOB_ID\|IGDETACHED\|schtasks.exe', '/Create\|schtasks.exe', '/Run\|UnderAppJob\|RelaunchDetached\|RemoveDetachTask" packaging/windows/Igneum-Miner.iss > "$t/tree.iss" + tree_out="$(check "$t/tree.iss" packaging/windows/stop-igneum.ps1 2>/dev/null || true)" + if ! grep -q "rule 6" <<<"$tree_out"; then echo "self-test: the job-tree shape passed, it must fail rule 6"; exit 1; fi echo "installer-stop self-test: tonight's shape is caught, the tree's files pass" exit 0 fi