957 lines
55 KiB
PowerShell
957 lines
55 KiB
PowerShell
# Igneum devnet mining test, Windows launcher. Started by START-MINING.bat, which sets the settings as environment
|
|
# variables (NODE_HOST, NODE_PORT, PAYOUT_ADDRESS, NVIDIA_DEVICE, CUDA_ARCH, AMD_VENDOR, AMD_DEVICE, USE_NVIDIA, USE_AMD,
|
|
# NVIDIA_JOB_NONCES, AMD_JOB_NONCES, PLAIN, VCVARS, VCVARS_VER). 3 October 2026.
|
|
#
|
|
# What it does, in order:
|
|
# 1. Lists the GPUs and the tools (nvcc, MSVC through vcvarsall, an OpenCL SDK) and prints which workers will start.
|
|
# 2. Makes sure igneum-miner.exe exists (shipped prebuilt; built with cargo only if missing).
|
|
# 3. Exports the pack for the node's current epoch and day (igneum-miner export-pack) and builds the workers from it
|
|
# on first run; the build is cached by the pack's seeds, so later starts are instant.
|
|
# 4. Starts MINERS igneum-miner identities per vendor (nvidia, amd), each with its own worker process (256 MiB cache
|
|
# plus 1 GiB dataset of GPU memory per instance), its own deterministic vote key and payout address
|
|
# (<vendor>-<PC name>-<index>), and its own random 64-bit nonce start per job. Logs: <vendor>-<index>-<stamp>.log
|
|
# (no index when MINERS=1), uploaded under the label <vendor>-<PC name>-<index>.
|
|
# 5. Shows a dashboard in the window, redrawn in place every 2 s: one card per GPU (hash rate in big digits, blocks
|
|
# found, one line per identity), a network line (from igneum-miner watch every 30 s), the last 5 events and a
|
|
# footer. The launcher log keeps the full detail (a status line per identity every 30 s). PLAIN=1 in the bat, or
|
|
# a window that is not a real console, gives the old scrolling output instead.
|
|
# Every 60 s the logs are uploaded with upload-log.bat (if present). The hourly program change is a hot swap: the
|
|
# miner sends `prepare` to the worker as soon as the node reports the next seed (one lead ahead of the boundary),
|
|
# the worker builds the next program in the background and swaps at the boundary with no pause and no process
|
|
# exit (dashboard event "hourly program change, swapped with no pause (prepared in N s)"). A worker whose ready
|
|
# line has no "prepare 1" (no nvcc on PATH, an old build) falls back to the old path: a miner that exits with
|
|
# code 42 (the epoch or day seed changed) gets a re-export, a rebuild and a restart, with the rebuild time logged;
|
|
# a miner that exits for any other reason is restarted after a random 5 to 60 s delay (never in step with other rigs)
|
|
# 10 s); Ctrl+C stops everything and prints a summary.
|
|
#
|
|
# Miner log lines this script reads (vendor/igneum-node/igneum/miner/src/main.rs, mine_worker):
|
|
# <unix secs>.<ms> ACCEPTED block nonce=0x... (gpu worker, cpu re-check ok)
|
|
# <unix secs>.<ms> STATUS '<label>' [worker]: <up>s jobs=N accepted=N rejected=N mismatched=N extra=N rate=X blocks/s
|
|
# hash=X MH/s wall (X MH/s inside jobs) template_age=Xs synced=true
|
|
# <unix secs>.<ms> worker: ready cuda|opencl <device> ...
|
|
# <unix secs>.<ms> SEED CHANGE at daa ...
|
|
# The STATUS line is printed only after a job ends and at most every --status-secs, so a slow GPU with a long job shows
|
|
# nothing for a while (the integrated AMD GPU took 100 s per 16.7M-nonce job on 3 Oct 2026; AMD_JOB_NONCES shortens it).
|
|
# igneum-miner watch prints: <unix> node1 blocks=N headers=N daa=N tips=N peers=N difficulty=X sink=...
|
|
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
function Test-Port($h, $p) {
|
|
# Plain TCP probe. Test-NetConnection paints a blue progress banner in some consoles whatever $ProgressPreference says.
|
|
try { $c = New-Object Net.Sockets.TcpClient; $r = $c.BeginConnect($h, [int]$p, $null, $null); $ok = $r.AsyncWaitHandle.WaitOne(2000) -and $c.Connected; $c.Close(); return [bool]$ok } catch { return $false }
|
|
}
|
|
# no blue progress banners (Test-NetConnection paints one over the dashboard)
|
|
$ErrorActionPreference = 'Continue'
|
|
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
Set-Location $root
|
|
|
|
# NODE_HOST=auto (the default since 3 Oct 2026, evening): a node on this PC first (START-NODE.bat listens on
|
|
# 127.0.0.1:26610), else the Mac node. The choice is logged on the launcher's first line.
|
|
$nodeHost = if ($env:NODE_HOST) { $env:NODE_HOST } else { 'auto' }
|
|
$nodePort = if ($env:NODE_PORT) { $env:NODE_PORT } else { '26610' }
|
|
$nodeHostNote = ''
|
|
if ($nodeHost -eq 'auto') {
|
|
$nodeHost = '192.168.68.64'
|
|
$nodeHostNote = " (NODE_HOST=auto: nothing listens on 127.0.0.1:$nodePort, using the Mac node)"
|
|
try {
|
|
$tcp = New-Object Net.Sockets.TcpClient
|
|
if ($tcp.ConnectAsync('127.0.0.1', [int]$nodePort).Wait(1500) -and $tcp.Connected) { $nodeHost = '127.0.0.1'; $nodeHostNote = ' (NODE_HOST=auto: using the node on this PC)' }
|
|
$tcp.Close()
|
|
} catch { }
|
|
}
|
|
$nodeUrl = "grpc://${nodeHost}:${nodePort}"
|
|
$payout = $env:PAYOUT_ADDRESS
|
|
$nvidiaDevice = if ($env:NVIDIA_DEVICE) { $env:NVIDIA_DEVICE } else { '0' }
|
|
$cudaArch = if ($env:CUDA_ARCH) { $env:CUDA_ARCH } else { 'sm_120' }
|
|
$amdVendor = if ($env:AMD_VENDOR) { $env:AMD_VENDOR } else { 'Advanced Micro Devices' }
|
|
$amdDevice = $env:AMD_DEVICE
|
|
$useNvidia = ($env:USE_NVIDIA -ne '0')
|
|
$useAmd = ($env:USE_AMD -ne '0')
|
|
$vcvars = if ($env:VCVARS) { $env:VCVARS } else { 'C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvarsall.bat' }
|
|
$vcvarsVer = if ($env:VCVARS_VER) { $env:VCVARS_VER } else { '14.30' }
|
|
$minersPerVendor = 1
|
|
if ($env:MINERS -and [int]$env:MINERS -ge 1) { $minersPerVendor = [int]$env:MINERS }
|
|
# One worker process per card (3 Oct 2026): the MINERS identities run through ONE igneum-miner with --identities N and one
|
|
# worker (one context, one cache and dataset, one host thread). ONE_WORKER_PER_CARD=0 restores a worker per identity.
|
|
$oneWorkerPerCard = -not ($env:ONE_WORKER_PER_CARD -eq '0')
|
|
# 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 = if ($env:AMD_JOB_NONCES) { $env:AMD_JOB_NONCES } else { '2097152' }
|
|
$plain = ($env:PLAIN -eq '1')
|
|
|
|
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
$machine = $env:COMPUTERNAME
|
|
$launcherLog = Join-Path $root "launcher-$stamp.log"
|
|
$uploader = Join-Path $root 'upload-log.bat'
|
|
$started = Get-Date
|
|
|
|
# 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
|
|
|
|
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
|
|
}
|
|
|
|
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 5 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) }
|
|
}
|
|
|
|
Log "igneum-mine-test launcher $machine node $nodeUrl$nodeHostNote (run $stamp)"
|
|
|
|
# ---- 1. hardware and tools ---------------------------------------------------------------------------------------
|
|
|
|
$gpuNames = @()
|
|
try { $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
|
|
|
|
# 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)
|
|
}
|
|
|
|
$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)' }))
|
|
|
|
$plan = @()
|
|
$skipped = @()
|
|
if ($hasNvidia -and $useNvidia) {
|
|
if ($haveNvcc -and $haveCl) { $plan += 'nvidia' } else { $skipped += "nvidia: GPU present but " + $(if (-not $haveNvcc) { 'nvcc' } else { 'MSVC' }) + " is missing" }
|
|
} elseif ($hasNvidia) { $skipped += 'nvidia: disabled by USE_NVIDIA=0' } else { $skipped += 'nvidia: no NVIDIA GPU reported' }
|
|
if ($hasAmd -and $useAmd) {
|
|
if ($haveCl -and $openclSdk) { $plan += 'amd' } else { $skipped += "amd: GPU present but " + $(if (-not $haveCl) { 'MSVC' } else { 'an OpenCL SDK' }) + " is missing" }
|
|
} elseif ($hasAmd) { $skipped += 'amd: disabled by USE_AMD=0' } else { $skipped += 'amd: no AMD GPU reported' }
|
|
|
|
Log ("workers to start: " + $(if ($plan.Count) { $plan -join ', ' } else { 'none' }))
|
|
foreach ($s in $skipped) { Log "skipped $s" }
|
|
if ($plan.Count -eq 0) { Log 'nothing to run. Fix the missing tool above and start again.'; exit 1 }
|
|
|
|
# ---- 2. the miner ----------------------------------------------------------------------------------------------------
|
|
|
|
$miner = Join-Path $root 'igneum-miner.exe'
|
|
if (-not (Test-Path $miner)) {
|
|
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'; exit 1 }
|
|
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'; exit 1 }
|
|
}
|
|
|
|
try { $tnc = Test-Port $nodeHost $nodePort } catch { $tnc = $false }
|
|
if (-not $tnc) { Log "cannot reach the node at ${nodeHost}:${nodePort}. Is the Mac node running and on the same network?"; exit 1 }
|
|
Log "node reachable at ${nodeHost}:${nodePort}"
|
|
|
|
# ---- 3. pack and worker builds -------------------------------------------------------------------------------------------
|
|
|
|
$packDir = Join-Path $root 'proto-cuda\packs\devnet'
|
|
# Packs the miner writes for prepared seeds (one directory per epoch and day); the workers build their next kernel from them
|
|
$preparedPacks = Join-Path $root 'proto-cuda\packs\prepared'
|
|
New-Item -ItemType Directory -Force -Path $preparedPacks | Out-Null
|
|
|
|
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 ($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
|
|
}
|
|
|
|
$seeds = Export-Pack
|
|
if (-not $seeds) { exit 1 }
|
|
|
|
# ---- 4. start miners ------------------------------------------------------------------------------------------------------------
|
|
|
|
# $vendors[name] = @{ name; card; exe; instances; rebuilds; building }
|
|
# 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 }
|
|
$vendors = @{}
|
|
foreach ($name in $plan) {
|
|
$exe = Build-Worker $name $seeds
|
|
if (-not $exe) { Log "$name skipped: no worker binary"; continue }
|
|
$instances = @()
|
|
$processes = if ($oneWorkerPerCard) { 1 } else { $minersPerVendor }
|
|
for ($i = 1; $i -le $processes; $i++) {
|
|
$suffix = if ($processes -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;
|
|
prepare = $false; preparedS = $null }
|
|
}
|
|
$vendors[$name] = @{ name = $name; card = (Get-CardName $name); exe = $exe; instances = $instances; rebuilds = 0; building = $false }
|
|
}
|
|
if ($vendors.Count -eq 0) { Log 'no worker could be built; see the launcher log'; exit 1 }
|
|
if ($oneWorkerPerCard) { Log ("identities per vendor: $minersPerVendor through ONE worker process per card (labels <vendor>-<PC>-1..$minersPerVendor; about 1.3 GiB of GPU memory per card)") }
|
|
else { Log ("identities per vendor: $minersPerVendor (each identity runs its own worker: about 1.3 GiB of GPU memory per instance)") }
|
|
Log ("job nonces: nvidia " + $(if ($nvidiaJobNonces) { $nvidiaJobNonces } else { 'miner default (16,777,216)' }) + ", amd $amdJobNonces")
|
|
|
|
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
|
|
# --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 is the fallback only: a worker that answers "prepare 1" on its ready line is never exited at a
|
|
# seed change; the miner writes the next pack under --prepare-packs and the worker builds it in the background.
|
|
$margs = @('mine', $nodeUrl, '1', '100000000', $inst.label, '--worker', ('"' + $v.exe + '"'), '--status-secs', '30', '--exit-on-seed-change', '--prepare-packs', ('"' + $preparedPacks + '"'))
|
|
if ($oneWorkerPerCard -and $minersPerVendor -gt 1) { $margs += @('--identities', $minersPerVendor) }
|
|
if ($payout) { $margs += @('--address', $payout) } else { $margs += @('--payout-label', $inst.label) }
|
|
if ($v.name -eq 'nvidia') {
|
|
if ($nvidiaJobNonces) { $margs += @('--job-nonces', $nvidiaJobNonces) }
|
|
$margs += @('--worker-args', ('"--device ' + $nvidiaDevice + '"'))
|
|
} else {
|
|
if ($amdJobNonces) { $margs += @('--job-nonces', $amdJobNonces) }
|
|
if ($amdDevice) { $margs += @('--worker-args', ('"--device ' + $amdDevice + '"')) }
|
|
else { $margs += @('--worker-args', ('"--vendor ' + $amdVendor.Replace(' ', '_') + '"')) }
|
|
}
|
|
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) {
|
|
if ($inst.proc -and -not $inst.proc.HasExited) { & taskkill.exe /T /F /PID $inst.proc.Id 2>$null | Out-Null }
|
|
}
|
|
}
|
|
|
|
# ---- 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 }
|
|
|
|
$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\) 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
|
|
$inst.prepare = ($l -match ' prepare 1')
|
|
} elseif ($l -match ' worker: prepared \S+ \S+ ([0-9.]+)') {
|
|
# The next hourly program is resident in the worker (ms from the prepare line to the answer)
|
|
$inst.preparedS = [double]$Matches[1] / 1000.0
|
|
} elseif ($l -match ' SEED CHANGE .*swapped with no pause') {
|
|
$took = if ($inst.preparedS) { ('{0:N1}' -f $inst.preparedS) } else { '?' }
|
|
Add-Event ("hourly program change, swapped with no pause (prepared in $took s) on $($v.card) miner $($inst.index)") 'ok'
|
|
} elseif ($l -match ' SEED CHANGE .*compiles inline') {
|
|
Add-Event ("hourly program change on $($v.card) miner $($inst.index): the worker was not prepared in time and compiled inline") 'info'
|
|
} elseif ($l -match ' worker exited .*restarting it in ([0-9]+) s') {
|
|
Add-Event ("$($v.card) worker $($inst.index) crashed; the miner restarts it in $($Matches[1]) s") 'error'
|
|
}
|
|
}
|
|
foreach ($l in (Read-NewLines $inst 'err')) {
|
|
if ($l -match ' rejected nonce=') { $inst.rejected += 1 }
|
|
elseif ($l -match 'submit error|template error|WORKER MISMATCH|worker error|worker exited|above target|panicked|CUDA error|FAIL') {
|
|
$inst.errors += 1; $inst.lastError = $l; $inst.lastErrorAt = Get-Date
|
|
}
|
|
}
|
|
# 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
|
|
}
|
|
|
|
# ---- the chain, read from the node with igneum-miner watch (one reading every 30 s, in the background) -----------------
|
|
|
|
$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 }
|
|
|
|
function Start-ChainSample {
|
|
$c = $script:chain
|
|
if ($c.proc -and -not $c.proc.HasExited) {
|
|
if (((Get-Date) - $c.lastStart).TotalSeconds -gt 90) { & taskkill.exe /T /F /PID $c.proc.Id 2>$null | Out-Null; $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
|
|
$c.lastStart = Get-Date
|
|
} catch { $c.proc = $null; $c.fails += 1 }
|
|
}
|
|
|
|
function Read-ChainSample {
|
|
$c = $script:chain
|
|
if (-not $c.proc -or -not $c.proc.HasExited) { return }
|
|
$c.proc = $null
|
|
$line = $null
|
|
try {
|
|
$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]+) .*daa=([0-9]+) .*difficulty=([0-9.]+)') {
|
|
[void]$c.samples.Add(@{ at = $c.lastStart; blocks = [long]$Matches[1]; daa = [long]$Matches[2]; difficulty = [double]$Matches[3] })
|
|
while ($c.samples.Count -gt 0 -and ((Get-Date) - $c.samples[0].at).TotalSeconds -gt 1800) { $c.samples.RemoveAt(0) }
|
|
if (-not $c.ok) { $c.ok = $true; Add-Event 'the node answers again' 'ok' }
|
|
$c.fails = 0
|
|
} else {
|
|
$c.fails += 1
|
|
if ($c.ok -and $c.fails -ge 2) { $c.ok = $false; Add-Event 'the node did not answer twice in a row (is the Mac node running?)' 'error' }
|
|
}
|
|
}
|
|
|
|
# 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 }
|
|
}
|
|
|
|
# ---- status in words --------------------------------------------------------------------------------------------------------
|
|
|
|
# One short state per identity: mining, warming up (ready, no report yet), starting, rebuilding, 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.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
|
|
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}, {7}" -f $inst.label, $inst.accepted, $inst.status.wall, $old, $inst.status.age, $inst.rejected, $inst.restarts, $st.text)
|
|
} 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, accepted {3} blocks ({4} in the last 10 min), {5:N2} MH/s, rebuilds {6}, up {7}" -f $v.name, $v.card, $v.instances.Count, $acc, $last10, $total, $v.rebuilds, $up)
|
|
}
|
|
$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))
|
|
|
|
# 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) }
|
|
$full = 3 + 1 + ($vendors.Count * (1 + 3 + 1)) + $idRows + 1 + 1 + 5 + 1 + 1
|
|
$compact = ($full -gt $h)
|
|
$eventRows = 5
|
|
if ($compact) {
|
|
$need = 3 + ($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 += "$($v.instances.Count) $($v.card) miners" }
|
|
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 'Igneum devnet mining test' 'White' }
|
|
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 ($connected) { Add-Seg $L 'connected' 'Green' } else { Add-Seg $L 'waiting for the node' 'Yellow' }
|
|
} else { Add-Seg $L (($counts -join ', ') + " running for $up") 'DarkGray' }
|
|
[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'
|
|
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' } 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
|
|
$L = New-Segs
|
|
$cs = Get-ChainStats
|
|
Add-Seg $L ' network ' '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
|
|
Add-Seg $L ' your share of the last 100 blocks ' 'DarkGray'
|
|
if ($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 30 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 } }
|
|
$Host.UI.RawUI.WindowTitle = ('Igneum mining {0} MH/s {1} blocks' -f (Format-Rate $total), $acc)
|
|
} catch {
|
|
$script:dashOn = $false
|
|
try { [Console]::CursorVisible = $true } catch { }
|
|
Log "dashboard off, back to plain output: $_"
|
|
}
|
|
}
|
|
|
|
# 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 { }
|
|
}
|
|
|
|
# ---- 5. loop: status, uploads, restarts, Ctrl+C --------------------------------------------------------------------------
|
|
|
|
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 += "$($v.instances.Count) $($v.card) miners" }
|
|
Add-Event ('started ' + ($startText -join ' and ')) 'ok'
|
|
|
|
Start-Sleep -Seconds 10
|
|
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)" }
|
|
}
|
|
}
|
|
|
|
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 }
|
|
}
|
|
}
|
|
$o = cmd /s /c "`"$uploader`" `"$launcherLog`" launcher-$machine launcher-$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
|
|
}
|
|
|
|
Upload-Logs # first upload right after the start summary, so a failed start reaches the intake too
|
|
Log 'first upload done'
|
|
|
|
function Stop-All { foreach ($v in $vendors.Values) { Stop-Vendor $v } }
|
|
|
|
# Keep the PC awake while mining: SetThreadExecutionState is per thread, so the launcher's own loop re-asserts it every
|
|
# 60 s and releases it on exit. The power plan is not touched.
|
|
$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 mining'
|
|
} catch { Log "could not set the keep-awake state: $_ (set Sleep to Never for the night)"; $keepAwake = $null }
|
|
$lastAwake = Get-Date
|
|
|
|
[Console]::TreatControlCAsInput = $true
|
|
Log 'running. Press Ctrl+C to stop (the summary prints and the logs are uploaded one last time).'
|
|
Start-ChainSample
|
|
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' }) + ')') }
|
|
$lastStatus = Get-Date
|
|
$lastUpload = Get-Date
|
|
$lastDraw = $started
|
|
$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 }
|
|
Update-AllInstances
|
|
Read-ChainSample
|
|
if (((Get-Date) - $script:chain.lastStart).TotalSeconds -ge 30) { Start-ChainSample }
|
|
foreach ($v in @($vendors.Values)) {
|
|
$seedChange = $false
|
|
foreach ($inst in $v.instances) {
|
|
if ($inst.proc -and $inst.proc.HasExited -and (Get-ExitCode $inst) -eq 42) { $seedChange = $true }
|
|
}
|
|
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): the epoch or day seed changed (miner exit 42: fallback path, this worker has no prepare support). Stopping its $($v.instances.Count) identities, re-exporting the pack, rebuilding the worker."
|
|
Add-Event "the hourly program changed, rebuilding the $($v.card) worker (fallback: no prepare support)" '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) {
|
|
$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 ("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 rig 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'
|
|
}
|
|
}
|
|
}
|
|
if ($script:dashOn -and ((Get-Date) - $lastDraw).TotalSeconds -ge 2) { Draw-Dashboard; $lastDraw = Get-Date }
|
|
if (((Get-Date) - $lastStatus).TotalSeconds -ge 30) { Print-Status; $lastStatus = Get-Date }
|
|
if (((Get-Date) - $lastUpload).TotalSeconds -ge 60) { Upload-Logs; $lastUpload = Get-Date }
|
|
}
|
|
} finally {
|
|
[Console]::TreatControlCAsInput = $false
|
|
Stop-Dashboard
|
|
if ($keepAwake) { [void]$keepAwake::SetThreadExecutionState([uint32]2147483648); Log 'released the keep-awake state' } # ES_CONTINUOUS alone clears it
|
|
Log 'stopping the miners and workers'
|
|
Stop-All
|
|
if ($script:chain.proc -and -not $script:chain.proc.HasExited) { & taskkill.exe /T /F /PID $script:chain.proc.Id 2>$null | Out-Null }
|
|
Start-Sleep -Seconds 2
|
|
Update-AllInstances
|
|
Log ('SUMMARY after ' + ((Get-Date) - $started).ToString('hh\:mm\:ss'))
|
|
Print-Status
|
|
Upload-Logs
|
|
Log 'logs uploaded (if upload-log.bat is present); they are also next to START-MINING.bat'
|
|
}
|