# Igneum devnet node, Windows launcher. Started by START-NODE.bat, which sets the settings as environment variables # (MAC_NODE, RPC_LISTEN, P2P_LISTEN, APPDIR, UNSYNCED_MINING, STATUS_SECS, EXTRA_ARGS). 3 October 2026. # # What it does, in order: # 1. Checks igneumd.exe and the ports, prints the node version, tests whether the Mac node answers on its p2p port. # 2. Starts igneumd.exe --devnet with --appdir, --rpclisten, --listen, --addpeer=, --nodnsseed, --disable-upnp and # --nologfiles; the node's own output goes to node-.log next to the bat (a new segment per start, because # Start-Process truncates its redirect target). The launcher's own lines go to node-launcher-.log. # 3. Keeps the PC awake (SetThreadExecutionState, no power plan change). # 4. Prints one status line every STATUS_SECS: blocks, headers, peers, blue score, DAA score, tips, synced yes or no. # The reading comes from `igneum-miner.exe watch 1 grpc://` run in the background, the same tool and # line the mining launcher reads (vendor/igneum-node/igneum/miner/src/main.rs, watch): # node1 blocks=N headers=N daa=N tips=N peers=N difficulty=X sink=<12 hex> blue=N synced=true|false # 5. Restarts the node after a crash with a random delay of 5 to 60 s; uploads the launcher log every 5 minutes with # upload-log.bat when it is present; Ctrl+C stops the node and prints a summary. # # Why --addpeer and not --connect: in igneumd (kaspad/src/daemon.rs) --connect sets the inbound limit to 0 and skips # outbound peers, so the Mac could never dial this PC. --addpeer keeps the Mac as a permanent peer (retried with a # backoff of up to 8 minutes, components/connectionmanager/src/lib.rs) and leaves the listener open for the Mac to # connect in. The Mac node's inbound limit is the default 128. $ProgressPreference = 'SilentlyContinue' function Test-Port($h, $p) { # Plain TCP probe. Test-NetConnection paints a blue progress banner in some consoles whatever $ProgressPreference says. try { $c = New-Object Net.Sockets.TcpClient; $r = $c.BeginConnect($h, [int]$p, $null, $null); $ok = $r.AsyncWaitHandle.WaitOne(2000) -and $c.Connected; $c.Close(); return [bool]$ok } catch { return $false } } $ErrorActionPreference = 'Continue' $root = Split-Path -Parent $MyInvocation.MyCommand.Path Set-Location $root $macNode = if ($env:MAC_NODE) { $env:MAC_NODE } else { '192.168.68.64:26611' } $rpcListen = if ($env:RPC_LISTEN) { $env:RPC_LISTEN } else { '127.0.0.1:26610' } $p2pListen = if ($env:P2P_LISTEN) { $env:P2P_LISTEN } else { '0.0.0.0:26611' } $appDir = if ($env:APPDIR) { $env:APPDIR } else { Join-Path $env:LOCALAPPDATA 'igneum\devnet' } $unsyncedMining = ($env:UNSYNCED_MINING -eq '1') $statusSecs = 30 if ($env:STATUS_SECS -and [int]$env:STATUS_SECS -ge 5) { $statusSecs = [int]$env:STATUS_SECS } $extraArgs = $env:EXTRA_ARGS $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' $machine = $env:COMPUTERNAME $launcherLog = Join-Path $root "node-launcher-$stamp.log" $uploader = Join-Path $root 'upload-log.bat' $nodeExe = Join-Path $root 'igneumd.exe' $miner = Join-Path $root 'igneum-miner.exe' $started = Get-Date # The RPC address the status reader dials: the listen address, with an unspecified interface mapped to loopback. $rpcHost = ($rpcListen -split ':')[0] $rpcPort = ($rpcListen -split ':')[-1] if ($rpcHost -eq '0.0.0.0' -or $rpcHost -eq '') { $rpcHost = '127.0.0.1' } $rpcUrl = "grpc://${rpcHost}:${rpcPort}" $p2pPort = ($p2pListen -split ':')[-1] $macHost = ($macNode -split ':')[0] $macPort = ($macNode -split ':')[-1] function Log([string]$msg) { $line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $msg" Write-Host $line Add-Content -Path $launcherLog -Value $line } # The last lines of a file another process is still writing (FileShare ReadWrite). function Get-TailLines([string]$path, [int]$count) { if (-not $path -or -not (Test-Path $path)) { return @() } try { $fs = New-Object IO.FileStream($path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite) try { $len = $fs.Length; $n = [Math]::Min($len, 16384); [void]$fs.Seek(-$n, [IO.SeekOrigin]::End) $b = New-Object byte[] $n; [void]$fs.Read($b, 0, $n) } finally { $fs.Close() } $lines = @(([Text.Encoding]::UTF8.GetString($b) -split "`n") | ForEach-Object { $_.TrimEnd("`r") } | Where-Object { $_ -ne '' }) if ($lines.Count -le $count) { return $lines } return @($lines[($lines.Count - $count)..($lines.Count - 1)]) } catch { return @() } } Log "igneum node launcher on $machine (run $stamp)" Log "settings: Mac node $macNode, RPC $rpcListen, p2p $p2pListen, appdir $appDir, unsynced mining $(if ($unsyncedMining) { 'on' } else { 'off' }), status every $statusSecs s" # ---- 1. checks ------------------------------------------------------------------------------------------------------ if (-not (Test-Path $nodeExe)) { Log 'igneumd.exe is missing. Run BUILD-NODE.bat first.'; exit 1 } $ver = '' try { $ver = (& $nodeExe --version 2>&1 | Select-Object -First 1) } catch { $ver = "could not run igneumd.exe: $_" } Log "node binary: $nodeExe ($ver)" if (-not (Test-Path $miner)) { Log 'igneum-miner.exe is not in this folder: the status line will only say whether the node process is alive' } # Another node on the same ports (a second START-NODE window, or a stale process) foreach ($port in @([int]$rpcPort, [int]$p2pPort)) { try { $busy = @(Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue) if ($busy.Count -gt 0) { $owner = '' try { $owner = (Get-Process -Id $busy[0].OwningProcess -ErrorAction SilentlyContinue).ProcessName } catch { } Log "port $port is already in use by $owner (pid $($busy[0].OwningProcess)). Is a node already running? Close it first." exit 1 } } catch { } } try { $macUp = Test-Port $macHost $macPort } catch { $macUp = $false } if ($macUp) { Log "the Mac node answers on $macNode" } else { Log "the Mac node at $macNode does not answer right now; igneumd will keep retrying (backoff up to 8 minutes)" } Log "If Windows Firewall asks about igneumd.exe: tick Private networks and click Allow access (the Mac node dials this PC on port $p2pPort). If you clicked Cancel once, run ALLOW-FIREWALL.bat." New-Item -ItemType Directory -Force -Path $appDir | Out-Null # ---- 2. the node --------------------------------------------------------------------------------------------------- # Start-Process joins the argument list with spaces and does not quote, so values that may contain spaces are quoted here. $nodeArgs = @('--devnet', ('"--appdir=' + $appDir + '"'), "--rpclisten=$rpcListen", "--listen=$p2pListen", "--addpeer=$macNode", '--nodnsseed', '--disable-upnp', '--nologfiles') if ($unsyncedMining) { $nodeArgs += '--enable-unsynced-mining' } if ($extraArgs) { $nodeArgs += ($extraArgs -split '\s+' | Where-Object { $_ -ne '' }) } $script:node = @{ proc = $null; log = $null; err = $null; starts = 0; startedAt = $null; restartAt = $null; restarts = 0; exitCode = $null } function Start-Node { $n = $script:node $n.starts += 1 $seg = if ($n.starts -gt 1) { "-r$($n.starts)" } else { '' } $n.log = Join-Path $root "node-$stamp$seg.log" $n.err = $n.log + '.err' $n.restartAt = $null $n.proc = Start-Process -FilePath $nodeExe -ArgumentList $nodeArgs -WorkingDirectory $root -RedirectStandardOutput $n.log -RedirectStandardError $n.err -NoNewWindow -PassThru if ($n.proc) { $null = $n.proc.Handle } # without touching Handle, ExitCode comes back empty after the process ends $n.startedAt = Get-Date Log "igneumd started (pid $($n.proc.Id)): `"$nodeExe`" $($nodeArgs -join ' ') -> $($n.log)" } function Stop-Node { $n = $script:node if ($n.proc -and -not $n.proc.HasExited) { & taskkill.exe /T /F /PID $n.proc.Id 2>$null | Out-Null } } # ---- 4. status, read with igneum-miner watch in the background ---------------------------------------------------------- $script:watch = @{ proc = $null; log = (Join-Path $root "status-$stamp.tmp"); err = (Join-Path $root "status-$stamp.tmp.err"); lastStart = $null; fails = 0; last = $null } $script:statusCount = 0 function Start-Watch { if (-not (Test-Path $miner)) { return } $w = $script:watch if ($w.proc -and -not $w.proc.HasExited) { if ($w.lastStart -and ((Get-Date) - $w.lastStart).TotalSeconds -gt 90) { & taskkill.exe /T /F /PID $w.proc.Id 2>$null | Out-Null; $w.proc = $null; $w.fails += 1 } else { return } } try { $w.proc = Start-Process -FilePath $miner -ArgumentList @('watch', '1', $rpcUrl) -WorkingDirectory $root -RedirectStandardOutput $w.log -RedirectStandardError $w.err -NoNewWindow -PassThru if ($w.proc) { $null = $w.proc.Handle } $w.lastStart = Get-Date } catch { $w.proc = $null; $w.fails += 1 } } # Returns the parsed reading when the watch process has finished, else $null. Sets $script:watch.last. function Read-Watch { $w = $script:watch if (-not $w.proc -or -not $w.proc.HasExited) { return $null } $w.proc = $null $line = $null try { $text = '' $fs = New-Object IO.FileStream($w.log, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite) try { $sr = New-Object IO.StreamReader($fs); $text = $sr.ReadToEnd(); $sr.Close() } finally { $fs.Close() } foreach ($l in ($text -split "`n")) { if ($l -match ' node1 blocks=') { $line = $l; break } } } catch { } if ($line -and $line -match 'blocks=([0-9]+) headers=([0-9]+) daa=([0-9]+) tips=([0-9]+) peers=([0-9]+) difficulty=([0-9.]+) sink=([0-9a-f]+)(?: blue=([0-9]+))?(?: synced=(\w+))?') { $r = @{ at = (Get-Date); blocks = [long]$Matches[1]; headers = [long]$Matches[2]; daa = [long]$Matches[3]; tips = [int]$Matches[4]; peers = [int]$Matches[5]; difficulty = [double]$Matches[6]; sink = $Matches[7]; blue = $(if ($Matches[8]) { [long]$Matches[8] } else { -1 }); synced = $(if ($Matches[9]) { ($Matches[9] -eq 'true') } else { $null }) } $w.fails = 0 $w.last = $r return $r } $w.fails += 1 return $null } function Print-Status($r) { $script:statusCount += 1 $n = $script:node $up = ((Get-Date) - $started).ToString('hh\:mm\:ss') $alive = ($n.proc -and -not $n.proc.HasExited) $tail = "up $up, restarts $($n.restarts)" if (-not $alive) { Log "status: the node process is not running ($tail)"; return } if ($r) { $syncTxt = if ($r.synced -eq $null) { 'n/a' } elseif ($r.synced) { 'yes' } else { 'no' } $blueTxt = if ($r.blue -ge 0) { [string]$r.blue } else { 'n/a' } $peerTxt = if ($r.peers -eq 0) { '0 (the Mac node is not connected yet)' } else { [string]$r.peers } Log ("status: blocks {0}, headers {1}, peers {2}, blue score {3}, DAA {4}, tips {5}, difficulty {6:N0}, synced {7} ({8})" -f $r.blocks, $r.headers, $peerTxt, $blueTxt, $r.daa, $r.tips, $r.difficulty, $syncTxt, $tail) } elseif (-not (Test-Path $miner)) { Log "status: igneumd is running (pid $($n.proc.Id)), no reader for the chain state without igneum-miner.exe ($tail)" } else { $age = [int]((Get-Date) - $n.startedAt).TotalSeconds $why = if ($age -lt 60) { 'it is still starting, opening the database' } else { "no answer from the RPC at $rpcUrl for $($script:watch.fails) readings; see $($n.log)" } Log "status: igneumd is running (pid $($n.proc.Id)) but gave no reading yet: $why ($tail)" } } # ---- upload (optional) -------------------------------------------------------------------------------------------------- function Upload-Logs { if (-not (Test-Path $uploader)) { return } $o = cmd /s /c "`"$uploader`" `"$launcherLog`" node-$machine node-$machine-$stamp" 2>&1 Add-Content -Path $launcherLog -Value (" upload launcher log: " + ($o -join ' ')) $n = $script:node if ($n.log -and (Test-Path $n.log)) { $o = cmd /s /c "`"$uploader`" `"$($n.log)`" nodelog-$machine nodelog-$machine-$stamp" 2>&1 Add-Content -Path $launcherLog -Value (" upload node log: " + ($o -join ' ')) } } # ---- 3. keep awake --------------------------------------------------------------------------------------------------- $keepAwake = $null try { $keepAwake = Add-Type -Name 'KeepAwake' -Namespace 'Igneum' -PassThru -MemberDefinition @' [DllImport("kernel32.dll", SetLastError = true)] public static extern uint SetThreadExecutionState(uint esFlags); '@ [void]$keepAwake::SetThreadExecutionState([uint32]2147483648 -bor [uint32]0x00000001) # ES_CONTINUOUS | ES_SYSTEM_REQUIRED Log 'keeping the PC awake while the node runs' } catch { Log "could not set the keep-awake state: $_ (set Sleep to Never for the night)"; $keepAwake = $null } $lastAwake = Get-Date # ---- 5. loop ---------------------------------------------------------------------------------------------------------- Start-Node Start-Sleep -Seconds 3 if ($script:node.proc.HasExited) { Log "igneumd exited at once with code $($script:node.proc.ExitCode). Last lines:" foreach ($l in (Get-TailLines $script:node.err 8)) { Log " stderr: $l" } foreach ($l in (Get-TailLines $script:node.log 8)) { Log " stdout: $l" } Log "Nothing to restart. Fix the cause above (a wrong --appdir, a port in use, a database from an older build that needs deleting: $appDir) and start again." if ($keepAwake) { [void]$keepAwake::SetThreadExecutionState([uint32]2147483648) } exit 1 } foreach ($l in (Get-TailLines $script:node.log 3)) { Log " node: $l" } [Console]::TreatControlCAsInput = $true Log "running. Press Ctrl+C to stop. Status every $statusSecs s; the node's full output is in $($script:node.log)" $lastStatus = (Get-Date).AddSeconds($statusSecs - 10) # first reading soon after the start $lastUpload = Get-Date $watchPending = $false $stop = $false try { while (-not $stop) { Start-Sleep -Milliseconds 500 if ($keepAwake -and ((Get-Date) - $lastAwake).TotalSeconds -ge 60) { [void]$keepAwake::SetThreadExecutionState([uint32]2147483648 -bor [uint32]0x00000001) $lastAwake = Get-Date } while ([Console]::KeyAvailable) { $k = [Console]::ReadKey($true) if ($k.Key -eq 'C' -and ($k.Modifiers -band [ConsoleModifiers]::Control)) { $stop = $true } } if ($stop) { break } $n = $script:node if ($n.proc -and $n.proc.HasExited -and -not $n.restartAt) { $n.restarts += 1 $n.exitCode = $null try { $n.exitCode = $n.proc.ExitCode } catch { } $delay = Get-Random -Minimum 5 -Maximum 61 $n.restartAt = (Get-Date).AddSeconds($delay) Log "igneumd exited with code $($n.exitCode) after $([int]((Get-Date) - $n.startedAt).TotalSeconds) s; restarting in $delay s (restart $($n.restarts))" foreach ($l in (Get-TailLines $n.err 5)) { Log " stderr: $l" } foreach ($l in (Get-TailLines $n.log 5)) { Log " stdout: $l" } } if ($n.restartAt -and (Get-Date) -ge $n.restartAt) { Start-Node } # one reading per status period: start the reader, print when it has finished if (((Get-Date) - $lastStatus).TotalSeconds -ge $statusSecs) { $lastStatus = Get-Date if ($n.proc -and -not $n.proc.HasExited -and (Test-Path $miner)) { Start-Watch; $watchPending = $true } else { Print-Status $null } } if ($watchPending) { $w = $script:watch if (-not $w.proc) { Print-Status $null; $watchPending = $false } elseif ($w.proc.HasExited) { $r = Read-Watch; Print-Status $r; $watchPending = $false } elseif ($w.lastStart -and ((Get-Date) - $w.lastStart).TotalSeconds -gt 60) { Start-Watch } # Start-Watch kills a reader older than 90 s } if (((Get-Date) - $lastUpload).TotalSeconds -ge 300) { Upload-Logs; $lastUpload = Get-Date } } } finally { [Console]::TreatControlCAsInput = $false if ($keepAwake) { [void]$keepAwake::SetThreadExecutionState([uint32]2147483648); Log 'released the keep-awake state' } # ES_CONTINUOUS alone clears it Log 'stopping the node' Stop-Node if ($script:watch.proc -and -not $script:watch.proc.HasExited) { & taskkill.exe /T /F /PID $script:watch.proc.Id 2>$null | Out-Null } Start-Sleep -Seconds 2 $last = $script:watch.last $lastTxt = if ($last) { "last reading at $($last.at.ToString('HH:mm:ss')): blocks $($last.blocks), headers $($last.headers), peers $($last.peers), DAA $($last.daa)" } else { 'no reading from the node in this run' } Log ('SUMMARY after ' + ((Get-Date) - $started).ToString('hh\:mm\:ss') + ": node started $($script:node.starts) time(s), restarts $($script:node.restarts), $($script:statusCount) status lines; $lastTxt") Remove-Item -Path $script:watch.log, $script:watch.err -ErrorAction SilentlyContinue Upload-Logs Log "the node's output is in $($script:node.log); the database stays in $appDir" }