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:
parent
439c404a9e
commit
50f9fb87d3
4 changed files with 600 additions and 54 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 &
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue