After PC 2's gfx1036 (prebuilt-generic path) completed 2,000 jobs a second with no hash from 600 s on: every OpenCL call in host.c's job path is now fatal on error (exit 3, the miner restarts the worker), the dispatch event must read CL_COMPLETE, a chunk 20x faster per nonce than the running mean or an output buffer unchanged since the previous dispatch is a fault, and a stats line every 200 jobs carries the live event and buffer counts (a leak over 16 events or 12 buffers is fatal too). IGNEUM_FAULT_TEST=N exercises the detectors on a healthy device (verified on Apple OpenCL: the stale-output guard fires on the chunk after the injected fault). The launcher shows 'worker fault' and 'restarting' for the card on the miner's WORKER FAULT line and drops the last rate. The emulation's NVRTC stand-in now applies NVRTC's execution-space rule (program.h(46) igneum_launch_* declarations are host code unless -default-device or -DIGNEUM_NO_CUDA), which is what the RTX 5090 reported; test.sh checks the rejection. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1422 lines
82 KiB
PowerShell
1422 lines
82 KiB
PowerShell
# Igneum for Windows: the shared module. Dot-sourced by start-igneum.ps1 (node and miners in one window),
|
|
# start-mining.ps1 (miners only) and start-node.ps1 (node only), so the logic lives once. 3 October 2026.
|
|
# Devnet v4, package 0.2.0 (4 October 2026): peers list (seed node and Mac), fresh appdir devnet-v4, one miner process
|
|
# with --identities per card, EVM payout address, finality voting on, hot swap through the worker's prepare command.
|
|
# Package 0.3.0 (4 October 2026): prebuilt one-click workers (igneum-worker-cuda.exe with NVRTC, igneum-worker-opencl.exe),
|
|
# nothing to install but the GPU driver; the nvcc / cl.exe build path stays as the fallback when a worker is missing.
|
|
# The caller sets $launcherName ('igneum', 'launcher' or 'node-launcher') before dot-sourcing; it names the launcher log.
|
|
#
|
|
# Sections: settings, logging and events, the node (igneumd), the chain reader (igneum-miner watch), GPUs and tools,
|
|
# pack and worker builds, the miners, log reading, states, the dashboard, uploads, keep-awake, the loop helpers.
|
|
#
|
|
# Lines this module reads:
|
|
# igneum-miner watch: <unix> node1 blocks=N headers=N daa=N tips=N peers=N difficulty=X sink=<12 hex> blue=N synced=true|false
|
|
# miner log: <unix> ACCEPTED block nonce=0x... | <unix> STATUS '<label>' [worker]: <up>s jobs=N accepted=N ...
|
|
# hash=X MH/s wall (Y MH/s inside jobs) [now=... (the v4 miner's interval rates)] template_age=Zs synced=true
|
|
# <unix> worker: ready cuda|opencl <device> ... prepare 1|0 | <unix> SEED CHANGE at daa ...
|
|
# miner stderr: template error: ... (the node is away: the miner retries every 200 ms but its gRPC client never
|
|
# reconnects, rpc/grpc/client/src/lib.rs GrpcClient::connect passes reconnect=false, so the
|
|
# launcher restarts the miners once the node is back and synced)
|
|
|
|
$ProgressPreference = 'SilentlyContinue' # no blue progress banners (Test-NetConnection paints one over the dashboard)
|
|
$ErrorActionPreference = 'Continue'
|
|
$root = $PSScriptRoot
|
|
Set-Location $root
|
|
if (-not $launcherName) { $launcherName = 'launcher' }
|
|
$packageVersion = '0.3.0'
|
|
$chainVersion = 'devnet v4'
|
|
|
|
# ---- settings (environment variables set by the bat; defaults match the bats) ---------------------------------------------
|
|
|
|
function Get-Setting([string]$name, [string]$default) {
|
|
$v = [Environment]::GetEnvironmentVariable($name)
|
|
if ($v) { return $v } else { return $default }
|
|
}
|
|
|
|
# node
|
|
$macNode = Get-Setting 'MAC_NODE' '192.168.68.64:26611'
|
|
# The peers igneumd dials with --addpeer (comma separated): the seed node first, then the Mac.
|
|
$peerNodes = @((Get-Setting 'PEER_NODES' "188.245.5.161:26611,$macNode") -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' })
|
|
$rpcListen = Get-Setting 'RPC_LISTEN' '127.0.0.1:26610'
|
|
$p2pListen = Get-Setting 'P2P_LISTEN' '0.0.0.0:26611'
|
|
$appDir = Get-Setting 'APPDIR' (Join-Path $env:LOCALAPPDATA 'igneum\devnet-v4')
|
|
$unsyncedMining = ($env:UNSYNCED_MINING -eq '1')
|
|
$extraArgs = $env:EXTRA_ARGS
|
|
$rpcHost = ($rpcListen -split ':')[0]
|
|
$rpcPort = ($rpcListen -split ':')[-1]
|
|
if ($rpcHost -eq '0.0.0.0' -or $rpcHost -eq '') { $rpcHost = '127.0.0.1' }
|
|
$p2pPort = ($p2pListen -split ':')[-1]
|
|
$macHost = ($macNode -split ':')[0]
|
|
$macPort = ($macNode -split ':')[-1]
|
|
|
|
# miners
|
|
$nodeHost = Get-Setting 'NODE_HOST' 'auto'
|
|
$nodePort = Get-Setting 'NODE_PORT' '26610'
|
|
$nodeUrl = ''
|
|
$payout = $env:PAYOUT_ADDRESS
|
|
$payoutEvm = $env:PAYOUT_EVM # 0x + 40 hex; empty = one address per vendor process, derived from the PC name
|
|
$vote = ($env:VOTE -ne '0') # finality voting is on unless VOTE=0
|
|
$nvidiaDevice = Get-Setting 'NVIDIA_DEVICE' '0'
|
|
$cudaArch = Get-Setting 'CUDA_ARCH' 'sm_120'
|
|
$workerArch = $env:WORKER_ARCH # prebuilt CUDA worker only: NVRTC target (sm_XY or compute_XY); empty = the card's own architecture
|
|
$amdVendor = Get-Setting 'AMD_VENDOR' 'Advanced Micro Devices'
|
|
$amdDevice = $env:AMD_DEVICE
|
|
$useNvidia = ($env:USE_NVIDIA -ne '0')
|
|
$useAmd = ($env:USE_AMD -ne '0')
|
|
$vcvars = Get-Setting 'VCVARS' 'C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvarsall.bat'
|
|
$vcvarsVer = Get-Setting 'VCVARS_VER' '14.30'
|
|
$minersPerVendor = 1
|
|
if ($env:MINERS -and [int]$env:MINERS -ge 1) { $minersPerVendor = [int]$env:MINERS }
|
|
# v4: one miner process per card serves every identity (--identities N). ONE_PROCESS_PER_IDENTITY=1 restores the old
|
|
# layout (N processes, N workers, about 1.3 GiB of GPU memory each).
|
|
$onePerIdentity = ($env:ONE_PROCESS_PER_IDENTITY -eq '1')
|
|
# Nonces per job, per vendor (a multiple of 32). Empty = the miner's default of 16,777,216. A job is one template, so a
|
|
# job should take well under a minute: a slow GPU with a long job mines a stale template and reports late.
|
|
$nvidiaJobNonces = $env:NVIDIA_JOB_NONCES
|
|
$amdJobNonces = Get-Setting 'AMD_JOB_NONCES' '2097152'
|
|
$plain = ($env:PLAIN -eq '1')
|
|
|
|
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
$machine = if ($env:MACHINE_NAME) { $env:MACHINE_NAME } else { $env:COMPUTERNAME } # MACHINE_NAME (bat setting) overrides the Windows name: two PCs cloned from one image share COMPUTERNAME and would share every vote key
|
|
$launcherLog = Join-Path $root "$launcherName-$stamp.log"
|
|
$uploader = Join-Path $root 'upload-log.bat'
|
|
$miner = Join-Path $root 'igneum-miner.exe'
|
|
$nodeExe = Join-Path $root 'igneumd.exe'
|
|
$packDir = Join-Path $root 'proto-cuda\packs\devnet'
|
|
# Where the v4 miner writes the pack of the NEXT program (--prepare-packs): the worker compiles it in the background and
|
|
# swaps at the epoch boundary with no exit (hot swap). One folder per epoch and day under here.
|
|
$preparePackDir = Join-Path $root 'proto-cuda\packs\prepare'
|
|
# The one-click workers (package 0.3.0, 4 October 2026): prebuilt exes next to igneum-miner.exe that need nothing but
|
|
# the GPU driver. igneum-worker-cuda.exe compiles each hourly program on the card itself with NVRTC (nvrtc64_*_0.dll
|
|
# and nvrtc-builtins64_*.dll next to it, NVIDIA's redistributable); igneum-worker-opencl.exe builds it through the
|
|
# OpenCL driver's own compiler. When a prebuilt worker is missing (or FORCE_BUILD=1), the nvcc / cl.exe build path of
|
|
# proto-cuda\build.bat and proto-opencl\build.bat is used as before, which needs CUDA Toolkit + Visual Studio.
|
|
$prebuiltCuda = Join-Path $root 'igneum-worker-cuda.exe'
|
|
$prebuiltCl = Join-Path $root 'igneum-worker-opencl.exe'
|
|
$packDirRel = 'proto-cuda\packs\devnet' # $packDir relative to $root, for the prebuilt worker's --pack (worker-args split on spaces)
|
|
$script:workerPath = @{} # per vendor: 'prebuilt' or 'build'
|
|
$script:tools = $null # the toolchain survey (Find-Toolchain), made only when the build path is needed
|
|
$script:lastSeeds = $null # the seeds of the last exported pack (Build-Worker's cache key)
|
|
$started = Get-Date
|
|
$gpuNames = @()
|
|
$vendors = @{} # $vendors[name] = @{ name; card; exe; instances; rebuilds; building; evm }
|
|
|
|
# The EVM payout address of a vendor's miner process: PAYOUT_EVM when set, else 20 bytes of SHA-256 over a fixed tag,
|
|
# the PC name and the vendor, so it is the same on every run of this PC and different per card. Printed in the log and
|
|
# the dashboard. (The v4 miner takes one --evm-address per process; with --identities every identity of that process
|
|
# pays there.)
|
|
function Get-EvmAddress([string]$vendorName) {
|
|
if ($payoutEvm) { return $payoutEvm.Trim().ToLower() }
|
|
$sha = [System.Security.Cryptography.SHA256]::Create()
|
|
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes("igneum-evm-payout-v0:${machine}:${vendorName}"))
|
|
$sha.Dispose()
|
|
return '0x' + (($bytes[0..19] | ForEach-Object { $_.ToString('x2') }) -join '')
|
|
}
|
|
if ($payoutEvm -and $payoutEvm.Trim() -notmatch '^0x[0-9a-fA-F]{40}$') { Write-Host "PAYOUT_EVM must be 0x followed by 40 hex characters (got '$payoutEvm')"; exit 1 }
|
|
|
|
# The node target the miners and the chain reader dial. 'auto' = a node on this PC when one answers, else the Mac.
|
|
function Set-NodeTarget([string]$targetHost, [string]$targetPort) {
|
|
$script:nodeHost = $targetHost
|
|
$script:nodePort = $targetPort
|
|
$script:nodeUrl = "grpc://${targetHost}:${targetPort}"
|
|
}
|
|
|
|
function Test-TcpPort([string]$targetHost, [int]$port, [int]$ms) {
|
|
$ok = $false
|
|
try {
|
|
$c = New-Object Net.Sockets.TcpClient
|
|
$r = $c.BeginConnect($targetHost, $port, $null, $null)
|
|
if ($r.AsyncWaitHandle.WaitOne($ms) -and $c.Connected) { $ok = $true }
|
|
$c.Close()
|
|
} catch { }
|
|
return $ok
|
|
}
|
|
|
|
function Resolve-NodeHost {
|
|
if ($script:nodeHost -ne 'auto') { Set-NodeTarget $script:nodeHost $script:nodePort; return '' }
|
|
if (Test-TcpPort '127.0.0.1' ([int]$script:nodePort) 1500) { Set-NodeTarget '127.0.0.1' $script:nodePort; return ' (NODE_HOST=auto: using the node on this PC)' }
|
|
Set-NodeTarget '192.168.68.64' $script:nodePort
|
|
return " (NODE_HOST=auto: nothing listens on 127.0.0.1:$($script:nodePort), using the Mac node)"
|
|
}
|
|
|
|
# ---- logging and events -------------------------------------------------------------------------------------------------
|
|
|
|
# Dashboard state. Functions set these through $script: so that the assignments reach the script scope.
|
|
$script:dashOn = $false # true while the window shows the dashboard; Log then writes to the file only
|
|
$script:dashW = 0 # console width the last frame was drawn for (0 = clear the screen first)
|
|
$script:dashRows = 0 # rows the last frame used
|
|
$script:tick = 0 # frame counter, drives the heartbeat marker
|
|
$script:events = New-Object System.Collections.ArrayList
|
|
$script:uploadOk = $true
|
|
$script:phase = '' # one line under the header while the miners are not running yet
|
|
$script:title = 'Igneum devnet'
|
|
|
|
function Log([string]$msg) {
|
|
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $msg"
|
|
if (-not $script:dashOn) { Write-Host $line }
|
|
Add-Content -Path $launcherLog -Value $line
|
|
}
|
|
|
|
# An event is one line in the dashboard's "last events" list, and a line in the launcher log.
|
|
# kind: found (ember), build (yellow), ok (green), error (red), info (grey)
|
|
function Add-Event([string]$text, [string]$kind) {
|
|
[void]$script:events.Add(@{ at = (Get-Date); first = (Get-Date); text = $text; kind = $kind; card = ''; count = 1 })
|
|
while ($script:events.Count -gt 60) { $script:events.RemoveAt(0) }
|
|
Log "event: $text"
|
|
}
|
|
|
|
# Found blocks arrive many times a minute on a fast card, so they are folded into one line per card per minute.
|
|
function Add-FoundEvent([string]$card, [datetime]$at) {
|
|
$last = $null
|
|
for ($i = $script:events.Count - 1; $i -ge 0; $i--) {
|
|
if ($script:events[$i].kind -eq 'found' -and $script:events[$i].card -eq $card) { $last = $script:events[$i]; break }
|
|
}
|
|
if ($last -and ($at - $last.first).TotalSeconds -lt 60 -and ($at - $last.first).TotalSeconds -ge 0) {
|
|
$last.count += 1
|
|
$last.at = $at
|
|
$last.text = "$card found $($last.count) blocks in the last minute"
|
|
return
|
|
}
|
|
[void]$script:events.Add(@{ at = $at; first = $at; text = "$card found a block"; kind = 'found'; card = $card; count = 1 })
|
|
while ($script:events.Count -gt 60) { $script:events.RemoveAt(0) }
|
|
}
|
|
|
|
# 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 @() }
|
|
}
|
|
|
|
function Stop-Proc($p) {
|
|
if ($p -and -not $p.HasExited) { & taskkill.exe /T /F /PID $p.Id 2>$null | Out-Null }
|
|
}
|
|
|
|
function Get-ExitCode($inst) {
|
|
$code = $null
|
|
try { $code = $inst.proc.ExitCode } catch { }
|
|
if ($null -eq $code -and $inst.log -and (Test-Path $inst.log)) {
|
|
try {
|
|
$fs = New-Object IO.FileStream($inst.log, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
|
|
$len = $fs.Length; $n = [Math]::Min($len, 4096); $fs.Seek(-$n, [IO.SeekOrigin]::End) | Out-Null
|
|
$b = New-Object byte[] $n; [void]$fs.Read($b, 0, $n); $fs.Close()
|
|
$tail = [Text.Encoding]::UTF8.GetString($b)
|
|
if ($tail -match 'exiting with code (\d+)') { $code = [int]$Matches[1] }
|
|
elseif ($tail -match 'SEED CHANGE') { $code = 42 }
|
|
} catch { }
|
|
}
|
|
return $code
|
|
}
|
|
|
|
# ---- the node (igneumd) ----------------------------------------------------------------------------------------------------
|
|
# on = the dashboard shows the NODE card; managed = this window started igneumd (an external node is only watched).
|
|
|
|
$script:node = @{ on = $false; managed = $false; proc = $null; log = $null; err = $null; starts = 0; startedAt = $null; restartAt = $null;
|
|
restarts = 0; exitCode = $null; synced = $false; syncedAt = $null; gaveUp = $false; quickExits = 0; lastPeers = $null;
|
|
answered = $false; minersNeedRestart = $false; firstStartAt = $null }
|
|
|
|
# Start-Process joins the argument list with spaces and does not quote, so values that may contain spaces are quoted here.
|
|
function Get-NodeArgs {
|
|
# --yes answers the database-version question so the node never waits on a console nobody sees.
|
|
$a = @('--devnet', ('"--appdir=' + $appDir + '"'), "--rpclisten=$rpcListen", "--listen=$p2pListen")
|
|
foreach ($p in $peerNodes) { $a += "--addpeer=$p" }
|
|
$a += @('--nodnsseed', '--disable-upnp', '--nologfiles', '--yes')
|
|
if ($unsyncedMining) { $a += '--enable-unsynced-mining' }
|
|
if ($extraArgs) { $a += ($extraArgs -split '\s+' | Where-Object { $_ -ne '' }) }
|
|
return $a
|
|
}
|
|
|
|
# Checks before the node starts. Returns 'external' when a node already answers on the RPC port, 'free' when the ports
|
|
# are free, or an error text.
|
|
function Test-NodePorts {
|
|
if (Test-TcpPort $rpcHost ([int]$rpcPort) 1500) { return 'external' }
|
|
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 { }
|
|
return "port $port is already in use by $owner (pid $($busy[0].OwningProcess)) and it is not an Igneum RPC. Close it first."
|
|
}
|
|
} catch { }
|
|
}
|
|
return 'free'
|
|
}
|
|
|
|
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.answered = $false
|
|
$n.synced = $false
|
|
$nodeArgs = Get-NodeArgs
|
|
$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
|
|
if (-not $n.firstStartAt) { $n.firstStartAt = $n.startedAt }
|
|
Log "igneumd started (pid $($n.proc.Id)): `"$nodeExe`" $($nodeArgs -join ' ') -> $($n.log)"
|
|
if ($n.starts -eq 1) { Add-Event 'node started' 'ok' } else { Add-Event "node restarted (restart $($n.restarts))" 'info' }
|
|
}
|
|
|
|
function Stop-Node {
|
|
$n = $script:node
|
|
if ($n.managed) { Stop-Proc $n.proc }
|
|
}
|
|
|
|
# Crash handling: a random delay of 5 to 60 s before the restart; three exits in a row within 20 s of a start = give up.
|
|
function Update-Node {
|
|
$n = $script:node
|
|
if (-not $n.managed -or $n.gaveUp) { return }
|
|
if ($n.proc -and $n.proc.HasExited -and -not $n.restartAt) {
|
|
$n.restarts += 1
|
|
$n.exitCode = $null
|
|
try { $n.exitCode = $n.proc.ExitCode } catch { }
|
|
$life = [int]((Get-Date) - $n.startedAt).TotalSeconds
|
|
if ($life -lt 20) { $n.quickExits += 1 } else { $n.quickExits = 0 }
|
|
foreach ($l in (Get-TailLines $n.err 5)) { Log " node stderr: $l" }
|
|
foreach ($l in (Get-TailLines $n.log 5)) { Log " node stdout: $l" }
|
|
if ($n.quickExits -ge 3) {
|
|
$n.gaveUp = $true
|
|
Add-Event "the node exits at once (code $($n.exitCode)), giving up: see $(Split-Path -Leaf $n.log); a database from an older build? delete $appDir" 'error'
|
|
return
|
|
}
|
|
$delay = Get-Random -Minimum 5 -Maximum 61
|
|
$n.restartAt = (Get-Date).AddSeconds($delay)
|
|
Add-Event "node stopped (code $($n.exitCode)) after $life s, restarting in $delay s" 'error'
|
|
}
|
|
if ($n.restartAt -and (Get-Date) -ge $n.restartAt) { Start-Node }
|
|
}
|
|
|
|
# One short state for the NODE card. The colour follows the state.
|
|
function Get-NodeState {
|
|
$n = $script:node
|
|
$last = $script:chain.last
|
|
if ($n.managed) {
|
|
if ($n.gaveUp) { return @{ text = 'stopped'; color = 'Red' } }
|
|
if ($n.restartAt) { return @{ text = 'restarting'; color = 'Yellow' } }
|
|
if (-not $n.proc -or $n.proc.HasExited) { return @{ text = 'stopped'; color = 'Red' } }
|
|
if (-not $n.answered) { return @{ text = 'starting'; color = 'Yellow' } }
|
|
} else {
|
|
if (-not $script:chain.ok) { return @{ text = 'not answering'; color = 'Red' } }
|
|
if (-not $last) { return @{ text = 'starting'; color = 'Yellow' } }
|
|
}
|
|
if ($n.synced) { return @{ text = 'synced'; color = 'Green' } }
|
|
return @{ text = 'syncing'; color = 'Yellow' }
|
|
}
|
|
|
|
function Get-NodeUptime {
|
|
$n = $script:node
|
|
if ($n.managed -and $n.startedAt -and $n.proc -and -not $n.proc.HasExited) { return ((Get-Date) - $n.startedAt).ToString('hh\:mm\:ss') }
|
|
return 'n/a'
|
|
}
|
|
|
|
# ---- the chain, read with igneum-miner watch (one reading per interval, in the background) ---------------------------------
|
|
# watch 1 <url> prints one line at once and exits about 10 s later; the line is read as soon as it is in the file and the
|
|
# process is then ended, so the next reading can start sooner than that (5 s while the node syncs, 30 s afterwards).
|
|
|
|
$script:chain = @{ proc = $null; log = (Join-Path $root "chain-$stamp.log"); err = (Join-Path $root "chain-$stamp.log.err");
|
|
samples = (New-Object System.Collections.ArrayList); lastStart = $started; ok = $true; fails = 0; last = $null; consumed = $false }
|
|
|
|
function Get-ChainInterval {
|
|
if ($script:node.on -and -not $script:node.synced) { return 5 }
|
|
return 30
|
|
}
|
|
|
|
function Start-ChainSample {
|
|
$c = $script:chain
|
|
if (-not (Test-Path $miner)) { return }
|
|
if ($c.proc -and -not $c.proc.HasExited) {
|
|
if (((Get-Date) - $c.lastStart).TotalSeconds -gt 90) { Stop-Proc $c.proc; $c.proc = $null; $c.fails += 1 }
|
|
else { return }
|
|
}
|
|
try {
|
|
$c.proc = Start-Process -FilePath $miner -ArgumentList @('watch', '1', $nodeUrl) -WorkingDirectory $root -RedirectStandardOutput $c.log -RedirectStandardError $c.err -NoNewWindow -PassThru
|
|
if ($c.proc) { $null = $c.proc.Handle }
|
|
$c.lastStart = Get-Date
|
|
$c.consumed = $false
|
|
} catch { $c.proc = $null; $c.fails += 1 }
|
|
}
|
|
|
|
function Read-ChainSample {
|
|
$c = $script:chain
|
|
if (-not $c.proc) { return }
|
|
$line = $null
|
|
if (-not $c.consumed) {
|
|
try {
|
|
$text = ''
|
|
$fs = New-Object IO.FileStream($c.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 = $c.lastStart; 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 }) }
|
|
$c.consumed = $true
|
|
Stop-Proc $c.proc
|
|
$c.proc = $null
|
|
[void]$c.samples.Add($r)
|
|
while ($c.samples.Count -gt 0 -and ((Get-Date) - $c.samples[0].at).TotalSeconds -gt 1800) { $c.samples.RemoveAt(0) }
|
|
$c.last = $r
|
|
if (-not $c.ok) { $c.ok = $true; Add-Event 'the node answers again' 'ok' }
|
|
$c.fails = 0
|
|
Update-NodeFromSample $r
|
|
return
|
|
}
|
|
if ($c.proc.HasExited) {
|
|
$c.proc = $null
|
|
if (-not $c.consumed) {
|
|
$c.fails += 1
|
|
if ($c.ok -and $c.fails -ge 2) {
|
|
$c.ok = $false
|
|
$who = if ($script:node.managed) { 'is igneumd still starting, or did it crash?' } elseif ($nodeHost -eq '127.0.0.1') { 'is the node on this PC running?' } else { 'is the Mac node running?' }
|
|
Add-Event "the node did not answer twice in a row ($who)" 'error'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# What a fresh reading means for the node card and the events: synced, peers joined or left, back after a restart.
|
|
function Update-NodeFromSample($r) {
|
|
$n = $script:node
|
|
if (-not $n.on) { return }
|
|
$n.answered = $true
|
|
# synced: from the node's own flag; an older igneum-miner.exe without synced= counts the node as synced once it has
|
|
# a peer and its blocks have caught up with its headers.
|
|
$synced = if ($null -ne $r.synced) { $r.synced } else { ($r.peers -gt 0 -and $r.blocks -ge $r.headers) }
|
|
if ($unsyncedMining) { $synced = $true }
|
|
if ($synced -and -not $n.synced) {
|
|
$n.synced = $true
|
|
$n.syncedAt = Get-Date
|
|
$since = if ($n.managed -and $n.startedAt) { $n.startedAt } else { $started }
|
|
Add-Event ("node synced in {0:N0} s ({1:N0} blocks)" -f ((Get-Date) - $since).TotalSeconds, $r.blocks) 'ok'
|
|
if ($n.managed -and $n.starts -gt 1) { $n.minersNeedRestart = $true }
|
|
} elseif (-not $synced -and $n.synced) {
|
|
$n.synced = $false
|
|
Add-Event 'node lost sync (blocks behind its headers)' 'info'
|
|
}
|
|
if ($null -ne $n.lastPeers -and $r.peers -ne $n.lastPeers) {
|
|
if ($r.peers -gt $n.lastPeers) { Add-Event "peer joined ($($r.peers) peers)" 'ok' } else { Add-Event "peer left ($($r.peers) peers)" 'info' }
|
|
}
|
|
$n.lastPeers = $r.peers
|
|
}
|
|
|
|
# Network rate over the last ~60 s, difficulty trend over ~5 min, and our share of the last 100 network blocks
|
|
# (our accepted blocks in the time the network took to add its last 100). Null until two readings exist.
|
|
function Get-ChainStats {
|
|
$s = $script:chain.samples
|
|
if ($s.Count -lt 2) { return $null }
|
|
$last = $s[$s.Count - 1]
|
|
$ref = $s[$s.Count - 2]
|
|
for ($i = $s.Count - 2; $i -ge 0; $i--) { if (($last.at - $s[$i].at).TotalSeconds -le 90) { $ref = $s[$i] } else { break } }
|
|
$dt = ($last.at - $ref.at).TotalSeconds
|
|
$rate = if ($dt -gt 0) { ($last.blocks - $ref.blocks) / $dt } else { 0.0 }
|
|
$old = $s[0]
|
|
for ($i = 0; $i -lt $s.Count; $i++) { if (($last.at - $s[$i].at).TotalSeconds -le 300) { $old = $s[$i]; break } }
|
|
$trend = 'steady'
|
|
if ($old.difficulty -gt 0 -and $old -ne $last) {
|
|
$r = $last.difficulty / $old.difficulty
|
|
if ($r -gt 1.02) { $trend = 'rising' } elseif ($r -lt 0.98) { $trend = 'falling' }
|
|
}
|
|
$share = $null
|
|
$base = $null
|
|
for ($i = $s.Count - 1; $i -ge 0; $i--) { if ($s[$i].blocks -le ($last.blocks - 100)) { $base = $s[$i]; break } }
|
|
if ($base) {
|
|
$span = $last.blocks - $base.blocks
|
|
$t0 = [double]([DateTimeOffset]$base.at).ToUnixTimeMilliseconds() / 1000.0
|
|
$t1 = [double]([DateTimeOffset]$last.at).ToUnixTimeMilliseconds() / 1000.0
|
|
$ours = 0
|
|
foreach ($v in $vendors.Values) { foreach ($inst in $v.instances) { foreach ($t in $inst.found) { if ([double]$t -gt $t0 -and [double]$t -le $t1) { $ours++ } } } }
|
|
if ($span -gt 0) { $share = [Math]::Min(100.0, 100.0 * $ours / $span) }
|
|
}
|
|
return @{ rate = $rate; trend = $trend; share = $share; difficulty = $last.difficulty; blocks = $last.blocks }
|
|
}
|
|
|
|
# ---- GPUs and tools -----------------------------------------------------------------------------------------------------
|
|
|
|
# The short card name shown on the dashboard: "NVIDIA GeForce RTX 5090" -> "RTX 5090", "AMD Radeon(TM) Graphics" -> "Radeon Graphics"
|
|
function Get-CardName([string]$vendor) {
|
|
$pattern = if ($vendor -eq 'nvidia') { 'NVIDIA' } else { 'AMD|Radeon' }
|
|
$n = $gpuNames | Where-Object { $_ -match $pattern } | Select-Object -First 1
|
|
if (-not $n) { if ($vendor -eq 'nvidia') { return 'NVIDIA GPU' } else { return 'AMD GPU' } }
|
|
$n = $n -replace '\(TM\)|\(R\)', ''
|
|
$n = $n -replace '^NVIDIA GeForce ', '' -replace '^NVIDIA ', '' -replace '^AMD ', ''
|
|
$n = $n -replace '\s+', ' '
|
|
return $n.Trim()
|
|
}
|
|
|
|
function Find-VcVarsAll {
|
|
if ($vcvars -and (Test-Path $vcvars)) { return $vcvars }
|
|
foreach ($pf in @('C:\Program Files', 'C:\Program Files (x86)')) {
|
|
$hits = Get-ChildItem -Path (Join-Path $pf 'Microsoft Visual Studio') -Recurse -Filter 'vcvarsall.bat' -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.FullName -match '\\VC\\Auxiliary\\Build\\vcvarsall\.bat$' } | Sort-Object FullName -Descending
|
|
if ($hits) { return $hits[0].FullName }
|
|
}
|
|
return $null
|
|
}
|
|
|
|
# Imports the MSVC toolset environment (PATH, LIB, INCLUDE, ...) into this process: cmd runs vcvarsall and prints `set`.
|
|
# Always done, even when cl.exe is already on PATH: a prompt can have cl without LIB, and then nvcc's link step fails
|
|
# with LNK1104 LIBCMT.lib (seen on the first Windows run, 3 Oct 2026).
|
|
function Import-VcVars {
|
|
$bat = Find-VcVarsAll
|
|
if (-not $bat) { Log "vcvarsall.bat not found under Program Files\Microsoft Visual Studio; looked for $vcvars"; return $false }
|
|
Log "importing the MSVC environment: `"$bat`" x64 -vcvars_ver=$vcvarsVer"
|
|
# cmd /s: always strip the first and last quote of the /c string, so the quoted path inside is left intact
|
|
$out = cmd /s /c "`"$bat`" x64 -vcvars_ver=$vcvarsVer >nul 2>&1 && set" 2>$null
|
|
if (-not $out) {
|
|
Log "vcvarsall failed for -vcvars_ver=$vcvarsVer; trying without the version (CUDA 12.8 may then refuse the host compiler)"
|
|
$out = cmd /s /c "`"$bat`" x64 >nul 2>&1 && set" 2>$null
|
|
}
|
|
if (-not $out) { return $false }
|
|
foreach ($l in $out) {
|
|
$i = $l.IndexOf('=')
|
|
if ($i -gt 0) { [Environment]::SetEnvironmentVariable($l.Substring(0, $i), $l.Substring($i + 1), 'Process') }
|
|
}
|
|
return [bool](Get-Command cl.exe -ErrorAction SilentlyContinue)
|
|
}
|
|
|
|
# Fallback when vcvarsall is unusable: LIB and INCLUDE from the newest MSVC toolset and Windows Kits, as proto-opencl/build.bat does.
|
|
function Set-LibFallback {
|
|
$vsRoots = @()
|
|
foreach ($pf in @('C:\Program Files', 'C:\Program Files (x86)')) { $vsRoots += Get-ChildItem -Path (Join-Path $pf 'Microsoft Visual Studio') -Directory -Recurse -Depth 6 -Filter 'MSVC' -ErrorAction SilentlyContinue }
|
|
$msvc = $null
|
|
foreach ($r in $vsRoots) {
|
|
# Prefer the toolset CUDA accepts (vcvarsVer, 14.30), then the newest one that has the libraries.
|
|
$tools = Get-ChildItem -Path $r.FullName -Directory -ErrorAction SilentlyContinue | Sort-Object @{Expression={ if ($_.Name.StartsWith($vcvarsVer)) { 0 } else { 1 } }}, @{Expression='Name'; Descending=$true}
|
|
foreach ($t in $tools) { if (Test-Path (Join-Path $t.FullName 'lib\x64\libcmt.lib')) { $msvc = $t.FullName; break } }
|
|
if ($msvc) { break }
|
|
}
|
|
$kits = 'C:\Program Files (x86)\Windows Kits\10'
|
|
$sdk = $null
|
|
$vers = Get-ChildItem -Path (Join-Path $kits 'Lib') -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending
|
|
foreach ($v in $vers) { if (Test-Path (Join-Path $v.FullName 'ucrt\x64\libucrt.lib')) { $sdk = $v.Name; break } }
|
|
if (-not $msvc -or -not $sdk) { Log "fallback: MSVC lib dir $(if ($msvc) { $msvc } else { 'not found' }), Windows SDK $(if ($sdk) { $sdk } else { 'not found' })"; return $false }
|
|
$env:LIB = "$msvc\lib\x64;$kits\Lib\$sdk\ucrt\x64;$kits\Lib\$sdk\um\x64;" + $env:LIB
|
|
$env:INCLUDE = "$msvc\include;$kits\Include\$sdk\ucrt;$kits\Include\$sdk\um;$kits\Include\$sdk\shared;" + $env:INCLUDE
|
|
if (-not (Get-Command cl.exe -ErrorAction SilentlyContinue)) { $env:PATH = "$msvc\bin\Hostx64\x64;" + $env:PATH } # never shadow an already loaded (14.30) cl.exe
|
|
Log "fallback MSVC environment set from $msvc and Windows SDK $sdk"
|
|
return [bool](Get-Command cl.exe -ErrorAction SilentlyContinue)
|
|
}
|
|
|
|
# The toolchain survey for the build path (MSVC environment, nvcc, an OpenCL SDK). Made once, only when a worker has to
|
|
# be built here: with the prebuilt workers present nothing is looked for. Returns @{ cl; nvcc; openclSdk }.
|
|
function Find-Toolchain {
|
|
if ($script:tools) { return $script:tools }
|
|
$haveCl = Import-VcVars
|
|
if (-not $haveCl) { $haveCl = Set-LibFallback }
|
|
if ($haveCl) {
|
|
$libFirst = if ($env:LIB) { ($env:LIB -split ';')[0] } else { 'EMPTY' }
|
|
$incFirst = if ($env:INCLUDE) { ($env:INCLUDE -split ';')[0] } else { 'EMPTY' }
|
|
Log "MSVC environment: cl at $((Get-Command cl.exe).Source); LIB starts with $libFirst; INCLUDE starts with $incFirst"
|
|
# The fallback only when the import left nothing: it would otherwise put the newest toolset's headers in front of
|
|
# INCLUDE and CUDA 12.8's cudafe++ crashes on those. The library path for nvcc's link step comes from build.bat itself.
|
|
if (-not $env:LIB) { Log 'LIB is empty: trying the fallback'; [void](Set-LibFallback) }
|
|
}
|
|
$haveNvcc = [bool](Get-Command nvcc.exe -ErrorAction SilentlyContinue)
|
|
$openclSdk = $null
|
|
if ($env:OPENCL_SDK -and (Test-Path (Join-Path $env:OPENCL_SDK 'include\CL\cl.h'))) { $openclSdk = $env:OPENCL_SDK }
|
|
elseif ($env:CUDA_PATH -and (Test-Path (Join-Path $env:CUDA_PATH 'include\CL\cl.h'))) { $openclSdk = $env:CUDA_PATH }
|
|
elseif ($env:OCL_ROOT -and (Test-Path (Join-Path $env:OCL_ROOT 'include\CL\cl.h'))) { $openclSdk = $env:OCL_ROOT }
|
|
Log ("tools: nvcc " + $(if ($haveNvcc) { 'found' } else { 'MISSING (install CUDA Toolkit 12.8 or newer)' }) +
|
|
", MSVC $vcvarsVer " + $(if ($haveCl) { 'found' } else { "MISSING (install Visual Studio with the MSVC v143 x64 component; looked at $vcvars)" }) +
|
|
", OpenCL SDK " + $(if ($openclSdk) { "found at $openclSdk" } else { 'MISSING (the CUDA Toolkit ships one, or set OPENCL_SDK)' }))
|
|
$script:tools = @{ cl = $haveCl; nvcc = $haveNvcc; openclSdk = $openclSdk }
|
|
return $script:tools
|
|
}
|
|
|
|
function Test-CanBuild([string]$name) {
|
|
$t = Find-Toolchain
|
|
if ($name -eq 'nvidia') { return ($t.nvcc -and $t.cl) }
|
|
return ($t.cl -and [bool]$t.openclSdk)
|
|
}
|
|
|
|
# What the dashboard and the log say about a vendor's worker
|
|
function Get-PathText([string]$name) {
|
|
if ($script:workerPath[$name] -eq 'prebuilt') {
|
|
if ($name -eq 'nvidia') { return 'prebuilt worker, NVRTC compiles on the card (driver only)' }
|
|
return 'prebuilt worker, the OpenCL driver compiles (driver only)'
|
|
}
|
|
if ($name -eq 'nvidia') { return 'worker built here with nvcc + MSVC' }
|
|
return 'worker built here with cl.exe + an OpenCL SDK'
|
|
}
|
|
|
|
# Lists the GPUs, chooses the prebuilt worker per vendor (or the build path when it is missing and a toolchain exists),
|
|
# logs which workers will start. Returns the vendor plan (@('nvidia', 'amd'), or empty).
|
|
function Find-Hardware {
|
|
$script:gpuNames = @()
|
|
try { $script:gpuNames = @(Get-CimInstance Win32_VideoController | ForEach-Object { $_.Name }) } catch { Log "could not list GPUs through WMI: $_" }
|
|
Log ("GPUs found: " + ($(if ($gpuNames.Count) { $gpuNames -join '; ' } else { 'none reported' })))
|
|
$hasNvidia = ($gpuNames | Where-Object { $_ -match 'NVIDIA' }).Count -gt 0
|
|
$hasAmd = ($gpuNames | Where-Object { $_ -match 'AMD|Radeon' }).Count -gt 0
|
|
$forceBuild = ($env:FORCE_BUILD -eq '1')
|
|
|
|
$nvrtcDlls = @(Get-ChildItem -Path $root -Filter 'nvrtc64_*_0.dll' -ErrorAction SilentlyContinue | Where-Object { $_.Name -notmatch '\.alt\.' })
|
|
$havePrebuiltCuda = (Test-Path $prebuiltCuda) -and ($nvrtcDlls.Count -gt 0)
|
|
$havePrebuiltCl = Test-Path $prebuiltCl
|
|
if ((Test-Path $prebuiltCuda) -and -not $havePrebuiltCuda) { Log 'igneum-worker-cuda.exe is here but no nvrtc64_*_0.dll is next to it: the prebuilt CUDA worker cannot run (extract the whole zip)' }
|
|
Log ("prebuilt workers: " + $(if ($havePrebuiltCuda) { "igneum-worker-cuda.exe with $($nvrtcDlls[0].Name)" } else { 'no CUDA worker' }) + ", " + $(if ($havePrebuiltCl) { 'igneum-worker-opencl.exe' } else { 'no OpenCL worker' }) + $(if ($forceBuild) { ' (FORCE_BUILD=1: not used)' } else { '' }))
|
|
|
|
$plan = @()
|
|
$skipped = @()
|
|
if ($hasNvidia -and $useNvidia) {
|
|
if ($havePrebuiltCuda -and -not $forceBuild) { $plan += 'nvidia'; $script:workerPath['nvidia'] = 'prebuilt' }
|
|
elseif (Test-CanBuild 'nvidia') { $plan += 'nvidia'; $script:workerPath['nvidia'] = 'build' }
|
|
else { $skipped += "nvidia: GPU present, no prebuilt igneum-worker-cuda.exe (with nvrtc64_*_0.dll) in this folder, and " + $(if (-not $script:tools.nvcc) { 'nvcc' } else { 'MSVC' }) + " is missing for the build path" }
|
|
} elseif ($hasNvidia) { $skipped += 'nvidia: disabled by USE_NVIDIA=0' } else { $skipped += 'nvidia: no NVIDIA GPU reported' }
|
|
if ($hasAmd -and $useAmd) {
|
|
if ($havePrebuiltCl -and -not $forceBuild) { $plan += 'amd'; $script:workerPath['amd'] = 'prebuilt' }
|
|
elseif (Test-CanBuild 'amd') { $plan += 'amd'; $script:workerPath['amd'] = 'build' }
|
|
else { $skipped += "amd: GPU present, no prebuilt igneum-worker-opencl.exe in this folder, and " + $(if (-not $script:tools.cl) { 'MSVC' } else { 'an OpenCL SDK' }) + " is missing for the build path" }
|
|
} elseif ($hasAmd) { $skipped += 'amd: disabled by USE_AMD=0' } else { $skipped += 'amd: no AMD GPU reported' }
|
|
|
|
Log ("workers to start: " + $(if ($plan.Count) { ($plan | ForEach-Object { "$_ ($(Get-PathText $_))" }) -join ', ' } else { 'none' }))
|
|
foreach ($s in $skipped) { Log "skipped $s" }
|
|
return ,$plan
|
|
}
|
|
|
|
# igneum-miner.exe is shipped prebuilt; built with cargo only if missing. Returns $true when it is there.
|
|
function Confirm-Miner {
|
|
if (Test-Path $miner) { return $true }
|
|
Log 'igneum-miner.exe is not in the package; building it from src\igneum-node (needs Rust)'
|
|
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
|
|
Log 'cargo not found: installing rustup with winget (silent)'
|
|
winget install --id Rustlang.Rustup -e --silent --accept-package-agreements --accept-source-agreements | Out-Null
|
|
$env:PATH = "$env:USERPROFILE\.cargo\bin;$env:PATH"
|
|
}
|
|
if (-not (Test-Path (Join-Path $root 'src\igneum-node\Cargo.toml'))) { Log 'no src\igneum-node in the package either; see WINDOWS-MINER.md'; return $false }
|
|
Push-Location (Join-Path $root 'src\igneum-node')
|
|
cargo build --release -p igneum-miner 2>&1 | ForEach-Object { Add-Content -Path $launcherLog -Value $_ }
|
|
Pop-Location
|
|
Copy-Item (Join-Path $root 'src\igneum-node\target\release\igneum-miner.exe') $miner -ErrorAction SilentlyContinue
|
|
if (-not (Test-Path $miner)) { Log 'the miner did not build; see the launcher log'; return $false }
|
|
return $true
|
|
}
|
|
|
|
# ---- pack and worker builds -----------------------------------------------------------------------------------------------
|
|
|
|
function Export-Pack {
|
|
$t = Get-Date
|
|
New-Item -ItemType Directory -Force -Path $packDir | Out-Null
|
|
$out = & $miner export-pack $nodeUrl $packDir 2>&1
|
|
$out | ForEach-Object { Add-Content -Path $launcherLog -Value " export-pack: $_" }
|
|
$seeds = Join-Path $packDir 'seeds.txt'
|
|
if (-not (Test-Path $seeds)) { Log 'export-pack wrote no seeds.txt (is the node reachable?)'; return $null }
|
|
$txt = (Get-Content $seeds) -join ' | '
|
|
Log ("pack exported in {0:N1} s: {1}" -f ((Get-Date) - $t).TotalSeconds, $txt)
|
|
return (Get-Content $seeds -Raw)
|
|
}
|
|
|
|
function Build-Worker([string]$name, [string]$seeds) {
|
|
if ($script:workerPath[$name] -eq 'prebuilt') {
|
|
$exe = if ($name -eq 'nvidia') { $prebuiltCuda } else { $prebuiltCl }
|
|
Log "$name worker: prebuilt $exe serves the pack in $packDir (nothing to build; it compiles the program on the card)"
|
|
return $exe
|
|
}
|
|
if ($name -eq 'nvidia') { $exe = Join-Path $root 'proto-cuda\igneum-bench-cuda-devnet.exe'; $dir = Join-Path $root 'proto-cuda'; $cmd = "build.bat devnet $cudaArch" }
|
|
else { $exe = Join-Path $root 'proto-opencl\igneum-bench-cl-devnet.exe'; $dir = Join-Path $root 'proto-opencl'; $cmd = 'build.bat devnet' }
|
|
$stampFile = "$exe.seeds"
|
|
if ((Test-Path $exe) -and (Test-Path $stampFile) -and ((Get-Content $stampFile -Raw) -eq $seeds)) {
|
|
Log "$name worker already built for this epoch and day (cached)"
|
|
return $exe
|
|
}
|
|
$t = Get-Date
|
|
Log "building the $name worker from the pack ($cmd)"
|
|
Push-Location $dir
|
|
$out = cmd /s /c "$cmd" 2>&1
|
|
$rc = $LASTEXITCODE
|
|
Pop-Location
|
|
$out | ForEach-Object { Add-Content -Path $launcherLog -Value " $name build: $_" }
|
|
if ($rc -ne 0 -or -not (Test-Path $exe)) { Log "$name worker build FAILED (exit $rc); the full output is in $launcherLog"; return $null }
|
|
Set-Content -Path $stampFile -Value $seeds -NoNewline
|
|
Log ("{0} worker built in {1:N1} s ({2})" -f $name, ((Get-Date) - $t).TotalSeconds, $exe)
|
|
return $exe
|
|
}
|
|
|
|
# Exports the pack, builds the workers of the plan and fills $vendors. Returns $true when at least one vendor is ready.
|
|
# an instance = @{ vendor; index; label; file; log; err; runId; proc; restarts; starts; startedAt; restartAt; exitCode;
|
|
# logPos; logRem; errPos; errRem (incremental log reading); accepted; found (unix times of accepted
|
|
# blocks, last hour); status (the last STATUS line, parsed); ready; readyAt; lastLineAt; errors;
|
|
# lastError; lastErrorAt; rejected; lastNoNodeAt (a "template error" line: the node is away) }
|
|
function Initialize-Vendors($plan) {
|
|
$script:phase = 'exporting the program pack from the node'
|
|
Draw-Dashboard
|
|
New-Item -ItemType Directory -Force -Path $preparePackDir | Out-Null
|
|
$seeds = Export-Pack
|
|
if (-not $seeds) { return $false }
|
|
$script:lastSeeds = $seeds
|
|
foreach ($name in $plan) {
|
|
$script:phase = if ($script:workerPath[$name] -eq 'prebuilt') { "starting the prebuilt $(Get-CardName $name) worker" } else { "building the $(Get-CardName $name) worker (about 30 s for CUDA)" }
|
|
Draw-Dashboard
|
|
$exe = Build-Worker $name $seeds
|
|
if (-not $exe) { Log "$name skipped: no worker binary"; continue }
|
|
$instances = @()
|
|
# v4 default: ONE process per vendor carries every identity (--identities N, label <vendor>-<PC>, identities
|
|
# <vendor>-<PC>-1..N inside the miner). ONE_PROCESS_PER_IDENTITY=1: N processes, labels <vendor>-<PC>-<i>.
|
|
$procCount = if ($onePerIdentity) { $minersPerVendor } else { 1 }
|
|
$identitiesPerProc = if ($onePerIdentity) { 1 } else { $minersPerVendor }
|
|
for ($i = 1; $i -le $procCount; $i++) {
|
|
$suffix = if ($procCount -gt 1) { "-$i" } else { '' }
|
|
$instances += @{ vendor = $name; index = $i; label = "$name-$machine$suffix"; file = "$name$suffix-$stamp"; log = $null; err = $null;
|
|
runId = "$name-$machine$suffix-$stamp"; proc = $null; restarts = 0; starts = 0; startedAt = $null; restartAt = $null; exitCode = $null;
|
|
logPos = [long]0; logRem = ''; errPos = [long]0; errRem = ''; accepted = 0; found = (New-Object System.Collections.ArrayList);
|
|
status = $null; ready = $false; readyAt = $null; lastLineAt = $null; errors = 0; lastError = ''; lastErrorAt = $null; rejected = 0; lastNoNodeAt = $null;
|
|
identities = $identitiesPerProc; prepare = $null; mismatchSince = $null; lastMismatchAt = $null; faults = 0; lastFaultAt = $null }
|
|
}
|
|
$script:vendors[$name] = @{ name = $name; card = (Get-CardName $name); exe = $exe; instances = $instances; rebuilds = 0; building = $false; evm = (Get-EvmAddress $name);
|
|
path = $script:workerPath[$name]; pathText = (Get-PathText $name); fellBack = $false }
|
|
Log "${name}: $(Get-PathText $name)"
|
|
}
|
|
$script:phase = ''
|
|
if ($vendors.Count -eq 0) { Log 'no worker could be built; see the launcher log'; return $false }
|
|
if ($onePerIdentity) { Log ("identities per vendor: $minersPerVendor, one worker process each (about 1.3 GiB of GPU memory per process; ONE_PROCESS_PER_IDENTITY=1)") }
|
|
else { Log ("identities per vendor: $minersPerVendor, all through ONE miner and ONE worker per card (--identities $minersPerVendor)") }
|
|
foreach ($v in $vendors.Values) { Log ("EVM payout for the $($v.card) rewards: $($v.evm)" + $(if ($payoutEvm) { ' (PAYOUT_EVM)' } else { " (derived from the PC name and the vendor; set PAYOUT_EVM to pay elsewhere)" })) }
|
|
Log ("finality voting: " + $(if ($vote) { 'on (every identity signs every checkpoint)' } else { 'off (VOTE=0)' }))
|
|
Log ("job nonces: nvidia " + $(if ($nvidiaJobNonces) { $nvidiaJobNonces } else { 'miner default (16,777,216)' }) + ", amd $amdJobNonces")
|
|
return $true
|
|
}
|
|
|
|
# ---- the miners -------------------------------------------------------------------------------------------------------------
|
|
|
|
function Start-Miner($v, $inst) {
|
|
# One log segment per start (Start-Process truncates its redirect target); the status and upload use the latest one.
|
|
# The accepted count and the found times carry over; the read position starts again at 0.
|
|
$inst.starts += 1
|
|
$seg = if ($inst.starts -gt 1) { "-r$($inst.starts)" } else { '' }
|
|
$inst.log = Join-Path $root "$($inst.file)$seg.log"
|
|
$inst.err = $inst.log + '.err'
|
|
$inst.logPos = [long]0; $inst.logRem = ''; $inst.errPos = [long]0; $inst.errRem = ''
|
|
$inst.status = $null; $inst.ready = $false; $inst.readyAt = $null; $inst.restartAt = $null; $inst.lastNoNodeAt = $null; $inst.lastErrorAt = $null
|
|
# --worker-args takes ONE argument (quoted); the miner splits it on spaces and turns underscores into spaces.
|
|
# Start-Process joins the list with spaces and does not quote, so paths and the worker-args value are quoted here.
|
|
# --exit-on-seed-change stays as the fallback: a worker whose ready line says "prepare 1" never exits, the miner
|
|
# writes the next program's pack under --prepare-packs and the worker swaps at the boundary; a worker without
|
|
# prepare (no nvcc on PATH) exits 42 at the boundary and the launcher rebuilds it as before.
|
|
$margs = @('mine', $nodeUrl, '1', '100000000', $inst.label, '--worker', ('"' + $v.exe + '"'), '--status-secs', '30', '--exit-on-seed-change',
|
|
'--prepare-packs', ('"' + $preparePackDir + '"'), '--evm-address', $v.evm)
|
|
if ($inst.identities -gt 1) { $margs += @('--identities', [string]$inst.identities) }
|
|
if (-not $vote) { $margs += '--no-vote' }
|
|
if ($payout) { $margs += @('--address', $payout) } else { $margs += @('--payout-label', $inst.label) }
|
|
# The prebuilt workers take the exported pack with --pack (a relative path: the miner splits worker-args on spaces,
|
|
# cwd is $root for nvidia and proto-opencl for amd); the workers built here have it compiled in.
|
|
if ($v.name -eq 'nvidia') {
|
|
if ($nvidiaJobNonces) { $margs += @('--job-nonces', $nvidiaJobNonces) }
|
|
$wargs = '--device ' + $nvidiaDevice
|
|
if ($v.path -eq 'prebuilt') { $wargs += ' --pack ' + $packDirRel; if ($workerArch) { $wargs += ' --arch ' + $workerArch } }
|
|
$margs += @('--worker-args', ('"' + $wargs + '"'))
|
|
} else {
|
|
if ($amdJobNonces) { $margs += @('--job-nonces', $amdJobNonces) }
|
|
$wargs = if ($amdDevice) { '--device ' + $amdDevice } else { '--vendor ' + $amdVendor.Replace(' ', '_') }
|
|
if ($v.path -eq 'prebuilt') { $wargs += ' --pack ..\' + $packDirRel }
|
|
$margs += @('--worker-args', ('"' + $wargs + '"'))
|
|
}
|
|
Set-Content -Path $inst.log -Value "igneum-mine-test $($inst.label) $machine $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') node $nodeUrl run $($inst.runId) start $($inst.starts)"
|
|
$inst.proc = Start-Process -FilePath $miner -ArgumentList $margs -WorkingDirectory $(if ($v.name -eq 'amd') { Join-Path $root 'proto-opencl' } else { $root }) -RedirectStandardOutput $inst.log -RedirectStandardError $inst.err -NoNewWindow -PassThru
|
|
if ($inst.proc) { $null = $inst.proc.Handle } # without touching Handle, ExitCode comes back empty after the process ends
|
|
$inst.startedAt = Get-Date
|
|
Log "$($inst.label) started (pid $($inst.proc.Id)): `"$miner`" $($margs -join ' ') -> $($inst.log)"
|
|
}
|
|
|
|
function Stop-Vendor($v) {
|
|
foreach ($inst in $v.instances) { Stop-Proc $inst.proc }
|
|
}
|
|
|
|
function Stop-All { foreach ($v in $vendors.Values) { Stop-Vendor $v } }
|
|
|
|
function Get-VendorIdentities($v) { $n = 0; foreach ($inst in $v.instances) { $n += $inst.identities }; return $n }
|
|
|
|
# "8 RTX 5090 identities (1 miner)" or, one process per identity, "8 RTX 5090 miners"
|
|
function Get-VendorCountText($v) {
|
|
$ids = Get-VendorIdentities $v
|
|
if ($v.instances.Count -eq 1 -and $ids -gt 1) { return "$ids $($v.card) identities (1 miner)" }
|
|
if ($v.instances.Count -eq 1) { return "1 $($v.card) miner" }
|
|
return "$($v.instances.Count) $($v.card) miners"
|
|
}
|
|
|
|
function Start-AllMiners {
|
|
foreach ($v in $vendors.Values) { foreach ($inst in $v.instances) { Start-Miner $v $inst; Start-Sleep -Milliseconds 500 } }
|
|
$startText = @()
|
|
foreach ($v in $vendors.Values) { $startText += (Get-VendorCountText $v) }
|
|
Add-Event ('started ' + ($startText -join ' and ')) 'ok'
|
|
}
|
|
|
|
# Logged once, 10 s after the miners started: did the first worker of each vendor say ready?
|
|
function Report-WorkerReady {
|
|
Update-AllInstances
|
|
foreach ($v in $vendors.Values) {
|
|
$first = $v.instances[0]
|
|
if ($first.ready) { Log "$($v.name): worker ready after $([int](($first.readyAt) - $first.startedAt).TotalSeconds) s (see $($first.log))" } else {
|
|
Log "$($v.name): no ready line yet (see $($first.log))"
|
|
if ($first.err -and (Test-Path $first.err)) { Get-Content $first.err -Tail 5 | ForEach-Object { Log "$($first.label) stderr: $_" } }
|
|
if ($first.proc.HasExited) { Log "$($first.label) already exited with code $($first.proc.ExitCode)" }
|
|
}
|
|
}
|
|
}
|
|
|
|
# ---- log reading (the files are being written by the miners: FileShare ReadWrite, incremental) --------------------------
|
|
|
|
# New complete lines of $inst.log or $inst.err since the last call. A partial last line waits for the next call.
|
|
function Read-NewLines($inst, [string]$field) {
|
|
$path = $inst[$field]
|
|
if (-not $path -or -not (Test-Path $path)) { return @() }
|
|
$posKey = $field + 'Pos'; $remKey = $field + 'Rem'
|
|
$lines = @()
|
|
try {
|
|
$fs = New-Object IO.FileStream($path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
|
|
try {
|
|
$len = [long]$fs.Length
|
|
$pos = [long]$inst[$posKey]
|
|
if ($len -lt $pos) { $pos = [long]0; $inst[$remKey] = '' } # the file was truncated
|
|
if ($len -gt $pos) {
|
|
[void]$fs.Seek($pos, [IO.SeekOrigin]::Begin)
|
|
$buf = New-Object byte[] ([int]($len - $pos))
|
|
$n = $fs.Read($buf, 0, $buf.Length)
|
|
$inst[$posKey] = $pos + $n
|
|
$text = [string]$inst[$remKey] + [Text.Encoding]::UTF8.GetString($buf, 0, $n)
|
|
$parts = $text -split "`n"
|
|
$inst[$remKey] = $parts[$parts.Count - 1]
|
|
if ($parts.Count -gt 1) { $lines = @($parts[0..($parts.Count - 2)] | ForEach-Object { $_.TrimEnd("`r") }) }
|
|
}
|
|
} finally { $fs.Close() }
|
|
} catch { }
|
|
return @($lines)
|
|
}
|
|
|
|
function Convert-UnixToLocal([double]$secs) {
|
|
return [DateTimeOffset]::FromUnixTimeMilliseconds([long]($secs * 1000)).LocalDateTime
|
|
}
|
|
function Get-UnixNow { return [double][DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() / 1000.0 }
|
|
|
|
# The v4 miner adds "now=... (interval rates, N jobs, seed walk N calls)" between the wall rate and template_age; optional here.
|
|
$statusRegex = ' STATUS .*?: ([0-9]+)s jobs=([0-9]+) accepted=([0-9]+) rejected=([0-9]+) mismatched=([0-9]+) extra=([0-9]+) rate=[0-9.]+ blocks/s hash=([0-9.]+) MH/s wall \(([0-9.]+) MH/s inside jobs\)(?: now=.*?\))? template_age=([0-9.]+)s synced=(\w+)'
|
|
|
|
function Update-Instance($v, $inst) {
|
|
foreach ($l in (Read-NewLines $inst 'log')) {
|
|
$inst.lastLineAt = Get-Date
|
|
if ($l -match '^([0-9]+(?:\.[0-9]+)?) ACCEPTED block') {
|
|
$t = [double]$Matches[1]
|
|
$inst.accepted += 1
|
|
[void]$inst.found.Add($t)
|
|
Add-FoundEvent $v.card (Convert-UnixToLocal $t)
|
|
} elseif ($l -match $statusRegex) {
|
|
$inst.status = @{ at = (Get-Date); up = [int]$Matches[1]; jobs = [int]$Matches[2]; accepted = [int]$Matches[3]; rejected = [int]$Matches[4];
|
|
mismatched = [int]$Matches[5]; extra = [int]$Matches[6]; wall = [double]$Matches[7]; inside = [double]$Matches[8];
|
|
age = [double]$Matches[9]; synced = ($Matches[10] -eq 'true') }
|
|
} elseif ($l -match ' worker: ready ') {
|
|
$inst.ready = $true; $inst.readyAt = Get-Date
|
|
# v4 hot swap: "prepare 1" on the ready line means the worker compiles the next program itself and swaps at
|
|
# the boundary with no exit; "prepare 0" (no nvcc on PATH) means the old exit-42 rebuild path.
|
|
if ($l -match ' prepare 1') { $inst.prepare = $true } elseif ($l -match ' prepare 0') { $inst.prepare = $false }
|
|
if ($inst.starts -eq 1) { Add-Event ("$($v.card) worker ready ($($v.pathText))" + $(if ($inst.prepare -eq $true) { ', hot swap at the hour boundary' } elseif ($inst.prepare -eq $false) { ', no hot swap (nvcc missing): rebuild at the boundary' } else { '' })) 'ok' }
|
|
} elseif ($l -match ' WORKER FAULT (.*)$') {
|
|
# The miner's guard: the worker answered jobs without running its kernel (or 10x the rate); the miner kills and
|
|
# restarts it. The last rate is dropped so the dashboard never shows the fake number.
|
|
$inst.faults += 1; $inst.lastFaultAt = Get-Date; $inst.status = $null
|
|
Add-Event ("$($v.card) worker fault, restarting: " + $Matches[1].Substring(0, [Math]::Min(110, $Matches[1].Length))) 'error'
|
|
} elseif ($l -match ' worker: prepared ') {
|
|
Add-Event "$($v.card): the next hourly program is compiled and resident (hot swap ready)" 'build'
|
|
} elseif ($l -match ' worker: prepare-failed ') {
|
|
Add-Event "$($v.card): the worker could not compile the next program (see the miner log); it compiles inline at the boundary" 'error'
|
|
}
|
|
}
|
|
foreach ($l in (Read-NewLines $inst 'err')) {
|
|
if ($l -match 'seed mismatch') {
|
|
# The worker has no program for the job's seeds (a prepare failed, or the boundary passed without one): Update-Miners
|
|
# re-exports the pack and restarts the vendor when this goes on for 90 s
|
|
if (-not $inst.mismatchSince) { $inst.mismatchSince = Get-Date }
|
|
$inst.lastMismatchAt = Get-Date
|
|
}
|
|
if ($l -match ' rejected nonce=') { $inst.rejected += 1 }
|
|
elseif ($l -match '^template error') { $inst.lastNoNodeAt = Get-Date }
|
|
elseif ($l -match 'submit error|template error|WORKER MISMATCH|WORKER FAULT|worker fault|worker error|worker exited|could not prepare|above target|panicked|CUDA error|FAIL') {
|
|
$inst.errors += 1; $inst.lastError = $l; $inst.lastErrorAt = Get-Date
|
|
}
|
|
}
|
|
if ($inst.lastMismatchAt -and ((Get-Date) - $inst.lastMismatchAt).TotalSeconds -gt 60) { $inst.mismatchSince = $null; $inst.lastMismatchAt = $null }
|
|
# keep one hour of found times (the dashboard needs 10 minutes and the share window)
|
|
$cut = (Get-UnixNow) - 3600
|
|
while ($inst.found.Count -gt 0 -and [double]$inst.found[0] -lt $cut) { $inst.found.RemoveAt(0) }
|
|
}
|
|
|
|
function Update-AllInstances { foreach ($v in @($vendors.Values)) { foreach ($inst in $v.instances) { Update-Instance $v $inst } } }
|
|
|
|
function Count-FoundSince($inst, [double]$sinceUnix) {
|
|
$n = 0
|
|
for ($i = $inst.found.Count - 1; $i -ge 0; $i--) { if ([double]$inst.found[$i] -ge $sinceUnix) { $n++ } else { break } }
|
|
return $n
|
|
}
|
|
|
|
# Restarts after exits: code 42 (the epoch or day seed changed) rebuilds the vendor's worker and restarts its identities;
|
|
# any other exit restarts that identity after a random 5 to 60 s; the node coming back synced after a restart restarts
|
|
# every miner (their gRPC client never reconnects).
|
|
function Update-Miners {
|
|
if ($vendors.Count -eq 0) { $script:node.minersNeedRestart = $false; return }
|
|
if ($script:node.minersNeedRestart) {
|
|
$script:node.minersNeedRestart = $false
|
|
$count = 0
|
|
foreach ($v in @($vendors.Values)) { $count += $v.instances.Count }
|
|
Add-Event "node is back and synced: restarting $count miners (their connection died with the node)" 'ok'
|
|
foreach ($v in @($vendors.Values)) {
|
|
Stop-Vendor $v
|
|
foreach ($inst in $v.instances) { Update-Instance $v $inst; Start-Miner $v $inst; Start-Sleep -Milliseconds 500 }
|
|
}
|
|
return
|
|
}
|
|
foreach ($v in @($vendors.Values)) {
|
|
# A prebuilt worker that never reports ready (a DLL missing, an NVRTC or driver error in the miner log): once, after
|
|
# 150 s, fall back to the build path when a toolchain is installed, else say so and leave it to the miner's restarts
|
|
if ($v.path -eq 'prebuilt' -and -not $v.fellBack) {
|
|
$first = $v.instances[0]
|
|
if ($first.startedAt -and -not $first.ready -and ((Get-Date) - $first.startedAt).TotalSeconds -gt 150) {
|
|
$v.fellBack = $true
|
|
if ((Test-CanBuild $v.name) -and $script:lastSeeds) {
|
|
Add-Event "the prebuilt $($v.card) worker is not ready after 150 s (see $(Split-Path -Leaf $first.log)); building one with the installed toolchain instead" 'error'
|
|
$script:workerPath[$v.name] = 'build'; $v.path = 'build'; $v.pathText = (Get-PathText $v.name)
|
|
$v.building = $true
|
|
Draw-Dashboard
|
|
Stop-Vendor $v
|
|
$exe = Build-Worker $v.name $script:lastSeeds
|
|
if ($exe) { $v.exe = $exe; Add-Event "$($v.card) worker built here, restarting its miners" 'build' } else { Add-Event "the build also failed (see the launcher log); restarting with the prebuilt worker" 'error' }
|
|
foreach ($inst in $v.instances) { Update-Instance $v $inst; Start-Miner $v $inst; Start-Sleep -Milliseconds 500 }
|
|
$v.building = $false
|
|
continue
|
|
} else {
|
|
Add-Event "the prebuilt $($v.card) worker is not ready after 150 s: see $(Split-Path -Leaf $first.log); no toolchain here to build one" 'error'
|
|
}
|
|
}
|
|
}
|
|
$seedChange = $false
|
|
$why = 'the epoch or day seed changed (miner exit 42)'
|
|
foreach ($inst in $v.instances) {
|
|
if ($inst.proc -and $inst.proc.HasExited -and (Get-ExitCode $inst) -eq 42) { $seedChange = $true }
|
|
if ($inst.mismatchSince -and ((Get-Date) - $inst.mismatchSince).TotalSeconds -gt 90 -and $inst.lastMismatchAt -and ((Get-Date) - $inst.lastMismatchAt).TotalSeconds -lt 45) {
|
|
$seedChange = $true; $why = 'the worker has had no program for the jobs'' seeds for 90 s (seed mismatch errors)'
|
|
}
|
|
}
|
|
if ($seedChange) {
|
|
# The whole vendor group restarts: the worker exe cannot be rebuilt while another identity still runs it
|
|
$v.rebuilds += 1
|
|
$v.building = $true
|
|
Log "$($v.name): $why. Stopping its $($v.instances.Count) identities, re-exporting the pack, $(if ($v.path -eq 'prebuilt') { 'restarting the prebuilt worker on it' } else { 'rebuilding the worker' })."
|
|
Add-Event $(if ($v.path -eq 'prebuilt') { "the hourly program changed, restarting the $($v.card) worker on it" } else { "the hourly program changed, rebuilding the $($v.card) worker" }) 'build'
|
|
Draw-Dashboard # the build blocks this loop; show the state first
|
|
Stop-Vendor $v
|
|
foreach ($inst in $v.instances) { Update-Instance $v $inst } # the last lines of the old log segments
|
|
$t = Get-Date
|
|
$exe = $null
|
|
$seeds = Export-Pack
|
|
if ($seeds) {
|
|
$script:lastSeeds = $seeds
|
|
$exe = Build-Worker $v.name $seeds
|
|
if ($exe) { $v.exe = $exe }
|
|
}
|
|
$took = ((Get-Date) - $t).TotalSeconds
|
|
Log ("{0}: rebuild for the new seeds took {1:N1} s in total" -f $v.name, $took)
|
|
if ($seeds -and $exe) { Add-Event ($(if ($v.path -eq 'prebuilt') { "the new hourly program is exported (took {0:N0} s), {1} {2} miners restarting" } else { "the new hourly program is built (took {0:N0} s), {1} {2} miners restarting" }) -f $took, $v.instances.Count, $v.card) 'build' }
|
|
else { Add-Event ("the rebuild for {0} failed, restarting its miners with the old worker (see the launcher log)" -f $v.card) 'error' }
|
|
foreach ($inst in $v.instances) { Start-Miner $v $inst; Start-Sleep -Milliseconds 500 }
|
|
$v.building = $false
|
|
continue
|
|
}
|
|
foreach ($inst in $v.instances) {
|
|
if ($inst.proc -and $inst.proc.HasExited -and -not $inst.restartAt) {
|
|
$inst.restarts += 1
|
|
$inst.exitCode = Get-ExitCode $inst
|
|
# A random delay so a crash that hits every miner at once never restarts them at once
|
|
$delay = Get-Random -Minimum 5 -Maximum 61
|
|
$inst.restartAt = (Get-Date).AddSeconds($delay)
|
|
Log "$($inst.label): miner exited unexpectedly with code $($inst.exitCode); restarting in $delay s (restart $($inst.restarts))"
|
|
Add-Event "$($v.card) miner $($inst.index) stopped (code $($inst.exitCode)), restarting in $delay s" 'error'
|
|
}
|
|
if ($inst.restartAt -and (Get-Date) -ge $inst.restartAt) {
|
|
Update-Instance $v $inst
|
|
Start-Miner $v $inst
|
|
Add-Event "$($v.card) miner $($inst.index) restarted after a crash (restart $($inst.restarts))" 'info'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# ---- status in words --------------------------------------------------------------------------------------------------------
|
|
|
|
# One short state per identity: mining, warming up (ready, no report yet), starting, rebuilding, no node (the node is
|
|
# away), waiting (no report for a while), errors, restarting, stopped. The colour follows the state.
|
|
function Get-InstanceState($v, $inst) {
|
|
$now = Get-Date
|
|
if ($inst.restartAt) { return @{ text = 'restarting'; color = 'Yellow' } }
|
|
if ($v.building) { return @{ text = 'rebuilding'; color = 'Yellow' } }
|
|
if (-not $inst.proc -or $inst.proc.HasExited) { return @{ text = 'stopped'; color = 'Red' } }
|
|
if (-not $inst.ready) {
|
|
if ($inst.startedAt -and ($now - $inst.startedAt).TotalSeconds -gt 180) { return @{ text = 'not ready'; color = 'Red' } }
|
|
return @{ text = 'starting'; color = 'Yellow' }
|
|
}
|
|
if ($inst.lastNoNodeAt -and ($now - $inst.lastNoNodeAt).TotalSeconds -lt 30) { return @{ text = 'no node'; color = 'Yellow' } }
|
|
if ($inst.lastFaultAt -and -not $inst.status -and ($now - $inst.lastFaultAt).TotalSeconds -lt 180) { return @{ text = 'worker fault'; color = 'Red' } }
|
|
if ($inst.lastErrorAt -and ($now - $inst.lastErrorAt).TotalSeconds -lt 60) { return @{ text = 'errors'; color = 'Red' } }
|
|
if (-not $inst.status) { return @{ text = 'warming up'; color = 'Yellow' } }
|
|
$age = ($now - $inst.status.at).TotalSeconds
|
|
if ($age -gt 150 -and (Count-FoundSince $inst ((Get-UnixNow) - 150)) -eq 0) { return @{ text = 'waiting'; color = 'Yellow' } }
|
|
return @{ text = 'mining'; color = 'Green' }
|
|
}
|
|
|
|
function Format-Rate([double]$r) {
|
|
if ($r -ge 100) { return ('{0:0}' -f $r) }
|
|
if ($r -ge 10) { return ('{0:0.0}' -f $r) }
|
|
return ('{0:0.00}' -f $r)
|
|
}
|
|
|
|
# ---- the launcher log's status block (every 30 s, and the Ctrl+C summary); the console too in plain mode --------------
|
|
|
|
function Print-Status {
|
|
$up = ((Get-Date) - $started).ToString('hh\:mm\:ss')
|
|
$nowUnix = Get-UnixNow
|
|
if ($script:node.on) {
|
|
$n = $script:node
|
|
$st = Get-NodeState
|
|
$last = $script:chain.last
|
|
$tail = "up $(Get-NodeUptime), restarts $($n.restarts)"
|
|
if ($last) {
|
|
$blueTxt = if ($last.blue -ge 0) { [string]$last.blue } else { 'n/a' }
|
|
$peerTxt = if ($last.peers -eq 0) { '0 (no peer yet)' } else { [string]$last.peers }
|
|
Log ("node: {0}, blocks {1}, headers {2}, peers {3}, blue score {4}, DAA {5}, tips {6}, difficulty {7:N0} ({8})" -f $st.text, $last.blocks, $last.headers, $peerTxt, $blueTxt, $last.daa, $last.tips, $last.difficulty, $tail)
|
|
} else { Log "node: $($st.text), no reading yet ($tail)" }
|
|
}
|
|
foreach ($v in $vendors.Values) {
|
|
$total = 0.0; $acc = 0; $last10 = 0
|
|
foreach ($inst in $v.instances) {
|
|
$st = Get-InstanceState $v $inst
|
|
$acc += $inst.accepted
|
|
$last10 += Count-FoundSince $inst ($nowUnix - 600)
|
|
if ($inst.status) {
|
|
$total += $inst.status.wall
|
|
$old = [int]((Get-Date) - $inst.status.at).TotalSeconds
|
|
Log ("{0}: accepted {1}, {2:N2} MH/s (report {3} s old), template age {4:N2}s, rejected {5}, restarts {6}, worker faults {8}, {7}" -f $inst.label, $inst.accepted, $inst.status.wall, $old, $inst.status.age, $inst.rejected, $inst.restarts, $st.text, $inst.faults)
|
|
} else {
|
|
$since = if ($inst.readyAt) { "ready for $([int]((Get-Date) - $inst.readyAt).TotalSeconds) s, first job still running" } else { 'worker not ready yet' }
|
|
Log ("{0}: accepted {1}, no STATUS line yet ({2}), restarts {3}, {4}" -f $inst.label, $inst.accepted, $since, $inst.restarts, $st.text)
|
|
}
|
|
}
|
|
Log ("{0} TOTAL ({1}): {2} identities in {8} process(es), accepted {3} blocks ({4} in the last 10 min), {5:N2} MH/s, rebuilds {6}, up {7}, EVM payout {9}, {10}" -f $v.name, $v.card, (Get-VendorIdentities $v), $acc, $last10, $total, $v.rebuilds, $up, $v.instances.Count, $v.evm, $v.pathText)
|
|
}
|
|
$cs = Get-ChainStats
|
|
if ($cs) {
|
|
$shareTxt = if ($cs.share -ne $null) { ('{0:N0}%' -f $cs.share) } else { 'n/a' }
|
|
Log ("chain: network {0:N2} blocks/s ({1} blocks), difficulty {2:N1} {3}, our share of the last 100 blocks {4}" -f $cs.rate, $cs.blocks, $cs.difficulty, $cs.trend, $shareTxt)
|
|
} else { Log 'chain: no reading from the node yet' }
|
|
}
|
|
|
|
# ---- the dashboard ------------------------------------------------------------------------------------------------------------
|
|
|
|
# Glyphs are written here in ASCII and translated when drawn: # = full block, ^ = upper half block, _ = lower half block.
|
|
$bigFont = @{
|
|
'0' = @('#^#', '# #', '^^^'); '1' = @('^# ', ' # ', '^^^'); '2' = @('^^#', '#^^', '^^^'); '3' = @('^^#', ' ^#', '^^^')
|
|
'4' = @('# #', '^^#', ' ^'); '5' = @('#^^', '^^#', '^^^'); '6' = @('#^^', '#^#', '^^^'); '7' = @('^^#', ' #', ' ^')
|
|
'8' = @('#^#', '#^#', '^^^'); '9' = @('#^#', '^^#', '^^^'); '.' = @(' ', ' ', ' ^ '); ',' = @(' ', ' ', ' ^ ')
|
|
'-' = @(' ', '^^^', ' '); ' ' = @(' ', ' ', ' ')
|
|
'I' = @('^#^', ' # ', '^^^'); 'G' = @('#^^', '# #', '^^^'); 'N' = @('#^#', '# #', '^ ^'); 'E' = @('#^^', '#^ ', '^^^')
|
|
'U' = @('# #', '# #', '^^^'); 'M' = @('#_#', '# #', '^ ^')
|
|
}
|
|
$glyphFull = [string][char]0x2588
|
|
$glyphUpper = [string][char]0x2580
|
|
$glyphLower = [string][char]0x2584
|
|
|
|
function Get-BigRow([string]$text, [int]$row) {
|
|
$out = ''
|
|
foreach ($ch in $text.ToCharArray()) {
|
|
$g = $bigFont[[string]$ch]
|
|
if (-not $g) { $g = @(' ', ' ', ' ') }
|
|
$out += $g[$row] + ' '
|
|
}
|
|
return $out.Replace('#', $glyphFull).Replace('^', $glyphUpper).Replace('_', $glyphLower)
|
|
}
|
|
|
|
function Test-Dashboard {
|
|
if ($plain) { return $false }
|
|
try {
|
|
if ([Console]::IsOutputRedirected) { return $false }
|
|
if ($Host.Name -ne 'ConsoleHost') { return $false }
|
|
if ([Console]::WindowWidth -lt 60 -or [Console]::WindowHeight -lt 12) { return $false }
|
|
[Console]::SetCursorPosition([Console]::CursorLeft, [Console]::CursorTop)
|
|
return $true
|
|
} catch { return $false }
|
|
}
|
|
|
|
# A frame is a list of lines; a line is a list of (text, colour) pairs.
|
|
function New-Segs { return ,(New-Object System.Collections.ArrayList) }
|
|
function Add-Seg($line, [string]$text, [string]$color) { [void]$line.Add(@([string]$text, $color)) }
|
|
|
|
function Build-Frame {
|
|
$frame = New-Object System.Collections.ArrayList
|
|
$w = [Console]::WindowWidth
|
|
$h = [Console]::WindowHeight
|
|
$now = Get-Date
|
|
$nowUnix = Get-UnixNow
|
|
$up = ($now - $started).ToString('hh\:mm\:ss')
|
|
$marks = @('|', '/', '-', '\')
|
|
$mark = $marks[$script:tick % 4]
|
|
$dots = '.' * (1 + ($script:tick % 3))
|
|
$nodeOn = $script:node.on
|
|
$last = $script:chain.last
|
|
$cs = Get-ChainStats
|
|
|
|
# How tall the full layout would be; drop the big digits and blank lines when the window is short.
|
|
$cellsPerRow = [Math]::Max(1, [int][Math]::Floor(($w - 2) / 29))
|
|
$idRows = 0
|
|
foreach ($v in $vendors.Values) { $idRows += [int][Math]::Ceiling($v.instances.Count / $cellsPerRow) }
|
|
$nodeRows = if ($nodeOn) { 2 } else { 0 } # the NODE card: two rows, plus a blank one when not compact
|
|
$phaseRows = if ($script:phase) { 1 } else { 0 }
|
|
$full = 3 + 1 + $(if ($nodeOn) { 3 } else { 0 }) + $(if ($script:phase) { 2 } else { 0 }) + ($vendors.Count * (1 + 3 + 1)) + $idRows + 1 + 1 + 5 + 1 + 1
|
|
$compact = ($full -gt $h)
|
|
$eventRows = 5
|
|
if ($compact) {
|
|
$need = 3 + $nodeRows + $phaseRows + ($vendors.Count * 2) + $idRows + 1 + $eventRows + 1
|
|
while ($need -gt $h -and $eventRows -gt 1) { $eventRows--; $need-- }
|
|
}
|
|
|
|
# header: the mark, three rows
|
|
$connected = $script:chain.ok
|
|
$counts = @()
|
|
foreach ($v in $vendors.Values) { $counts += (Get-VendorCountText $v) }
|
|
for ($r = 0; $r -lt 3; $r++) {
|
|
$L = New-Segs
|
|
Add-Seg $L (' ' + (Get-BigRow 'IGNEUM' $r) + ' ') 'DarkYellow'
|
|
if ($r -eq 0) { Add-Seg $L $script:title 'White'; Add-Seg $L " $chainVersion, package $packageVersion" 'DarkGray' }
|
|
elseif ($r -eq 1) {
|
|
Add-Seg $L 'PC ' 'DarkGray'; Add-Seg $L $machine 'Gray'; Add-Seg $L ' node ' 'DarkGray'; Add-Seg $L "${nodeHost}:${nodePort}" 'Gray'; Add-Seg $L ' ' 'DarkGray'
|
|
if ($nodeOn) {
|
|
$st = Get-NodeState
|
|
if ($st.text -eq 'syncing' -and $last) { Add-Seg $L ('node: syncing, {0:N0} blocks' -f $last.blocks) 'Yellow' }
|
|
else { Add-Seg $L "node: $($st.text)" $st.color }
|
|
} elseif ($connected) { Add-Seg $L 'connected' 'Green' } else { Add-Seg $L 'waiting for the node' 'Yellow' }
|
|
} else {
|
|
$who = if ($counts.Count) { $counts -join ', ' } elseif ($nodeOn -and -not $script:phase) { 'node only' } else { 'miners not started yet' }
|
|
Add-Seg $L ($who + " running for $up") 'DarkGray'
|
|
}
|
|
[void]$frame.Add($L)
|
|
}
|
|
if (-not $compact) { [void]$frame.Add((New-Segs)) }
|
|
|
|
# the NODE card, drawn first
|
|
if ($nodeOn) {
|
|
$n = $script:node
|
|
$st = Get-NodeState
|
|
$L = New-Segs
|
|
Add-Seg $L ' NODE ' 'White'
|
|
Add-Seg $L ('{0,-13}' -f $st.text) $st.color
|
|
if ($last) {
|
|
Add-Seg $L ' blocks ' 'DarkGray'; Add-Seg $L ('{0:N0}' -f $last.blocks) 'White'
|
|
Add-Seg $L ' headers ' 'DarkGray'; Add-Seg $L ('{0:N0}' -f $last.headers) 'White'
|
|
Add-Seg $L ' blue score ' 'DarkGray'; Add-Seg $L $(if ($last.blue -ge 0) { ('{0:N0}' -f $last.blue) } else { 'n/a' }) 'White'
|
|
Add-Seg $L ' peers ' 'DarkGray'; Add-Seg $L ([string]$last.peers) $(if ($last.peers -gt 0) { 'White' } else { 'Yellow' })
|
|
} elseif ($n.managed -and $n.startedAt -and -not $n.restartAt) {
|
|
Add-Seg $L (' opening the database' + $dots) 'DarkGray'
|
|
} else { Add-Seg $L ' no reading yet' 'DarkGray' }
|
|
[void]$frame.Add($L)
|
|
$L = New-Segs
|
|
Add-Seg $L ' chain ' 'DarkGray'
|
|
if ($cs) {
|
|
Add-Seg $L ('{0:N2} blocks/s' -f $cs.rate) 'White'
|
|
Add-Seg $L ' difficulty ' 'DarkGray'
|
|
$tc = switch ($cs.trend) { 'rising' { 'Yellow' } 'falling' { 'Green' } default { 'White' } }
|
|
Add-Seg $L $cs.trend $tc
|
|
} else { Add-Seg $L 'rate after two readings' 'DarkGray' }
|
|
Add-Seg $L ' node up ' 'DarkGray'; Add-Seg $L (Get-NodeUptime) 'White'
|
|
if ($n.managed) {
|
|
if ($n.restarts -gt 0) { Add-Seg $L " restarts $($n.restarts)" 'DarkGray' }
|
|
if ($last -and $last.peers -eq 0 -and $n.startedAt -and ($now - $n.startedAt).TotalSeconds -gt 90) { Add-Seg $L " no peer: is the Mac node up at $macNode, is the firewall open?" 'Yellow' }
|
|
} else { Add-Seg $L ' started outside this window' 'DarkGray' }
|
|
[void]$frame.Add($L)
|
|
if (-not $compact) { [void]$frame.Add((New-Segs)) }
|
|
}
|
|
|
|
# the phase line while the miners are not running yet
|
|
if ($script:phase) {
|
|
$L = New-Segs
|
|
Add-Seg $L ' MINERS ' 'White'; Add-Seg $L ($script:phase + $dots) 'Yellow'
|
|
[void]$frame.Add($L)
|
|
if (-not $compact) { [void]$frame.Add((New-Segs)) }
|
|
}
|
|
|
|
# one card per GPU vendor
|
|
foreach ($v in $vendors.Values) {
|
|
$total = 0.0; $reported = 0; $acc = 0; $last10 = 0; $rej = 0
|
|
foreach ($inst in $v.instances) {
|
|
if ($inst.status) { $total += $inst.status.wall; $reported++ }
|
|
$acc += $inst.accepted
|
|
$last10 += Count-FoundSince $inst ($nowUnix - 600)
|
|
$rej += $inst.rejected
|
|
}
|
|
$L = New-Segs
|
|
Add-Seg $L (' ' + $v.card) 'White'
|
|
$ids = Get-VendorIdentities $v
|
|
if ($v.instances.Count -eq 1 -and $ids -gt 1) { Add-Seg $L " $ids identities, one worker" 'DarkGray' }
|
|
Add-Seg $L (' ' + $v.pathText) 'DarkGray'
|
|
if ($v.building) { Add-Seg $L " building the new hourly program$dots" 'Yellow' }
|
|
elseif ($reported -lt $v.instances.Count -and $reported -gt 0) { Add-Seg $L " $reported of $($v.instances.Count) miners reporting" 'DarkGray' }
|
|
elseif ($reported -eq 0) { Add-Seg $L ' no report from the miners yet (the first job has to finish)' 'Yellow' }
|
|
if ($rej -gt 0) { Add-Seg $L " $rej rejected" 'DarkGray' }
|
|
[void]$frame.Add($L)
|
|
$rateTxt = if ($reported -gt 0) { Format-Rate $total } else { '-' }
|
|
if ($compact) {
|
|
$L = New-Segs
|
|
Add-Seg $L (' ' + $rateTxt + ' MH/s') 'White'
|
|
Add-Seg $L ' blocks tonight ' 'DarkGray'; Add-Seg $L ([string]$acc) 'White'
|
|
Add-Seg $L ' last 10 min ' 'DarkGray'; Add-Seg $L ([string]$last10) 'White'
|
|
[void]$frame.Add($L)
|
|
} else {
|
|
for ($r = 0; $r -lt 3; $r++) {
|
|
$L = New-Segs
|
|
Add-Seg $L (' ' + (Get-BigRow $rateTxt $r)) 'White'
|
|
if ($r -eq 1) { Add-Seg $L ' MH/s' 'White' } else { Add-Seg $L ' ' 'White' }
|
|
if ($r -eq 0) { Add-Seg $L ' blocks found tonight ' 'DarkGray'; Add-Seg $L ([string]$acc) 'White' }
|
|
if ($r -eq 1) { Add-Seg $L ' in the last 10 minutes ' 'DarkGray'; Add-Seg $L ([string]$last10) 'White' }
|
|
[void]$frame.Add($L)
|
|
}
|
|
}
|
|
# one cell per identity: index, state, marker, rate
|
|
$L = New-Segs
|
|
$cell = 0
|
|
foreach ($inst in $v.instances) {
|
|
$st = Get-InstanceState $v $inst
|
|
$m = switch ($st.text) { 'mining' { $mark } 'warming up' { $mark } 'starting' { $dots.PadRight(3) } 'rebuilding' { $dots.PadRight(3) } 'restarting' { $dots.PadRight(3) } 'stopped' { 'x' } default { ' ' } }
|
|
$rate = if ($inst.status) { (Format-Rate $inst.status.wall) + ' MH/s' } elseif ($st.text -eq 'worker fault') { 'restarting' } else { '' }
|
|
Add-Seg $L (' {0,2} ' -f $inst.index) 'DarkGray'
|
|
Add-Seg $L ('{0,-10}' -f $st.text) $st.color
|
|
Add-Seg $L (' ' + ('{0,-3}' -f $m)) 'DarkGray'
|
|
Add-Seg $L ('{0,10}' -f $rate) 'Gray'
|
|
$cell++
|
|
if ($cell -ge $cellsPerRow) { [void]$frame.Add($L); $L = New-Segs; $cell = 0 }
|
|
}
|
|
if ($cell -gt 0) { [void]$frame.Add($L) }
|
|
if (-not $compact) { [void]$frame.Add((New-Segs)) }
|
|
}
|
|
|
|
# the chain (the node card already shows the rate and the trend; with it, this line keeps the share only)
|
|
$L = New-Segs
|
|
Add-Seg $L ' network ' 'DarkGray'
|
|
if ($cs) {
|
|
if (-not $nodeOn) {
|
|
Add-Seg $L ('{0:N2} blocks/s' -f $cs.rate) 'White'
|
|
Add-Seg $L ' difficulty ' 'DarkGray'
|
|
$tc = switch ($cs.trend) { 'rising' { 'Yellow' } 'falling' { 'Green' } default { 'White' } }
|
|
Add-Seg $L $cs.trend $tc
|
|
Add-Seg $L ' ' 'DarkGray'
|
|
}
|
|
Add-Seg $L 'your share of the last 100 blocks ' 'DarkGray'
|
|
if ($vendors.Count -eq 0) { Add-Seg $L 'no miners' 'DarkGray' }
|
|
elseif ($cs.share -ne $null) { Add-Seg $L ('{0:N0}%' -f $cs.share) 'White' } else { Add-Seg $L 'counting' 'DarkGray' }
|
|
} elseif ($connected) { Add-Seg $L "waiting for the first two readings from the node (one every $(Get-ChainInterval) s)" 'Yellow' }
|
|
else { Add-Seg $L 'no answer from the node' 'Yellow' }
|
|
[void]$frame.Add($L)
|
|
if (-not $compact) { [void]$frame.Add((New-Segs)) }
|
|
|
|
# the last events, newest at the bottom
|
|
$sorted = @($script:events | Sort-Object { $_.at })
|
|
$start = [Math]::Max(0, $sorted.Count - $eventRows)
|
|
for ($i = $start; $i -lt $sorted.Count; $i++) {
|
|
$e = $sorted[$i]
|
|
$L = New-Segs
|
|
Add-Seg $L (' ' + $e.at.ToString('HH:mm:ss') + ' ') 'DarkGray'
|
|
$ec = switch ($e.kind) { 'found' { 'DarkYellow' } 'build' { 'Yellow' } 'ok' { 'Green' } 'error' { 'Red' } default { 'Gray' } }
|
|
Add-Seg $L $e.text $ec
|
|
[void]$frame.Add($L)
|
|
}
|
|
for ($i = $sorted.Count; $i -lt $eventRows; $i++) { [void]$frame.Add((New-Segs)) }
|
|
if (-not $compact) { [void]$frame.Add((New-Segs)) }
|
|
|
|
# footer
|
|
$L = New-Segs
|
|
if (Test-Path $uploader) {
|
|
if ($script:uploadOk) { Add-Seg $L ' logs upload every minute' 'DarkGray' } else { Add-Seg $L ' the last log upload failed' 'Red' }
|
|
} else { Add-Seg $L ' no upload tool in this folder, the logs stay local' 'DarkGray' }
|
|
Add-Seg $L ' press Ctrl+C to stop' 'DarkGray'
|
|
Add-Seg $L (" full detail in " + (Split-Path -Leaf $launcherLog)) 'DarkGray'
|
|
[void]$frame.Add($L)
|
|
return ,$frame
|
|
}
|
|
|
|
function Draw-Frame($frame) {
|
|
$w = [Console]::WindowWidth
|
|
$h = [Console]::WindowHeight
|
|
if ($w -ne $script:dashW -or [Console]::WindowTop -ne 0) { Clear-Host; $script:dashW = $w; $script:dashRows = 0 }
|
|
$max = $h - 1
|
|
$row = 0
|
|
foreach ($line in $frame) {
|
|
if ($row -ge $max) { break }
|
|
[Console]::SetCursorPosition(0, $row)
|
|
$len = 0
|
|
foreach ($seg in $line) {
|
|
$t = [string]$seg[0]
|
|
$room = $w - 1 - $len
|
|
if ($t.Length -gt $room) { $t = $t.Substring(0, [Math]::Max(0, $room)) }
|
|
if ($t.Length -gt 0) { Write-Host -NoNewline -ForegroundColor $seg[1] $t; $len += $t.Length }
|
|
}
|
|
if ($len -lt $w - 1) { Write-Host -NoNewline (' ' * ($w - 1 - $len)) }
|
|
$row++
|
|
}
|
|
$blank = ' ' * [Math]::Max(0, $w - 1)
|
|
while ($row -lt $script:dashRows -and $row -lt $max) { [Console]::SetCursorPosition(0, $row); Write-Host -NoNewline $blank; $row++ }
|
|
$script:dashRows = [Math]::Min($frame.Count, $max)
|
|
[Console]::SetCursorPosition(0, [Math]::Min($script:dashRows, $max))
|
|
}
|
|
|
|
function Draw-Dashboard {
|
|
if (-not $script:dashOn) { return }
|
|
try {
|
|
$script:tick += 1
|
|
$frame = Build-Frame
|
|
Draw-Frame $frame
|
|
$total = 0.0; $acc = 0
|
|
foreach ($v in $vendors.Values) { foreach ($inst in $v.instances) { if ($inst.status) { $total += $inst.status.wall }; $acc += $inst.accepted } }
|
|
$titleTxt = if ($vendors.Count -gt 0) { ('Igneum v4 {0} MH/s {1} blocks' -f (Format-Rate $total), $acc) } else { 'Igneum v4' }
|
|
if ($script:node.on) { $titleTxt += ' node ' + (Get-NodeState).text }
|
|
$Host.UI.RawUI.WindowTitle = $titleTxt
|
|
} catch {
|
|
$script:dashOn = $false
|
|
try { [Console]::CursorVisible = $true } catch { }
|
|
Log "dashboard off, back to plain output: $_"
|
|
}
|
|
}
|
|
|
|
function Start-Dashboard {
|
|
if (Test-Dashboard) {
|
|
$script:dashOn = $true
|
|
$script:dashW = 0
|
|
try { [Console]::CursorVisible = $false } catch { }
|
|
Log 'dashboard on (PLAIN=1 in the bat gives the scrolling output instead)'
|
|
} else { Log ('plain output (' + $(if ($plain) { 'PLAIN=1' } else { 'not a console window' }) + ')') }
|
|
}
|
|
|
|
# Leaves the dashboard: the cursor goes below the frame so that the summary scrolls as plain text.
|
|
function Stop-Dashboard {
|
|
if (-not $script:dashOn) { return }
|
|
$script:dashOn = $false
|
|
try {
|
|
[Console]::SetCursorPosition(0, [Math]::Min($script:dashRows, [Console]::WindowHeight - 1))
|
|
Write-Host ''
|
|
[Console]::CursorVisible = $true
|
|
} catch { }
|
|
}
|
|
|
|
# ---- uploads (optional: upload-log.bat next to the scripts) --------------------------------------------------------------------
|
|
|
|
function Upload-Logs {
|
|
if (-not (Test-Path $uploader)) { return }
|
|
$allOk = $true
|
|
foreach ($v in $vendors.Values) {
|
|
foreach ($inst in $v.instances) {
|
|
if ($inst.err -and (Test-Path $inst.err) -and ((Get-Item $inst.err).Length -gt 0)) {
|
|
Get-Content $inst.err -Tail 5 | ForEach-Object { Add-Content -Path $launcherLog -Value (" " + $inst.label + " stderr: " + $_) }
|
|
}
|
|
$o = cmd /s /c "`"$uploader`" `"$($inst.log)`" $($inst.label) $($inst.runId)" 2>&1
|
|
$txt = ($o -join ' ')
|
|
Add-Content -Path $launcherLog -Value (" upload " + $inst.label + ": " + $txt)
|
|
if ($txt -notmatch '"ok"\s*:\s*true') { $allOk = $false }
|
|
}
|
|
}
|
|
$n = $script:node
|
|
if ($n.managed -and $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 ' '))
|
|
}
|
|
$o = cmd /s /c "`"$uploader`" `"$launcherLog`" $launcherName-$machine $launcherName-$machine-$stamp" 2>&1
|
|
if (($o -join ' ') -notmatch '"ok"\s*:\s*true') { $allOk = $false }
|
|
if (-not $allOk -and $script:uploadOk) { Add-Event 'the log upload failed (no internet?); the logs stay next to the bat' 'error' }
|
|
elseif ($allOk -and -not $script:uploadOk) { Add-Event 'log uploads work again' 'ok' }
|
|
$script:uploadOk = $allOk
|
|
}
|
|
|
|
# ---- keep awake -----------------------------------------------------------------------------------------------------------------
|
|
# SetThreadExecutionState is per thread, so the loop re-asserts it every 60 s and releases it on exit. The power plan is not touched.
|
|
|
|
$script:keepAwake = $null
|
|
$script:lastAwake = Get-Date
|
|
|
|
function Enable-KeepAwake([string]$what) {
|
|
try {
|
|
$script:keepAwake = Add-Type -Name 'KeepAwake' -Namespace 'Igneum' -PassThru -MemberDefinition @'
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
public static extern uint SetThreadExecutionState(uint esFlags);
|
|
'@
|
|
$ka = $script:keepAwake
|
|
[void]$ka::SetThreadExecutionState([uint32]2147483648 -bor [uint32]0x00000001) # ES_CONTINUOUS | ES_SYSTEM_REQUIRED
|
|
Log "keeping the PC awake while $what"
|
|
} catch { Log "could not set the keep-awake state: $_ (set Sleep to Never for the night)"; $script:keepAwake = $null }
|
|
$script:lastAwake = Get-Date
|
|
}
|
|
|
|
function Assert-KeepAwake {
|
|
if ($script:keepAwake -and ((Get-Date) - $script:lastAwake).TotalSeconds -ge 60) {
|
|
$ka = $script:keepAwake
|
|
[void]$ka::SetThreadExecutionState([uint32]2147483648 -bor [uint32]0x00000001)
|
|
$script:lastAwake = Get-Date
|
|
}
|
|
}
|
|
|
|
function Release-KeepAwake {
|
|
if ($script:keepAwake) { $ka = $script:keepAwake; [void]$ka::SetThreadExecutionState([uint32]2147483648); Log 'released the keep-awake state' } # ES_CONTINUOUS alone clears it
|
|
}
|
|
|
|
# ---- the loop helpers ------------------------------------------------------------------------------------------------------------
|
|
|
|
# True when Ctrl+C was pressed (the console treats it as a key while TreatControlCAsInput is on).
|
|
function Test-StopKey {
|
|
$stop = $false
|
|
while ([Console]::KeyAvailable) {
|
|
$k = [Console]::ReadKey($true)
|
|
if ($k.Key -eq 'C' -and ($k.Modifiers -band [ConsoleModifiers]::Control)) { $stop = $true }
|
|
}
|
|
return $stop
|
|
}
|
|
|
|
$script:timers = @{ draw = $started; status = (Get-Date); upload = (Get-Date) }
|
|
|
|
# One pass of the shared work: keep-awake, the chain reading, the node, the dashboard, the status block, the uploads.
|
|
function Invoke-CommonStep {
|
|
Assert-KeepAwake
|
|
Read-ChainSample
|
|
if (((Get-Date) - $script:chain.lastStart).TotalSeconds -ge (Get-ChainInterval)) { Start-ChainSample }
|
|
Update-Node
|
|
if ($script:dashOn -and ((Get-Date) - $script:timers.draw).TotalSeconds -ge 2) { Draw-Dashboard; $script:timers.draw = Get-Date }
|
|
if (((Get-Date) - $script:timers.status).TotalSeconds -ge 30) { Print-Status; $script:timers.status = Get-Date }
|
|
if (((Get-Date) - $script:timers.upload).TotalSeconds -ge 60) { Upload-Logs; $script:timers.upload = Get-Date }
|
|
}
|
|
|
|
# The end of every run: miners first, then the node, then the summary and the last upload.
|
|
function Stop-Everything([string]$why) {
|
|
[Console]::TreatControlCAsInput = $false
|
|
$wasDash = $script:dashOn
|
|
Stop-Dashboard
|
|
if ($wasDash -and $script:events.Count -gt 0) {
|
|
# the dashboard hid the log lines; show the last events as plain text before the summary
|
|
Write-Host 'last events:'
|
|
$sorted = @($script:events | Sort-Object { $_.at })
|
|
for ($i = [Math]::Max(0, $sorted.Count - 5); $i -lt $sorted.Count; $i++) { Write-Host (' ' + $sorted[$i].at.ToString('HH:mm:ss') + ' ' + $sorted[$i].text) }
|
|
}
|
|
Release-KeepAwake
|
|
if ($why) { Log $why }
|
|
if ($vendors.Count -gt 0) { Log 'stopping the miners and workers'; Stop-All }
|
|
if ($script:node.managed) { Log 'stopping the node'; Stop-Node }
|
|
Stop-Proc $script:chain.proc
|
|
Start-Sleep -Seconds 2
|
|
Update-AllInstances
|
|
Log ('SUMMARY after ' + ((Get-Date) - $started).ToString('hh\:mm\:ss'))
|
|
if ($script:node.managed) { Log "node started $($script:node.starts) time(s), restarts $($script:node.restarts)" }
|
|
Print-Status
|
|
Upload-Logs
|
|
Remove-Item -Path $script:chain.log, $script:chain.err -ErrorAction SilentlyContinue
|
|
Log 'logs uploaded (if upload-log.bat is present); they are also next to the bat'
|
|
if ($script:node.managed) { Log "the node's output is in $($script:node.log); the database stays in $appDir" }
|
|
}
|