windows-miner: console dashboard in the mining window, AMD jobs shortened

The window now shows a dashboard redrawn in place every 2 s (Write-Host colours,
[Console]::SetCursorPosition): the Igneum mark, PC and node with connected/waiting,
one card per GPU with the hash rate in big digits, blocks found tonight and in the
last 10 minutes, one cell per identity (mining, starting, warming up, rebuilding,
waiting, errors, restarting, stopped), a network line from igneum-miner watch
(blocks/s, difficulty trend, share of the last 100 blocks), the last 5 events in
words and a footer. PLAIN=1 in the bat, a redirected output or a non-console host
give the old scrolling output. The launcher log keeps the full detail; miner logs
are read incrementally with FileShare ReadWrite.

The 0.00 MH/s / template age n/a lines were AMD identities whose first 16.7M-nonce
job took 100 s (the miner prints STATUS only after a job ends); the regex matched
the real format. AMD_JOB_NONCES=2097152 keeps those jobs near 10 s and the
dashboard says "warming up" until the first STATUS line. Crash restarts no longer
block the loop for 10 s. README and WINDOWS-MINER.md updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-03 20:00:11 +00:00
parent 439c404a9e
commit 50f9fb87d3
4 changed files with 600 additions and 54 deletions

View file

@ -18,15 +18,38 @@ What the launcher does: lists GPUs and tools; exports the pack for the node's cu
`igneum-miner mine ... --worker <exe> --exit-on-seed-change` identities per GPU vendor, each with its own worker process
(about 1.3 GiB of GPU memory: 256 MiB cache plus 1 GiB dataset), its own vote key and payout address derived from the
PC name and the index (`--payout-label nvidia-<PC>-<i>`, the same on a rerun) and its own random 64-bit nonce start per
job; prints a status line per identity and a total per vendor every 30 s; uploads the logs every 60 s under
`<vendor>-<PC>-<index>`; on miner exit code 42 (the epoch or day seed changed) stops that vendor's identities,
re-exports, rebuilds and restarts them all, logging the time (the exe cannot be rebuilt while an instance runs it); on
any other exit restarts that identity after 10 s; Ctrl+C prints a summary and uploads once more. One shared worker per
vendor would need a multiplexing protocol and is not built; with 8 identities the 5090 holds about 10 GiB of worker
memory, the integrated AMD GPU takes the same from system RAM.
job; writes a status line per identity and a total per vendor to the launcher log every 30 s; uploads the logs every
60 s under `<vendor>-<PC>-<index>`; on miner exit code 42 (the epoch or day seed changed) stops that vendor's
identities, re-exports, rebuilds and restarts them all, logging the time (the exe cannot be rebuilt while an instance
runs it); on any other exit restarts that identity after 10 s; Ctrl+C prints a summary and uploads once more. One
shared worker per vendor would need a multiplexing protocol and is not built; with 8 identities the 5090 holds about
10 GiB of worker memory, the integrated AMD GPU takes the same from system RAM.
The window (since 3 October 2026, evening) is a dashboard redrawn in place every 2 s with `[Console]::SetCursorPosition`
and `Write-Host` colours only: a header (the Igneum mark in the half-block font, the PC name, the node address,
"connected" in green or "waiting for the node" in yellow); one card per vendor with the card name from WMI ("RTX 5090",
"Radeon Graphics"), the summed hash rate of the identities' last STATUS lines in big digits, blocks found this run and in
the last 10 minutes, and one cell per identity (index, state, heartbeat marker, its own MH/s); a network line; the last
5 events; a footer. States: `mining` (green), `starting` (no `worker: ready` line yet), `warming up` (ready, no STATUS
line yet), `rebuilding`, `restarting`, `waiting` (no STATUS for 150 s and no block) in yellow, `errors` (a stderr line
in the last 60 s) and `stopped` in red. The network line comes from `igneum-miner watch 1 <url>` started in the
background every 30 s (its first line gives `blocks=`, `daa=`, `difficulty=`): blocks/s over the last 60 to 90 s,
difficulty rising/falling/steady against the reading 5 min back (2% band), and "your share of the last 100 blocks" =
accepted blocks of this PC in the time the network took to add its last 100. Found blocks are folded into one event per
card per minute. The launcher log keeps every line as before (events are logged too). `PLAIN=1` in the bat, a
redirected output, the ISE host or a window under 60x12 give the old scrolling output.
Why some identities showed `0.00 MH/s, template age n/a` (fixed 3 October 2026): the miner prints its STATUS line only
after a job ends (`mine_worker` checks `last_report` once per job), and a job is `--job-nonces` nonces, 16,777,216 by
default. An RTX 5090 identity finishes that in under a second; the gfx1036 integrated GPU at 0.17 MH/s per identity
(eight share one compute unit) took 100 s, so the launcher's first status rounds found no STATUS line at all, and when
one came its template was 100 s old. The parser was matching the right format. The launcher now says "warming up" until
the first STATUS line, and the bat sets `AMD_JOB_NONCES=2097152` (a 2^21 job, about 10 s on that GPU; `NVIDIA_JOB_NONCES`
stays empty for the miner's default). The regex and the line it matches are quoted at the top of start-mining.ps1.
Settings at the top of START-MINING.bat: `NODE_HOST`, `NODE_PORT`, `PAYOUT_ADDRESS` (one address for every identity;
empty = derived per identity), `MINERS`, `NVIDIA_DEVICE` (CUDA index),
empty = derived per identity), `MINERS`, `NVIDIA_JOB_NONCES` and `AMD_JOB_NONCES` (nonces per job, see above), `PLAIN`
(1 = scrolling output instead of the dashboard), `NVIDIA_DEVICE` (CUDA index),
`CUDA_ARCH` (sm_120, or native), `AMD_VENDOR` (the OpenCL vendor string to match, default "Advanced Micro Devices", so
NVIDIA's OpenCL device is never picked for the AMD worker), `AMD_DEVICE` (flat index override), `USE_NVIDIA`, `USE_AMD`,
`VCVARS` and `VCVARS_VER` (14.30, the MSVC v143 toolset CUDA 12.8 accepts; Visual Studio 2026's own 14.5x crashes
@ -102,4 +125,7 @@ live); the OpenCL worker built with clang against Apple's OpenCL runtime (same b
with `kernel_bound.cu` through the clang emulation shim (`emu/emu.sh igneum-devnet-emu`, bench PASS and a serve job
bit-exact with Rust, seed-mismatch error exercised); `igneum-miner.exe` cross-compiled for x86_64-pc-windows-gnu.
Not tested here: nvcc and cl.exe builds, the WMI GPU listing, vcvarsall import, winget, taskkill and Ctrl+C handling in
start-mining.ps1 (no PowerShell on this Mac), and running the .exe files on Windows.
start-mining.ps1 (no PowerShell on this Mac), the dashboard drawing (cursor positioning, the half-block glyphs in the
console font, the window-height fallback), and running the .exe files on Windows. The 3 October 2026 dashboard version
was checked only with a bracket and quote balance pass and a read-through; the first Windows run is the real test, and
`PLAIN=1` is the way back if the window misbehaves.

View file

@ -1,5 +1,6 @@
Igneum devnet mining test. Double-click START-MINING.bat; it finds your GPUs, builds the NVIDIA (CUDA) and AMD (OpenCL) workers on the first run, and mines against the Mac node at 192.168.68.64 port 26610 (gRPC; p2p is 26611). Change the address, port, payout address or device choice at the top of START-MINING.bat.
It needs CUDA Toolkit 12.8 or newer and Visual Studio with the MSVC v143 (14.30) x64 component; the AMD worker uses the OpenCL headers the CUDA Toolkit ships. The miner (igneum-miner.exe) is prebuilt, no Rust needed.
MINERS=8 (top of the bat) starts 8 miner identities per GPU vendor, each with its own worker (about 1.3 GiB of GPU memory each), its own vote key and payout address derived from the PC name and the index (the same on a rerun), and its own random 64-bit nonce start per job, so no two miners repeat work; set MINERS=1 for a single-identity run. Logs are written next to the bat as nvidia-<index>-<stamp>.log, amd-<index>-<stamp>.log and launcher-<stamp>.log, and uploaded every 60 seconds under the labels nvidia-<PC name>-<index> and amd-<PC name>-<index>, so the log is already with us.
Every hour the program changes (epoch of 3,600 blocks) and once a day the cache seed changes: the miner stops with code 42, the launcher re-exports the pack from the node, rebuilds the worker (about 30 s for CUDA, a few seconds for OpenCL) and restarts it; a worker that dies for any other reason is restarted after 10 s. A status line per worker prints every 30 s.
The window shows a dashboard, redrawn every 2 seconds: the Igneum mark, the PC name, the node address and "connected" (or "waiting for the node" in yellow); one card per GPU (RTX 5090, Radeon Graphics) with its total hash rate in big digits, blocks found tonight, blocks in the last 10 minutes and one cell per miner ("mining" in green, "starting", "warming up", "rebuilding" or "waiting" in yellow, "errors" or "stopped" in red, with the miner's own hash rate); a network line (blocks per second on the chain, whether the difficulty is rising, falling or steady, and your share of the last 100 blocks, read from the node every 30 seconds); the last 5 events in words (a block found, the hourly program rebuilt, a miner restarted, an upload failure); and a footer. The launcher log keeps the full detail. Set PLAIN=1 at the top of the bat for the old scrolling output; the launcher uses it by itself when the window is not a real console (PowerShell ISE, redirected output).
Every hour the program changes (epoch of 3,600 blocks) and once a day the cache seed changes: the miner stops with code 42, the launcher re-exports the pack from the node, rebuilds the worker (about 30 s for CUDA, a few seconds for OpenCL) and restarts it; a worker that dies for any other reason is restarted after 10 s. A status line per miner goes to the launcher log every 30 s (the miner itself reports only after a job ends, so a slow GPU shows "warming up" until its first job is done; AMD_JOB_NONCES at the top of the bat keeps AMD jobs short).
The launcher holds Windows awake while it runs (SetThreadExecutionState, no power plan change); for the night also set Sleep to Never in Settings > System > Power and check that no Windows Update restart is pending. Stop it in the morning with Ctrl+C in the window: the miners and workers are killed, a summary prints, the logs are uploaded one last time and the window stays open. The Mac side restarts with: cd ~/Projects/igneum/vendor/igneum-node && caffeinate -dims ./target/release/kaspad --devnet --nodnsseed --disable-upnp --enable-unsynced-mining --appdir=/tmp/igneum-devnet/node1 --rpclisten=0.0.0.0:26610 --listen=0.0.0.0:26611 --nologfiles >> /tmp/igneum-devnet/node1.log 2>&1 &

View file

@ -2,7 +2,7 @@
rem Igneum devnet mining test for Windows. Double-click this file.
rem It finds the GPUs in this PC, builds the matching GPU workers on first run (NVIDIA: CUDA, AMD: OpenCL), and runs
rem one igneum-miner per worker against the Mac node. Logs land next to this file and are uploaded every 60 seconds.
rem Stop with Ctrl+C; the window stays open at the end.
rem The window shows a dashboard: hash rate, blocks found and one line per miner. Stop with Ctrl+C; the window stays open at the end.
rem ---- settings: edit these lines only ----------------------------------------------------------
set "NODE_HOST=192.168.68.64"
@ -22,6 +22,14 @@ set "MINERS=8"
rem Set to 0 to skip a vendor even if the hardware is present.
set "USE_NVIDIA=1"
set "USE_AMD=1"
rem Nonces per job (one job = one block template; a multiple of 32). Empty = the miner's default of 16,777,216, which an
rem RTX 5090 identity finishes in under a second. The integrated AMD GPU needs 100 s for that, so it mines stale templates
rem and reports late; 2,097,152 keeps its jobs near 10 s.
set "NVIDIA_JOB_NONCES="
set "AMD_JOB_NONCES=2097152"
rem The window shows a dashboard (one card per GPU, redrawn in place). Set PLAIN=1 for the old scrolling log instead;
rem the launcher also falls back to it by itself when the window is not a real console.
set "PLAIN=0"
rem Visual Studio: the MSVC toolset CUDA 12.8 accepts (14.30 = MSVC v143). Adjust the path if Visual Studio is elsewhere.
set "VCVARS=C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvarsall.bat"
set "VCVARS_VER=14.30"

View file

@ -1,6 +1,6 @@
# 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,
# VCVARS, VCVARS_VER). 3 October 2026.
# 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.
@ -11,9 +11,23 @@
# 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. Every 30 s prints a status line per worker; every 60 s uploads the logs with upload-log.bat (if present);
# restarts a miner that exits (code 42 = the epoch or day seed changed: re-export, rebuild, restart, with the
# rebuild time logged); Ctrl+C stops everything and prints a summary.
# 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); a miner that exits is restarted (code 42 =
# the epoch or day seed changed: re-export, rebuild, restart, with the rebuild time logged; anything else: after
# 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=...
$ErrorActionPreference = 'Continue'
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
@ -33,6 +47,11 @@ $vcvars = if ($env:VCVARS) { $env:VCVARS } else { 'C:\Program Files\Microsoft Vi
$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 }
# 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
@ -40,12 +59,44 @@ $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 Log([string]$msg) {
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $msg"
Write-Host $line
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 (run $stamp)"
# ---- 1. hardware and tools ---------------------------------------------------------------------------------------
@ -56,6 +107,17 @@ Log ("GPUs found: " + ($(if ($gpuNames.Count) { $gpuNames -join '; ' } else { 'n
$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)')) {
@ -207,7 +269,11 @@ if (-not $seeds) { exit 1 }
# ---- 4. start miners ------------------------------------------------------------------------------------------------------------
# $vendors[name] = @{ exe; instances = @(...) }; an instance = @{ vendor; index; label; log; runId; proc; restarts; startedAt }
# $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
@ -215,27 +281,35 @@ foreach ($name in $plan) {
$instances = @()
for ($i = 1; $i -le $minersPerVendor; $i++) {
$suffix = if ($minersPerVendor -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; acceptedBefore = 0; startedAt = $null }
$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 }
}
$vendors[$name] = @{ name = $name; exe = $exe; instances = $instances; rebuilds = 0 }
$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 }
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
# and the accepted count of earlier segments is carried in acceptedBefore.
if ($inst.log -and (Test-Path $inst.log)) { $inst.acceptedBefore += (Select-String -Path $inst.log -Pattern 'ACCEPTED block').Count }
# 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.
$margs = @('mine', $nodeUrl, '1', '100000000', $inst.label, '--worker', ('"' + $v.exe + '"'), '--status-secs', '30', '--exit-on-seed-change')
if ($payout) { $margs += @('--address', $payout) } else { $margs += @('--payout-label', $inst.label) }
if ($v.name -eq 'nvidia') { $margs += @('--worker-args', ('"--device ' + $nvidiaDevice + '"')) }
else {
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(' ', '_') + '"')) }
}
@ -251,59 +325,466 @@ function Stop-Vendor($v) {
}
}
# ---- 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
}
}
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]
$ready = Select-String -Path $first.log -Pattern 'worker: ready' | Select-Object -Last 1
if ($ready) { Log "$($v.name): $($ready.Line)" } else {
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)" }
}
}
# ---- 5. loop: status, uploads, restarts, Ctrl+C --------------------------------------------------------------------------
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
Add-Content -Path $launcherLog -Value (" upload " + $inst.label + ": " + ($o -join ' '))
$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 Instance-Stats($inst) {
$accepted = $inst.acceptedBefore + (Select-String -Path $inst.log -Pattern 'ACCEPTED block').Count
$last = Select-String -Path $inst.log -Pattern 'STATUS' | Select-Object -Last 1
$wall = 0.0; $age = 'n/a'
if ($last -and $last.Line -match 'hash=([0-9.]+) MH/s wall \(([0-9.]+) MH/s inside jobs\) template_age=([0-9.]+)s') { $wall = [double]$Matches[1]; $age = $Matches[3] + 's' }
return @{ accepted = $accepted; wall = $wall; age = $age }
}
function Print-Status {
$up = ((Get-Date) - $started).ToString('hh\:mm\:ss')
foreach ($v in $vendors.Values) {
$total = 0.0; $acc = 0
foreach ($inst in $v.instances) {
$st = Instance-Stats $inst
$total += $st.wall; $acc += $st.accepted
Log ("{0}: accepted {1}, {2:N2} MH/s, template age {3}, restarts {4}" -f $inst.label, $st.accepted, $st.wall, $st.age, $inst.restarts)
}
Log ("{0} TOTAL: {1} identities, accepted {2} blocks, {3:N2} MH/s, rebuilds {4}, up {5}" -f $v.name, $v.instances.Count, $acc, $total, $v.rebuilds, $up)
}
}
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
@ -321,12 +802,20 @@ $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 1000
Start-Sleep -Milliseconds 500
if ($keepAwake -and ((Get-Date) - $lastAwake).TotalSeconds -ge 60) {
[void]$keepAwake::SetThreadExecutionState([uint32]2147483648 -bor [uint32]0x00000001)
$lastAwake = Get-Date
@ -336,6 +825,9 @@ try {
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) {
@ -344,36 +836,55 @@ try {
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). Stopping its $($v.instances.Count) identities, re-exporting the pack, rebuilding the worker."
Add-Event "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) {
$exe = Build-Worker $v.name $seeds
if ($exe) { $v.exe = $exe }
}
Log ("{0}: rebuild for the new seeds took {1:N1} s in total" -f $v.name, ((Get-Date) - $t).TotalSeconds)
$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) {
if ($inst.proc -and $inst.proc.HasExited -and -not $inst.restartAt) {
$inst.restarts += 1
Log "$($inst.label): miner exited unexpectedly with code $($inst.proc.ExitCode); restarting in 10 s (restart $($inst.restarts))"
Start-Sleep -Seconds 10
$inst.exitCode = $inst.proc.ExitCode
$inst.restartAt = (Get-Date).AddSeconds(10)
Log "$($inst.label): miner exited unexpectedly with code $($inst.exitCode); restarting in 10 s (restart $($inst.restarts))"
Add-Event "$($v.card) miner $($inst.index) stopped (code $($inst.exitCode)), restarting in 10 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