chore: add Windows agent source baseline

This commit is contained in:
lussierj committed 2026-08-24 20:40:20 +00:00
1 parent c3501b987e
commit 6cd876b4b3
12 files changed
+2574

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
*.ps1 -text
+8
View File
@@ -0,0 +1,8 @@
agent-config.json
agent-state.json
logs/
*.bak
*.bak.*
*-probe-*.json
rollback/
+8
View File
@@ -0,0 +1,8 @@
d10c355772381fb0a3953bb8cf3af247be21659a7eee4c489438d30f8bdd2da8 EnrichOneMiner.ps1
ef56e1eca40aa838ff1e3826cdba33dc948064b8d685c6d1d4c365b049984d4e MinerOpsAgentGUI.ps1
51fbcec81331ae9b171783b55f589f7c3d4d0e872ae66611b918703ef2b8725d MinerOpsAgentService.ps1
bee0f9925e5ace27d8d7207d12af0ab7e5cab6b2a655654fca0ecf7dd71d7380 MinerOpsAgentTray.ps1
c68ad90566be4ff5b18142048cdc5186d6c71c900f45e5d15d07ef4ecfc25317 MinerOpsApi3EnrichmentPoller.ps1
90e4bbe7b1fc6467fc37da7c4e67adc01e74f332e9a2d14d1961f1488a984894 MinerOpsPoolPoller.ps1
18b36f45b966d1ff0c0cd008bb25567204e1cb90839b825e80671f1c2071d3e5 MinerOpsPoolPollerTask.ps1
+249
View File
@@ -0,0 +1,249 @@
param(
[Parameter(Mandatory=$true)]
[string]$IpAddress,
[string]$MinerId = ""
)
$ConfigPath = "C:\MinerOpsAgent\agent-config.json"
function Read-AgentConfig {
if (!(Test-Path $ConfigPath)) {
throw "Agent config not found: $ConfigPath"
}
return Get-Content $ConfigPath -Raw | ConvertFrom-Json
}
function Invoke-MinerCommandRaw {
param(
[string]$IpAddress,
[string]$Command,
[int]$Port = 4028,
[int]$TimeoutMs = 2500
)
$client = $null
try {
$client = New-Object System.Net.Sockets.TcpClient
$async = $client.BeginConnect($IpAddress, $Port, $null, $null)
if (-not $async.AsyncWaitHandle.WaitOne($TimeoutMs)) {
return ""
}
$client.EndConnect($async)
$client.ReceiveTimeout = $TimeoutMs
$client.SendTimeout = $TimeoutMs
$stream = $client.GetStream()
$payload = [System.Text.Encoding]::ASCII.GetBytes("{`"cmd`":`"$Command`"}`n")
$stream.Write($payload, 0, $payload.Length)
try {
$client.Client.Shutdown([System.Net.Sockets.SocketShutdown]::Send)
} catch {
}
$buffer = New-Object byte[] 262144
$chunks = New-Object System.Collections.ArrayList
$started = Get-Date
while (((Get-Date) - $started).TotalMilliseconds -lt $TimeoutMs) {
if (-not $stream.DataAvailable) {
Start-Sleep -Milliseconds 20
continue
}
$read = $stream.Read($buffer, 0, $buffer.Length)
if ($read -le 0) {
break
}
for ($i = 0; $i -lt $read; $i++) {
[void]$chunks.Add([byte]$buffer[$i])
}
if ($chunks.Count -gt 2097152) {
break
}
}
return [System.Text.Encoding]::UTF8.GetString([byte[]]$chunks.ToArray([byte])).Trim()
} catch {
return ""
} finally {
if ($client -ne $null) {
$client.Close()
}
}
}
function Try-ParseJson {
param([string]$Raw)
if (!$Raw -or $Raw.Trim().Length -eq 0) {
return $null
}
try {
return $Raw | ConvertFrom-Json
} catch {
return $null
}
}
function Find-FirstValue {
param(
[object]$Object,
[string[]]$Names
)
if ($null -eq $Object) {
return ""
}
$json = $Object | ConvertTo-Json -Depth 80 -Compress
foreach ($name in $Names) {
$escaped = [regex]::Escape($name)
$pattern = '"' + $escaped + '"\s*:\s*"([^"]+)"'
$m = [regex]::Match($json, $pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
if ($m.Success) {
return $m.Groups[1].Value
}
$pattern2 = '"' + $escaped + '"\s*:\s*([0-9A-Za-z\.\-_:]+)'
$m2 = [regex]::Match($json, $pattern2, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
if ($m2.Success) {
return $m2.Groups[1].Value
}
}
return ""
}
function Find-MacAddress {
param([string]$Text)
if (!$Text) {
return ""
}
$m = [regex]::Match($Text, '([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}')
if ($m.Success) {
return $m.Value.Replace("-", ":").ToUpper()
}
return ""
}
$config = Read-AgentConfig
$hubUrl = ([string]$config.hub_url).TrimEnd("/")
$secret = [string]$config.agent_secret
$commands = @(
"summary",
"devs",
"edevs",
"stats",
"pools",
"devdetails",
"version",
"get_version",
"get_miner_info"
)
$raws = [ordered]@{}
$parsed = [ordered]@{}
foreach ($cmd in $commands) {
Write-Host "Querying $IpAddress command: $cmd"
$raw = Invoke-MinerCommandRaw -IpAddress $IpAddress -Command $cmd -TimeoutMs 2500
$raws[$cmd] = $raw
$parsed[$cmd] = Try-ParseJson -Raw $raw
Write-Host " bytes: $($raw.Length)"
}
$allText = ($raws.GetEnumerator() | ForEach-Object { $_.Value }) -join "`n"
$allParsed = [ordered]@{
summary = $parsed["summary"]
devs = $parsed["devs"]
edevs = $parsed["edevs"]
stats = $parsed["stats"]
pools = $parsed["pools"]
devdetails = $parsed["devdetails"]
version = $parsed["version"]
get_version = $parsed["get_version"]
get_miner_info = $parsed["get_miner_info"]
}
$mac = Find-FirstValue -Object $allParsed -Names @("MAC", "Mac", "mac", "macaddr", "mac_address", "Net MAC", "ethaddr", "Ethernet MAC")
if (!$mac) {
$mac = Find-MacAddress -Text $allText
}
$hostname = Find-FirstValue -Object $allParsed -Names @("Hostname", "hostname", "host", "Host")
$minerSn = Find-FirstValue -Object $allParsed -Names @("Miner SN", "miner_sn", "miner sn", "Serial", "serial", "SN", "sn", "M_Serial")
$firmware = Find-FirstValue -Object $allParsed -Names @("Firmware", "firmware", "firmware_version", "Soft Version", "Version", "CompileTime")
$apiVersion = Find-FirstValue -Object $allParsed -Names @("API", "api", "api_version", "Api Version")
$platform = Find-FirstValue -Object $allParsed -Names @("Platform", "platform", "Machine", "machine")
$model = Find-FirstValue -Object $allParsed -Names @("Model", "model", "Miner Type", "miner_type", "Type", "type")
if (!$model) {
$model = "WhatsMiner"
}
$payload = [ordered]@{
ip_address = $IpAddress
miner_id = $MinerId
mac_address = $mac
hostname = $hostname
miner_sn = $minerSn
model = $model
miner_type = $model
api_version = $apiVersion
firmware_version = $firmware
platform = $platform
raw_version = @{
version = $raws["version"]
get_version = $raws["get_version"]
}
raw_miner_info = @{
devs = $raws["devs"]
edevs = $raws["edevs"]
stats = $raws["stats"]
pools = $raws["pools"]
devdetails = $raws["devdetails"]
get_miner_info = $raws["get_miner_info"]
}
}
Write-Host ""
Write-Host "Detected:"
Write-Host " MAC: $mac"
Write-Host " Hostname: $hostname"
Write-Host " SN: $minerSn"
Write-Host " Model: $model"
Write-Host " Firmware: $firmware"
Write-Host " API: $apiVersion"
Write-Host " Platform: $platform"
Write-Host ""
$response = Invoke-RestMethod `
-Method Post `
-Uri "$hubUrl/api/agent/miner-enrichment-v3" `
-Headers @{
"X-MinerOps-Agent-Secret" = $secret
} `
-ContentType "application/json" `
-Body ($payload | ConvertTo-Json -Depth 80)
$response | ConvertTo-Json -Depth 20
+226
View File
@@ -0,0 +1,226 @@
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$BaseDir = "C:\MinerOpsAgent"
$ConfigPath = Join-Path $BaseDir "agent-config.json"
$StatePath = Join-Path $BaseDir "agent-state.json"
$LogPath = Join-Path $BaseDir "logs\agent.log"
$TaskName = "MinerOpsAgent"
function Read-Config {
if (Test-Path $ConfigPath) {
return Get-Content $ConfigPath -Raw | ConvertFrom-Json
}
return $null
}
function Read-ConfigText {
if (Test-Path $ConfigPath) {
return Get-Content $ConfigPath -Raw
}
return ""
}
function Save-ConfigObject {
param([object]$Config)
$Config | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigPath -Encoding UTF8
}
function Save-ConfigText {
param([string]$Text)
$Text | Set-Content -Path $ConfigPath -Encoding UTF8
}
function Get-AgentTaskText {
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if (!$task) { return "Scheduled task not installed" }
$info = Get-ScheduledTaskInfo -TaskName $TaskName -ErrorAction SilentlyContinue
return "$($task.State) | Last result: $($info.LastTaskResult) | Last run: $($info.LastRunTime)"
}
$form = New-Object System.Windows.Forms.Form
$form.Text = "MinerOps Agent"
$form.Size = New-Object System.Drawing.Size(950, 780)
$form.StartPosition = "CenterScreen"
$statusLabel = New-Object System.Windows.Forms.Label
$statusLabel.Location = New-Object System.Drawing.Point(15, 15)
$statusLabel.Size = New-Object System.Drawing.Size(900, 24)
$statusLabel.Text = "Task: " + (Get-AgentTaskText)
$form.Controls.Add($statusLabel)
$startButton = New-Object System.Windows.Forms.Button
$startButton.Text = "Start"
$startButton.Location = New-Object System.Drawing.Point(15, 45)
$startButton.Size = New-Object System.Drawing.Size(100, 32)
$form.Controls.Add($startButton)
$stopButton = New-Object System.Windows.Forms.Button
$stopButton.Text = "Stop"
$stopButton.Location = New-Object System.Drawing.Point(125, 45)
$stopButton.Size = New-Object System.Drawing.Size(100, 32)
$form.Controls.Add($stopButton)
$restartButton = New-Object System.Windows.Forms.Button
$restartButton.Text = "Restart"
$restartButton.Location = New-Object System.Drawing.Point(235, 45)
$restartButton.Size = New-Object System.Drawing.Size(100, 32)
$form.Controls.Add($restartButton)
$testButton = New-Object System.Windows.Forms.Button
$testButton.Text = "Test Hub"
$testButton.Location = New-Object System.Drawing.Point(345, 45)
$testButton.Size = New-Object System.Drawing.Size(100, 32)
$form.Controls.Add($testButton)
$saveButton = New-Object System.Windows.Forms.Button
$saveButton.Text = "Save Config"
$saveButton.Location = New-Object System.Drawing.Point(455, 45)
$saveButton.Size = New-Object System.Drawing.Size(120, 32)
$form.Controls.Add($saveButton)
$secretLabel = New-Object System.Windows.Forms.Label
$secretLabel.Text = "Agent Secret:"
$secretLabel.Location = New-Object System.Drawing.Point(15, 90)
$secretLabel.Size = New-Object System.Drawing.Size(100, 22)
$form.Controls.Add($secretLabel)
$secretBox = New-Object System.Windows.Forms.TextBox
$secretBox.Location = New-Object System.Drawing.Point(115, 87)
$secretBox.Size = New-Object System.Drawing.Size(570, 24)
$secretBox.UseSystemPasswordChar = $true
$form.Controls.Add($secretBox)
$showSecretCheck = New-Object System.Windows.Forms.CheckBox
$showSecretCheck.Text = "Show"
$showSecretCheck.Location = New-Object System.Drawing.Point(695, 88)
$showSecretCheck.Size = New-Object System.Drawing.Size(70, 24)
$form.Controls.Add($showSecretCheck)
$saveSecretButton = New-Object System.Windows.Forms.Button
$saveSecretButton.Text = "Save Secret"
$saveSecretButton.Location = New-Object System.Drawing.Point(775, 84)
$saveSecretButton.Size = New-Object System.Drawing.Size(120, 30)
$form.Controls.Add($saveSecretButton)
$configBox = New-Object System.Windows.Forms.TextBox
$configBox.Location = New-Object System.Drawing.Point(15, 125)
$configBox.Size = New-Object System.Drawing.Size(900, 350)
$configBox.Multiline = $true
$configBox.ScrollBars = "Both"
$configBox.Font = New-Object System.Drawing.Font("Consolas", 10)
$configBox.Text = Read-ConfigText
$form.Controls.Add($configBox)
$stateBox = New-Object System.Windows.Forms.TextBox
$stateBox.Location = New-Object System.Drawing.Point(15, 490)
$stateBox.Size = New-Object System.Drawing.Size(900, 85)
$stateBox.Multiline = $true
$stateBox.ScrollBars = "Vertical"
$stateBox.Font = New-Object System.Drawing.Font("Consolas", 9)
$form.Controls.Add($stateBox)
$logBox = New-Object System.Windows.Forms.TextBox
$logBox.Location = New-Object System.Drawing.Point(15, 590)
$logBox.Size = New-Object System.Drawing.Size(900, 130)
$logBox.Multiline = $true
$logBox.ScrollBars = "Vertical"
$logBox.Font = New-Object System.Drawing.Font("Consolas", 9)
$form.Controls.Add($logBox)
function Refresh-Display {
$statusLabel.Text = "Task: " + (Get-AgentTaskText)
$config = Read-Config
if ($config -and $secretBox.Text.Length -eq 0) {
$secretBox.Text = [string]$config.agent_secret
}
if (Test-Path $StatePath) {
$stateBox.Text = Get-Content $StatePath -Raw
} else {
$stateBox.Text = "No state file yet."
}
if (Test-Path $LogPath) {
$logBox.Text = (Get-Content $LogPath -Tail 30) -join [Environment]::NewLine
} else {
$logBox.Text = "No log file yet."
}
}
$showSecretCheck.Add_CheckedChanged({
$secretBox.UseSystemPasswordChar = -not $showSecretCheck.Checked
})
$saveSecretButton.Add_Click({
try {
$config = $configBox.Text | ConvertFrom-Json
$config.agent_secret = $secretBox.Text
$config | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigPath -Encoding UTF8
$configBox.Text = Get-Content $ConfigPath -Raw
[System.Windows.Forms.MessageBox]::Show("Secret saved. Restart the agent to apply changes.")
} catch {
[System.Windows.Forms.MessageBox]::Show("Could not save secret: " + $_.Exception.Message)
}
})
$startButton.Add_Click({
Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
Refresh-Display
})
$stopButton.Add_Click({
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
Refresh-Display
})
$restartButton.Add_Click({
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
Refresh-Display
})
$saveButton.Add_Click({
try {
$null = $configBox.Text | ConvertFrom-Json
Save-ConfigText -Text $configBox.Text
[System.Windows.Forms.MessageBox]::Show("Config saved. Restart the agent to apply changes.")
} catch {
[System.Windows.Forms.MessageBox]::Show("Invalid JSON: " + $_.Exception.Message)
}
})
$testButton.Add_Click({
try {
$config = $configBox.Text | ConvertFrom-Json
if ($secretBox.Text.Length -gt 0) {
$config.agent_secret = $secretBox.Text
}
$hub = [string]$config.hub_url
$secret = [string]$config.agent_secret
$headers = @{
"X-MinerOps-Agent-Secret" = $secret
}
$result = Invoke-RestMethod -Uri ($hub.TrimEnd("/") + "/api/agent/config") -Headers $headers -TimeoutSec 20
[System.Windows.Forms.MessageBox]::Show("Hub test OK.`nSite: " + $result.site_name)
} catch {
[System.Windows.Forms.MessageBox]::Show("Hub test failed: " + $_.Exception.Message)
}
})
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 3000
$timer.Add_Tick({ Refresh-Display })
$timer.Start()
Refresh-Display
[void]$form.ShowDialog()
File diff suppressed because it is too large. Load diff
+108
View File
@@ -0,0 +1,108 @@
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$TaskName = "MinerOpsAgent"
$GuiPath = "C:\MinerOpsAgent\MinerOpsAgentGUI.ps1"
$LogPath = "C:\MinerOpsAgent\logs\agent.log"
$DashboardUrl = "https://plexus.lussier.cc/dashboard"
function Start-Agent {
Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
}
function Stop-Agent {
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
}
function Restart-Agent {
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
}
function Open-Gui {
Start-Process powershell.exe -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$GuiPath`""
}
function Open-Dashboard {
Start-Process $DashboardUrl
}
function Open-Log {
if (Test-Path $LogPath) {
Start-Process notepad.exe $LogPath
}
}
function Get-AgentStatusText {
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if (!$task) {
return "MinerOps Agent: task not installed"
}
return "MinerOps Agent: $($task.State)"
}
$notify = New-Object System.Windows.Forms.NotifyIcon
$notify.Icon = [System.Drawing.SystemIcons]::Application
$notify.Text = Get-AgentStatusText
$notify.Visible = $true
$menu = New-Object System.Windows.Forms.ContextMenuStrip
$itemOpen = $menu.Items.Add("Open Agent GUI")
$itemOpen.Add_Click({ Open-Gui })
$itemDashboard = $menu.Items.Add("Open Plexus Dashboard")
$itemDashboard.Add_Click({ Open-Dashboard })
$menu.Items.Add("-") | Out-Null
$itemStart = $menu.Items.Add("Start Agent")
$itemStart.Add_Click({
Start-Agent
Start-Sleep -Seconds 1
$notify.Text = Get-AgentStatusText
})
$itemStop = $menu.Items.Add("Stop Agent")
$itemStop.Add_Click({
Stop-Agent
Start-Sleep -Seconds 1
$notify.Text = Get-AgentStatusText
})
$itemRestart = $menu.Items.Add("Restart Agent")
$itemRestart.Add_Click({
Restart-Agent
Start-Sleep -Seconds 1
$notify.Text = Get-AgentStatusText
})
$menu.Items.Add("-") | Out-Null
$itemLog = $menu.Items.Add("Open Log")
$itemLog.Add_Click({ Open-Log })
$itemExit = $menu.Items.Add("Exit Tray Icon")
$itemExit.Add_Click({
$notify.Visible = $false
$notify.Dispose()
[System.Windows.Forms.Application]::Exit()
})
$notify.ContextMenuStrip = $menu
$notify.Add_DoubleClick({
Open-Gui
})
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 5000
$timer.Add_Tick({
$notify.Text = Get-AgentStatusText
})
$timer.Start()
[System.Windows.Forms.Application]::Run()
@@ -0,0 +1,246 @@
param(
[int] $Limit = 25,
[int] $TimeoutMs = 2500
)
$ErrorActionPreference = "Stop"
$ConfigPath = "C:\MinerOpsAgent\agent-config.json"
if (-not (Test-Path $ConfigPath)) {
throw "Missing config file: $ConfigPath"
}
$Config = Get-Content $ConfigPath -Raw | ConvertFrom-Json
$HubUrl = [string]$Config.hub_url
$HubUrl = $HubUrl.TrimEnd("/")
$AgentId = [string]$Config.agent_id
$SiteName = [string]$Config.site_name
$Secret = [string]$Config.agent_secret
$Headers = @{
"X-MinerOps-Agent-Secret" = $Secret
}
function Invoke-WhatsMinerApi3Read {
param(
[Parameter(Mandatory=$true)] [string] $Ip,
[Parameter(Mandatory=$true)] [hashtable] $Request,
[int] $TimeoutMs = 2500
)
$client = $null
$stream = $null
try {
$json = ($Request | ConvertTo-Json -Depth 20 -Compress)
$payload = [System.Text.Encoding]::ASCII.GetBytes($json)
$length = [BitConverter]::GetBytes([Int32]$payload.Length)
$client = New-Object System.Net.Sockets.TcpClient
$client.ReceiveTimeout = $TimeoutMs
$client.SendTimeout = $TimeoutMs
$connect = $client.BeginConnect($Ip, 4433, $null, $null)
if (-not $connect.AsyncWaitHandle.WaitOne($TimeoutMs, $false)) {
try { $client.Close() } catch {}
throw "Timed out connecting to $Ip`:4433"
}
$client.EndConnect($connect)
$stream = $client.GetStream()
$stream.ReadTimeout = $TimeoutMs
$stream.WriteTimeout = $TimeoutMs
$stream.Write($length, 0, 4)
$stream.Write($payload, 0, $payload.Length)
$stream.Flush()
$lenBuf = New-Object byte[] 4
$offset = 0
while ($offset -lt 4) {
$read = $stream.Read($lenBuf, $offset, 4 - $offset)
if ($read -le 0) {
throw "No response length received."
}
$offset += $read
}
$responseLength = [BitConverter]::ToInt32($lenBuf, 0)
if ($responseLength -le 0 -or $responseLength -gt 1048576) {
throw "Invalid response length: $responseLength"
}
$buffer = New-Object byte[] $responseLength
$offset = 0
while ($offset -lt $responseLength) {
$read = $stream.Read($buffer, $offset, $responseLength - $offset)
if ($read -le 0) {
throw "Response ended early at $offset of $responseLength bytes."
}
$offset += $read
}
$raw = [System.Text.Encoding]::ASCII.GetString($buffer, 0, $responseLength)
$parsed = $raw | ConvertFrom-Json
return [pscustomobject]@{
ok = $true
parsed = $parsed
error = $null
}
}
catch {
return [pscustomobject]@{
ok = $false
parsed = $null
error = $_.Exception.Message
}
}
finally {
if ($stream) { try { $stream.Close() } catch {} }
if ($client) { try { $client.Close() } catch {} }
}
}
function Invoke-Api3OkCommand {
param(
[string] $Ip,
[hashtable] $Request
)
$result = Invoke-WhatsMinerApi3Read -Ip $Ip -Request $Request -TimeoutMs $TimeoutMs
if (-not $result.ok) {
return $null
}
if ($result.parsed.code -ne 0) {
return $null
}
return $result.parsed
}
Write-Host "Fetching known miners from $HubUrl"
$known = Invoke-RestMethod `
-Uri "$HubUrl/api/agent/known-miners" `
-Headers $Headers `
-Method Get
$miners = @($known.miners)
if ($Limit -gt 0) {
$miners = @($miners | Select-Object -First $Limit)
}
Write-Host "API3 enrichment check count: $($miners.Count)"
$items = New-Object System.Collections.Generic.List[object]
$checked = 0
$api3Ok = 0
$api3Failed = 0
foreach ($miner in $miners) {
$checked += 1
$ip = [string]$miner.ip_address
$mac = [string]$miner.mac_address
if (-not $ip) {
continue
}
Write-Host "[$checked/$($miners.Count)] $ip $mac"
$deviceInfoResult = Invoke-WhatsMinerApi3Read `
-Ip $ip `
-Request @{ cmd = "get.device.info" } `
-TimeoutMs $TimeoutMs
if (-not $deviceInfoResult.ok -or $deviceInfoResult.parsed.code -ne 0) {
$api3Failed += 1
$err = $deviceInfoResult.error
if ($deviceInfoResult.ok) {
$err = "API3 code $($deviceInfoResult.parsed.code): $($deviceInfoResult.parsed.msg)"
}
$items.Add([pscustomobject]@{
ip = $ip
mac = $mac
api3_supported = $false
api3_error = $err
device_info = $null
miner_setting = $null
miner_status = $null
fan_setting = $null
custom_data = $null
})
continue
}
$api3Ok += 1
$minerSetting = Invoke-Api3OkCommand `
-Ip $ip `
-Request @{ cmd = "get.miner.setting" }
$minerStatus = Invoke-Api3OkCommand `
-Ip $ip `
-Request @{ cmd = "get.miner.status"; param = "summary+pools+edevs" }
$fanSetting = Invoke-Api3OkCommand `
-Ip $ip `
-Request @{ cmd = "get.fan.setting" }
$customData = Invoke-Api3OkCommand `
-Ip $ip `
-Request @{ cmd = "get.device.custom_data" }
$items.Add([pscustomobject]@{
ip = $ip
mac = $mac
api3_supported = $true
api3_error = $null
device_info = $deviceInfoResult.parsed
miner_setting = $minerSetting
miner_status = $minerStatus
fan_setting = $fanSetting
custom_data = $customData
})
}
$payload = @{
agent_id = $AgentId
site_name = $SiteName
items = @($items.ToArray())
}
$json = $payload | ConvertTo-Json -Depth 80
Write-Host "Posting API3 enrichment results to hub..."
$result = Invoke-RestMethod `
-Uri "$HubUrl/api/agent/api3-enrichment" `
-Method Post `
-Headers $Headers `
-ContentType "application/json" `
-Body $json
Write-Host ""
Write-Host "Local checked: $checked"
Write-Host "API3 OK: $api3Ok"
Write-Host "API3 failed: $api3Failed"
Write-Host ""
$result | ConvertTo-Json -Depth 20
+146
View File
@@ -0,0 +1,146 @@
$ErrorActionPreference = "Stop"
$ConfigPath = "C:\MinerOpsAgent\agent-config.json"
$Config = Get-Content -Raw $ConfigPath | ConvertFrom-Json
$HubUrl = ($Config.hub_url + "").TrimEnd("/")
$AgentId = $Config.agent_id
$AgentSecret = $Config.agent_secret
$Headers = @{
"X-MinerOps-Agent-Secret" = $AgentSecret
}
function Normalize-MinerOpsIp {
param([string] $Ip)
if (-not $Ip) {
return ""
}
return ($Ip -replace "/32$", "").Trim()
}
function Invoke-MinerOps4028PoolsSimple {
param(
[Parameter(Mandatory=$true)] [string] $Ip,
[int] $Port = 4028,
[int] $TimeoutMs = 5000
)
$client = $null
$stream = $null
try {
$json = '{"command":"pools"}'
$client = New-Object System.Net.Sockets.TcpClient
$client.ReceiveTimeout = $TimeoutMs
$client.SendTimeout = $TimeoutMs
$client.Connect($Ip, $Port)
$stream = $client.GetStream()
$stream.ReadTimeout = $TimeoutMs
$stream.WriteTimeout = $TimeoutMs
$bytes = [System.Text.Encoding]::ASCII.GetBytes($json)
$stream.Write($bytes, 0, $bytes.Length)
$stream.Flush()
Start-Sleep -Milliseconds 500
$buffer = New-Object byte[] 131072
$read = $stream.Read($buffer, 0, $buffer.Length)
if ($read -le 0) {
return $null
}
$raw = [System.Text.Encoding]::ASCII.GetString($buffer, 0, $read).Trim([char]0).Trim()
if (-not $raw) {
return $null
}
$parsed = $raw | ConvertFrom-Json
if (-not $parsed.POOLS) {
return $null
}
return @($parsed.POOLS)
}
catch {
return $null
}
finally {
if ($stream) {
try { $stream.Close() } catch {}
}
if ($client) {
try { $client.Close() } catch {}
}
}
}
$response = Invoke-RestMethod -Method Get -Uri "$HubUrl/api/agent/known-miners" -Headers $Headers -TimeoutSec 60
$knownMiners = @()
if ($response.miners) {
$knownMiners = @($response.miners)
} elseif ($response.data -and $response.data.miners) {
$knownMiners = @($response.data.miners)
} elseif ($response.data -and $response.data.known_miners) {
$knownMiners = @($response.data.known_miners)
}
Write-Host "Known miners: $($knownMiners.Count)"
$poolSnapshots = New-Object System.Collections.Generic.List[object]
$count = 0
foreach ($miner in $knownMiners) {
$count++
$ip = Normalize-MinerOpsIp -Ip $miner.ip_address
if (-not $ip) {
continue
}
if (($count % 25) -eq 0) {
Write-Host "Checked $count / $($knownMiners.Count). Collected $($poolSnapshots.Count)."
}
$pools = @(Invoke-MinerOps4028PoolsSimple -Ip $ip)
if ($pools.Count -gt 0) {
$poolSnapshots.Add([pscustomobject]@{
miner_id = $miner.miner_id
ip_address = $ip
mac_address = $miner.mac_address
model = $miner.model
pools = @($pools)
})
}
}
Write-Host "Pool snapshots collected: $($poolSnapshots.Count)"
$body = @{
agent_id = $AgentId
pool_snapshots = $poolSnapshots
} | ConvertTo-Json -Depth 20
$result = Invoke-RestMethod `
-Method Post `
-Uri "$HubUrl/api/agent/pool-snapshots" `
-Headers $Headers `
-Body $body `
-ContentType "application/json" `
-TimeoutSec 180
$result | ConvertTo-Json -Depth 10
+28
View File
@@ -0,0 +1,28 @@
$LogDir = "C:\MinerOpsAgent\logs"
$LogFile = Join-Path $LogDir "pool-poller.log"
if (-not (Test-Path $LogDir)) {
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
}
$stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
try {
Add-Content -Path $LogFile -Value "$stamp Starting MinerOps pool poller."
$output = powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\MinerOpsAgent\MinerOpsPoolPoller.ps1" 2>&1
foreach ($line in $output) {
Add-Content -Path $LogFile -Value "$line"
}
$stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Add-Content -Path $LogFile -Value "$stamp Pool poller finished."
Add-Content -Path $LogFile -Value ""
}
catch {
$stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Add-Content -Path $LogFile -Value "$stamp Pool poller failed: $($_.Exception.Message)"
Add-Content -Path $LogFile -Value ""
exit 1
}
+65
View File
@@ -0,0 +1,65 @@
# MinerOps Windows Agent
The Windows Agent runs at a mining site and performs local discovery, polling,
API3 enrichment, pool polling, and approved queued miner-control operations.
MinerOps does not send API3 credentials through the hub command queue; those
credentials remain in the Agent's local `agent-config.json`.
## Recovered Baseline
The seven runtime and support scripts in this directory were recovered from the
live `C:\MinerOpsAgent` installation. `BASELINE_SHA256SUMS` records their exact
pre-change hashes. Historical backups, live configuration, state, logs, and
probe output are intentionally excluded.
## Local Files
Create `agent-config.json` from `config.example.json` on the Agent host. Never
commit that file. `site_id` is the durable MinerOps site UUID; `site_name` is
retained only for transitional display compatibility.
The Agent writes runtime state to `agent-state.json` and logs under `logs/`.
Installation and upgrades preserve all three paths.
## Security
API3 account and password values are local secrets. Missing credentials make
miner control unavailable. Passwords, API tokens, salts, and the Agent secret
must not be written to normal logs or returned to MinerOps command history.
The Internal Alpha supports one Agent identity protected by the deployment
shared secret. A second active Agent requires per-Agent credentials before it
may be enabled.
## Legacy Compatibility
The older direct Hub API3, `/agent-control`, password-change, bulk-control, and
scheduling paths remain available for compatibility and are deprecated for the
Internal Alpha. The new Miner Detail controls use only the typed control API and
Agent queue; the modular control backend does not connect directly to miners.
## Deployment
Deploy Agent compatibility before enabling Hub enforcement of the
`X-MinerOps-Agent-ID` header:
1. Copy this directory to a temporary folder on the Windows Agent host.
2. Create or update `C:\MinerOpsAgent\agent-config.json` locally. Do not pass
secrets as installer or scheduled-task arguments.
3. Run `install.ps1` from an elevated Windows PowerShell session.
4. Confirm the four scheduled tasks are registered and that Agent check-in
reports the expected non-secret `VERSION`.
5. Enable the matching Agent ID and shared secret on the Hub.
The installer copies runtime code to `C:\MinerOpsAgent`, preserves
`agent-config.json`, `agent-state.json`, and `logs\`, and registers or
updates `MinerOpsAgent`, `MinerOpsAgentTray`,
`MinerOpsApi3Enrichment`, and `MinerOpsPoolPoller`.
Before replacing existing runtime files, the installer copies them to a
timestamped folder under `C:\MinerOpsAgent\backups`. To roll back, stop the
Agent tasks, copy the selected backup's runtime files back into
`C:\MinerOpsAgent`, and restart the tasks. `uninstall.ps1` unregisters the
four tasks and removes runtime code while preserving local configuration,
state, logs, and backups.
+21
View File
@@ -0,0 +1,21 @@
{
"hub_url": "https://minerops.example.invalid",
"agent_id": "replace-with-agent-id",
"site_id": "00000000-0000-0000-0000-000000000000",
"site_name": "Example Site",
"agent_secret": "replace-with-agent-secret",
"poll_interval_seconds": 120,
"ip_ranges": [
{
"name": "Example Range",
"cidr": "192.0.2.0/24",
"enabled": false,
"api_port": 4028,
"api3_port": 4433
}
],
"api3_account": "replace-with-api3-account",
"api3_password": "replace-with-api3-password",
"api3_port": 4433
}