Merge install-detach-25 3333faaa into release-0.3.25 (an installer started under the app's job runner re-launches itself as a one-shot scheduled task outside the job's process tree, so the engine's kill_tree on quit cannot end it between PrepareToInstall and the copy; the 0.3.24 take 2 class on PC 2; known-failed first)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-08 10:04:21 +00:00
commit aed46086a0
2 changed files with 96 additions and 1 deletions

View file

@ -143,19 +143,98 @@ begin
DeleteFile(MarkerPath); DeleteFile(MarkerPath);
end; 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); procedure CurStepChanged(CurStep: TSetupStep);
begin begin
if CurStep = ssDone then ClearMarker; if CurStep = ssDone then
begin
ClearMarker;
RemoveDetachTask;
end;
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 // 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 // (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. // installer left behind (older than 15 min: it died) is cleared with a line, so nothing holds on it.
function InitializeSetup: Boolean; function InitializeSetup: Boolean;
var var
Name: String; Name: String;
begin begin
Result := True; 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}')); Name := ExtractFileName(ExpandConstant('{srcexe}'));
if (Pos('Setup-', Name) > 0) and (Pos('Setup-{#AppVersion}', Name) = 0) then if (Pos('Setup-', Name) > 0) and (Pos('Setup-{#AppVersion}', Name) = 0) then
begin begin

View file

@ -31,6 +31,18 @@ check() { # <iss file> <ps1 file> -> prints each broken rule, returns 1 when
quit="$(grep -n "api/quit" "$ps1" | 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 [[ -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 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 "'/Create /F /SC ONCE" "$iss" || ! grep -q "Exec('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 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" 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 "$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 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\|/Create /F /SC ONCE\|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" echo "installer-stop self-test: tonight's shape is caught, the tree's files pass"
exit 0 exit 0
fi fi