Merge install-detach-26 4d3aa6e4 into release-0.3.26 (the installer's detach step logs the OS error, copies through a .part name, runs the source in place when no copy can be made, and under a job refuses rather than installing in place; take 3b's 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 14:09:27 +00:00
commit 0fd93532b3
2 changed files with 79 additions and 17 deletions

View file

@ -169,31 +169,77 @@ 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.
// The OS's words for the last error, for the log (PC 2, take 3b, 8 October 2026: "could not copy" with no reason).
function LastErrorWords: String;
var
E: Integer;
begin
E := DLLGetLastError;
Result := IntToStr(E) + ' ' + SysErrorMessage(E);
end;
// The installer's copy beside the app's data, or the source itself when no copy can be made (the job folder persists
// after the job; the detached task may run from it). Empty when neither can be used.
function DetachSource: String;
var
Dir, Copy, Temp, Src: String;
begin
Result := '';
Src := ExpandConstant('{srcexe}');
if not FileExists(Src) then
begin
Log('detach: the source ' + Src + ' does not exist (' + LastErrorWords + ')');
exit;
end;
Dir := ExpandConstant('{localappdata}\igneum\app\updates');
if not DirExists(Dir) then
begin
if not ForceDirectories(Dir) then
Log('detach: could not create ' + Dir + ' (' + LastErrorWords + ')');
end;
Copy := Dir + '\Igneum-Miner-Setup-{#AppVersion}.exe';
Temp := Dir + '\Igneum-Miner-Setup-{#AppVersion}.exe.part';
if DirExists(Dir) then
begin
DeleteFile(Temp);
if FileCopy(Src, Temp, False) then
begin
DeleteFile(Copy);
if RenameFile(Temp, Copy) then
begin
Result := Copy;
exit;
end;
Log('detach: copied to ' + Temp + ' but could not rename it to ' + Copy + ' (' + LastErrorWords + '); the task runs the .part copy');
Result := Temp;
exit;
end;
Log('detach: could not copy ' + Src + ' to ' + Temp + ' (' + LastErrorWords + ')');
end;
// no copy: the source itself, from where it is (a job's folder persists after the job)
Log('detach: the task runs the source in place: ' + Src);
Result := Src;
end;
// Starts the detached installer through a one-shot task. True when the task started.
function RelaunchDetached: Boolean;
var
Dir, Copy, Args, Cmd: String;
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;
Copy := DetachSource;
if Copy = '' then exit;
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));
Log('detach: schtasks /Create returned ' + IntToStr(ResultCode) + ' (' + LastErrorWords + ') for ' + Cmd);
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));
Log('detach: schtasks /Run returned ' + IntToStr(ResultCode) + ' (' + LastErrorWords + ')');
Exec('schtasks.exe', '/Delete /F /TN "' + DetachTask + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
exit;
end;
@ -229,11 +275,18 @@ 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)');
// the detached installer 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. When no detached start can be made the install STOPS with the
// reason in the log (take 3b, PC 2, 8 October 2026: the in-place fallback under the job ended the engine and died
// with it 3 s later, the app down, nothing installed); it never installs in place under a job.
if RelaunchDetached then
begin
Result := False;
exit;
end;
Log('REFUSED: this installer runs under the app''s job ' + GetEnv('IGNEUM_JOB_ID') + ' and no detached start could be made (the lines above say why); nothing is installed and the app is left running');
Result := False;
exit;
end;
Name := ExtractFileName(ExpandConstant('{srcexe}'));
if (Pos('Setup-', Name) > 0) and (Pos('Setup-{#AppVersion}', Name) = 0) then

View file

@ -39,6 +39,11 @@ check() { # <iss file> <ps1 file> -> prints each broken rule, returns 1 when
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
# rule 6b (take 3b, PC 2, 8 October 2026): the copy's OS error is logged, and a job install that cannot detach STOPS with the
# reason; it never installs in place under the job (the stop step would end the engine and the job's tree with it)
if ! grep -q "SysErrorMessage" "$iss" || ! grep -q "LastErrorWords" "$iss"; then echo "rule 6b: the detach step does not log the OS error of a failed copy or task"; bad=1; fi
if grep -q "installing in place under the job" "$iss"; then echo "rule 6b: a job install that cannot detach falls back to installing in place (it must stop with the reason)"; bad=1; fi
if ! grep -q "REFUSED: this installer runs under the app''s job" "$iss"; then echo "rule 6b: no REFUSED line for a job install that cannot detach"; 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)"
@ -66,6 +71,10 @@ 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
# the take 3b shape (install-detach-25): a copy that fails with no reason and an in-place fallback under the job
{ grep -v "SysErrorMessage\|LastErrorWords\|REFUSED: this installer runs under the app''s job" packaging/windows/Igneum-Miner.iss; echo " Log('detach: the detached start failed; installing in place under the job');"; } > "$t/take3b.iss"
t3b_out="$(check "$t/take3b.iss" packaging/windows/stop-igneum.ps1 2>/dev/null || true)"
if ! grep -q "rule 6b" <<<"$t3b_out"; then echo "self-test: the take 3b shape passed, it must fail rule 6b"; exit 1; fi
echo "installer-stop self-test: tonight's shape is caught, the tree's files pass"
exit 0
fi