proto-cuda/windows-node/: START-NODE.bat and start-node.ps1 (igneumd --devnet with --addpeer to the Mac, status line every 30 s through igneum-miner watch, keep-awake, random-delay restart, Ctrl+C), BUILD-NODE.bat and build-node.ps1 (builds on the PC from the src.zip snapshot; winget for Rustup, LLVM and protoc; MSVC default toolset), ALLOW-FIREWALL.bat and allow-firewall.ps1, README.txt, cross-build.sh (the mingw-w64 recipe that built igneumd.exe in 8 min 25 s; the exe ships with the three mingw runtime DLLs) and make-package.sh. WINDOWS-MINER.md gains "Run your own node"; bench-log records the cross-compile and the two-peer sync test on the Mac (ports 27000 and 27010, live node untouched). The mining launcher's NODE_HOST=auto edit stays uncommitted for the agent that owns start-mining.ps1. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
293 lines
16 KiB
PowerShell
293 lines
16 KiB
PowerShell
# 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=<Mac>, --nodnsseed, --disable-upnp and
|
|
# --nologfiles; the node's own output goes to node-<stamp>.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-<stamp>.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://<RPC_LISTEN>` run in the background, the same tool and
|
|
# line the mining launcher reads (vendor/igneum-node/igneum/miner/src/main.rs, watch):
|
|
# <unix> 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'
|
|
$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-NetConnection -ComputerName $macHost -Port ([int]$macPort) -WarningAction SilentlyContinue -InformationLevel Quiet } 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"
|
|
}
|