diff --git a/relay/clients/igneum-agent-service.ps1 b/relay/clients/igneum-agent-service.ps1 index d7a3b0ced..e567f3dfe 100644 --- a/relay/clients/igneum-agent-service.ps1 +++ b/relay/clients/igneum-agent-service.ps1 @@ -1,6 +1,6 @@ # The relay agent as a logon task (IgneumRelayService, registered by install-agent.ps1 from IgneumRelayService.xml). # This loop keeps igneum-agent.ps1 running with no window: the Task Scheduler restarts this loop on failure (PT1M, 999 -# times), and the loop restarts the agent 15 s after any exit, so the agent outlives the app, a crash of its own, and a +# times), and the loop restarts the agent 10 s after any exit, so the agent outlives the app, a crash of its own, and a # reboot (the task fires at logon; the PC signs in by itself). MF-11, 7 October 2026: PC 2's agent had been dead since # 6 October and its one-shot logon task exited at every boot, so neither a signed job nor a relay task could start the app. # Output goes to %LOCALAPPDATA%\igneum-relay\logs\agent-service.log (5 MB, one rotation); the idle line is left out. @@ -19,7 +19,7 @@ while ($true) { # one Add-Content per line, so the file is never held open: a reader (a job's Get-Content -Tail) is refused while a # pipeline holds it (PC 2, 7 October 2026, 15:38 local: "being used by another process") & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $Agent 2>&1 | ForEach-Object { "$_" } | Where-Object { $_ -notmatch 'idle as ' } | ForEach-Object { Add-Content -Path $log -Value $_ } - Note ('agent exited with code ' + $LASTEXITCODE + '; again in 15 s') - } catch { Note ('agent failed to start: ' + $_.Exception.Message + '; again in 15 s') } - Start-Sleep -Seconds 15 + Note ('agent exited with code ' + $LASTEXITCODE + '; again in 10 s') + } catch { Note ('agent failed to start: ' + $_.Exception.Message + '; again in 10 s') } + Start-Sleep -Seconds 10 } diff --git a/relay/clients/install-agent.ps1 b/relay/clients/install-agent.ps1 index 7e26c5593..886555c02 100644 --- a/relay/clients/install-agent.ps1 +++ b/relay/clients/install-agent.ps1 @@ -6,8 +6,13 @@ # the task. Every IgneumRelayAgent* task (the one-shot reboot arms of X25) and the RunOnce key go first: on PC 2 a stale # one pointed at a path that was not there and popped "Windows cannot find 'igneum-agent'" at every boot (7 October 2026). # A failure here is a line on stdout and an exit code, never a dialog. -# powershell -ExecutionPolicy Bypass -File install-agent.ps1 [-Highest] [-Remove] -param([switch]$Highest, [switch]$Remove) +# The watchdog (watchdog.ps1 beside this file; 9 October 2026, the founder's rule that no machine needs a hand to +# recover): two more tasks, IgneumRelayWatchdog every 5 minutes and IgneumRelayWatchdogLogon a minute after logon, start +# the service task when the agent's pid is dead (the logon one as a loop checking every 10 s, so the agent is back within +# 15 s) and register it again when it is missing, so the agent outlives the +# engine's update, restart, quit and a reboot. -NoWatchdog skips them (the watchdog itself calls this script with it). +# powershell -ExecutionPolicy Bypass -File install-agent.ps1 [-Highest] [-Remove] [-NoWatchdog] +param([switch]$Highest, [switch]$Remove, [switch]$NoWatchdog) $ErrorActionPreference = 'Continue' $Here = Split-Path -Parent $MyInvocation.MyCommand.Path $TaskName = 'IgneumRelayService' @@ -30,11 +35,19 @@ try { if ((Test-Path $k) -and ((Get-ItemProperty -Path $k -ErrorAction SilentlyContinue).PSObject.Properties.Name -contains 'IgneumRelayAgent')) { Remove-ItemProperty -Path $k -Name 'IgneumRelayAgent' -ErrorAction SilentlyContinue; Write-Host 'removed the RunOnce key IgneumRelayAgent' } } catch { } if ($Remove) { + foreach ($w in @('IgneumRelayWatchdog', 'IgneumRelayWatchdogLogon')) { & schtasks.exe /Delete /F /TN $w 2>&1 | Out-Null } & schtasks.exe /End /TN $TaskName 2>&1 | Out-Null & schtasks.exe /Delete /F /TN $TaskName 2>&1 | Out-Null Write-Host ('removed the ' + $TaskName + ' task (an agent window started by hand is not touched)') exit 0 } +# A stale lock (9 October 2026, both PCs at 09:0x UK): an agent copy that holds the mutex but no longer polls (it sat waiting +# on an app a task had relaunched) made the bat refuse a second copy, and a hand was needed. Every shell running +# igneum-agent.ps1 or its service loop is ended here BY PID before the task is registered, so the install always starts +# clean; nothing of the Miner is touched (the engine and its workers are not powershell shells of ours). +$stale = @(Get-CimInstance Win32_Process | Where-Object { $_.Name -match '^(powershell|pwsh)\.exe$' -and $_.CommandLine -match 'igneum-agent(-service)?\.ps1' -and $_.ProcessId -ne $PID }) +foreach ($p in $stale) { Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue; Write-Host ('ended the old agent shell pid ' + $p.ProcessId + ' (it held the lock)') } +if ($stale.Count -gt 0) { Start-Sleep -Seconds 2 } foreach ($f in @('IgneumRelayService.xml', 'igneum-agent-service.ps1', 'igneum-agent.ps1', 'machine-secret.txt')) { if (-not (Test-Path (Join-Path $Here $f))) { Write-Host ('missing ' + $f + ' beside this script (unzip the whole client zip from make-clients.sh --machine )'); exit 2 } } @@ -53,6 +66,16 @@ try { Write-Host ('registered ' + $TaskName + ' for ' + $user + ' at ' + $level + ' from ' + $Here) } finally { Remove-Item -Path $tmp -Force -ErrorAction SilentlyContinue } & schtasks.exe /Run /TN $TaskName 2>&1 | ForEach-Object { "$_" } | Write-Host +if (-not $NoWatchdog) { + $wd = Join-Path $Here 'watchdog.ps1' + if (Test-Path $wd) { + $tr = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + $wd + '"' + $w1 = & schtasks.exe /Create /F /TN 'IgneumRelayWatchdog' /SC MINUTE /MO 5 /RL LIMITED /TR $tr 2>&1 | ForEach-Object { "$_" } + $w2 = & schtasks.exe /Create /F /TN 'IgneumRelayWatchdogLogon' /SC ONLOGON /DELAY 0001:00 /RL LIMITED /TR ($tr + ' -Loop') 2>&1 | ForEach-Object { "$_" } + Write-Host ('watchdog tasks: ' + (($w1 + $w2) -join ' | ')) + & schtasks.exe /Run /TN 'IgneumRelayWatchdogLogon' 2>&1 | Out-Null + } else { Write-Host ('no watchdog.ps1 beside this script; the watchdog tasks are not registered') } +} Start-Sleep -Seconds 3 & schtasks.exe /Query /TN $TaskName /V /FO LIST 2>&1 | ForEach-Object { "$_" } | Where-Object { $_ -match '^(TaskName|Status|Logon Mode|Last Run Time|Last Result|Task To Run|Run As User|Schedule Type):' } | Write-Host Write-Host 'the agent now runs under the task; close any igneum-agent.bat window (the task copy exits while that one holds the mutex, and comes back 15 s after it closes)' diff --git a/relay/clients/make-clients.sh b/relay/clients/make-clients.sh index 046510e93..f415b64aa 100755 --- a/relay/clients/make-clients.sh +++ b/relay/clients/make-clients.sh @@ -31,7 +31,7 @@ fi mkdir -p "$OUT" NAME="igneum-relay-clients${MACHINE:+-$MACHINE}" STAGE="$(mktemp -d)/$NAME"; mkdir -p "$STAGE"; umask 077 -for f in send.bat send.ps1 send.sh igneum-agent.bat igneum-agent.ps1 igneum-agent-service.ps1 install-agent.ps1 IgneumRelayService.xml agent.sh CLAUDE-PC.md; do +for f in send.bat send.ps1 send.sh igneum-agent.bat igneum-agent.ps1 igneum-agent-service.ps1 install-agent.ps1 watchdog.ps1 IgneumRelayService.xml agent.sh CLAUDE-PC.md; do sed -e "s#__RELAY_URL__#$URL#g" -e "s#__RELAY_KEY__#$KEY#g" -e "s#__RELAY_TOKEN__#$TOKEN#g" -e "s#__DL_BASE__#$DL_BASE#g" "$HERE/$f" > "$STAGE/$f" done [ -n "$SECRET" ] && printf '%s\n' "$SECRET" > "$STAGE/machine-secret.txt" diff --git a/relay/clients/watchdog.ps1 b/relay/clients/watchdog.ps1 new file mode 100644 index 000000000..c12da888c --- /dev/null +++ b/relay/clients/watchdog.ps1 @@ -0,0 +1,46 @@ +# Relay agent watchdog (9 October 2026, the founder's standing rule: no machine needs a hand to recover). Registered by +# install-agent.ps1 as two scheduled tasks beside IgneumRelayService: IgneumRelayWatchdog every 5 minutes and +# IgneumRelayWatchdogLogon one minute after this user's logon. When the agent's pid file names no live process and no +# igneum-agent.ps1 shell runs, it starts the IgneumRelayService task; when that task is missing, it registers it again +# with install-agent.ps1 from this folder. The agent is only ever started by the task scheduler, never by a job of the +# engine (an agent started under a job died with the engine's update on PC 1, 9 October 09:08 UK). It ends nothing. +# watchdog.ps1 one check (the five-minute task): the agent, then the loop below when none runs +# watchdog.ps1 -Loop the loop (the logon task): one check every 10 s for 55 minutes, so a dead agent is back within +# 15 s (the founder's bar); it exits after 55 minutes and the five-minute task starts it again +param([switch]$Loop) +$ErrorActionPreference = 'Continue' +$Here = Split-Path -Parent $MyInvocation.MyCommand.Path +$state = Join-Path $env:LOCALAPPDATA 'igneum-relay' +New-Item -ItemType Directory -Force -Path $state | Out-Null +$log = Join-Path $state 'watchdog.log' +function L($s) { Add-Content -Path $log -Value ((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + ' ' + $s) } +$live = Join-Path $state 'agent.live' +function Agent-Alive { + if (Test-Path -LiteralPath $live) { try { $t = (Get-Content -LiteralPath $live -Raw).Trim(); if ($t -match '^\d+$' -and (Get-Process -Id ([int]$t) -ErrorAction SilentlyContinue)) { return $true } } catch { } } + return [bool](Get-CimInstance Win32_Process | Where-Object { $_.Name -match '^(powershell|pwsh)\.exe$' -and $_.CommandLine -match 'igneum-agent\.ps1' } | Select-Object -First 1) +} +function Check { + if (Agent-Alive) { return } + $q = & schtasks.exe /Query /TN 'IgneumRelayService' 2>&1 | Out-String + if ($q -match 'IgneumRelayService') { + & schtasks.exe /Run /TN 'IgneumRelayService' 2>&1 | Out-Null + L 'agent not running: IgneumRelayService task started' + } else { + $inst = Join-Path $Here 'install-agent.ps1' + if (Test-Path -LiteralPath $inst) { & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $inst -NoWatchdog 2>&1 | Out-Null; L ('IgneumRelayService task missing: registered again from ' + $inst) } else { L ('IgneumRelayService task missing and no install-agent.ps1 beside ' + $Here + '; nothing done') } + } +} +if ($Loop) { + # one loop at a time: a second one exits + $mine = $PID + $other = Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -ne $mine -and $_.Name -match '^(powershell|pwsh)\.exe$' -and $_.CommandLine -match 'watchdog\.ps1' -and $_.CommandLine -match '-Loop' } | Select-Object -First 1 + if ($other) { exit 0 } + $until = (Get-Date).AddMinutes(55) + while ((Get-Date) -lt $until) { Check; Start-Sleep -Seconds 10 } + exit 0 +} +Check +# the loop itself: started here when none runs (after a logon it is the logon task's; after 55 minutes it is this task's) +$loopUp = Get-CimInstance Win32_Process | Where-Object { $_.Name -match '^(powershell|pwsh)\.exe$' -and $_.CommandLine -match 'watchdog\.ps1' -and $_.CommandLine -match '-Loop' } | Select-Object -First 1 +if (-not $loopUp) { & schtasks.exe /Run /TN 'IgneumRelayWatchdogLogon' 2>&1 | Out-Null; L 'watchdog loop not running: IgneumRelayWatchdogLogon task started' } +exit 0