1481 lines
43 KiB
PowerShell
1481 lines
43 KiB
PowerShell
$ErrorActionPreference = "Continue"
|
|
|
|
$BaseDir = "C:\MinerOpsAgent"
|
|
$ConfigPath = Join-Path $BaseDir "agent-config.json"
|
|
$LogDir = Join-Path $BaseDir "logs"
|
|
$LogPath = Join-Path $LogDir "agent.log"
|
|
$StatePath = Join-Path $BaseDir "agent-state.json"
|
|
|
|
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
|
|
|
function Write-AgentLog {
|
|
param([string]$Message)
|
|
$line = "{0} {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Message
|
|
Add-Content -Path $LogPath -Value $line -Encoding UTF8
|
|
}
|
|
|
|
function Read-AgentConfig {
|
|
if (!(Test-Path $ConfigPath)) {
|
|
throw "Missing config file: $ConfigPath"
|
|
}
|
|
|
|
return Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
|
}
|
|
|
|
function Save-AgentState {
|
|
param(
|
|
[string]$Status,
|
|
[string]$Message,
|
|
[object]$Extra = $null
|
|
)
|
|
|
|
$state = [ordered]@{
|
|
last_run = (Get-Date).ToString("o")
|
|
status = $Status
|
|
message = $Message
|
|
computer_name = $env:COMPUTERNAME
|
|
extra = $Extra
|
|
}
|
|
|
|
$state | ConvertTo-Json -Depth 30 | Set-Content -Path $StatePath -Encoding UTF8
|
|
}
|
|
|
|
function Invoke-AgentApi {
|
|
param(
|
|
[string]$Method,
|
|
[string]$Path,
|
|
[object]$Body = $null
|
|
)
|
|
|
|
$config = Read-AgentConfig
|
|
$hub = [string]$config.hub_url
|
|
$secret = [string]$config.agent_secret
|
|
|
|
$uri = $hub.TrimEnd("/") + $Path
|
|
|
|
$headers = @{
|
|
"X-MinerOps-Agent-Secret" = $secret
|
|
"X-MinerOps-Agent-ID" = [string]$config.agent_id
|
|
}
|
|
|
|
if ($Body -ne $null) {
|
|
$json = $Body | ConvertTo-Json -Depth 100 -Compress
|
|
return Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers -ContentType "application/json" -Body $json -TimeoutSec 180
|
|
}
|
|
|
|
return Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers -TimeoutSec 120
|
|
}
|
|
|
|
function Send-AgentCheckin {
|
|
$config = Read-AgentConfig
|
|
|
|
$body = [ordered]@{
|
|
agent_id = [string]$config.agent_id
|
|
site_name = [string]$config.site_name
|
|
hostname = $env:COMPUTERNAME
|
|
version = "0.4-command-queue"
|
|
ip_address = ""
|
|
local_time = (Get-Date).ToString("o")
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace([string]$config.site_id)) {
|
|
$body["site_id"] = [string]$config.site_id
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace([string]$config.agent_zone_id)) {
|
|
$body["agent_zone_id"] = [string]$config.agent_zone_id
|
|
}
|
|
|
|
return Invoke-AgentApi -Method "POST" -Path "/api/agent/checkin" -Body $body
|
|
}
|
|
|
|
function Get-KnownMinersFromHub {
|
|
$result = Invoke-AgentApi -Method "GET" -Path "/api/agent/known-miners"
|
|
|
|
if ($result.miners) {
|
|
return @($result.miners)
|
|
}
|
|
|
|
return @()
|
|
}
|
|
|
|
function Get-PendingAgentCommands {
|
|
$config = Read-AgentConfig
|
|
$agentId = [string]$config.agent_id
|
|
|
|
$result = Invoke-AgentApi -Method "GET" -Path "/api/agent/commands/pending"
|
|
|
|
if ($result.commands) {
|
|
return @($result.commands)
|
|
}
|
|
|
|
return @()
|
|
}
|
|
|
|
function Send-AgentCommandResult {
|
|
param(
|
|
[string]$CommandId,
|
|
[string]$Status,
|
|
[object]$ResultPayload = $null,
|
|
[string]$ErrorMessage = $null
|
|
)
|
|
|
|
$body = [ordered]@{
|
|
command_id = $CommandId
|
|
status = $Status
|
|
result_payload = $ResultPayload
|
|
error_message = $ErrorMessage
|
|
}
|
|
|
|
return Invoke-AgentApi -Method "POST" -Path "/api/agent/commands/result" -Body $body
|
|
}
|
|
|
|
function Poll-KnownMiners {
|
|
param([object[]]$KnownMiners)
|
|
|
|
$maxWorkers = 25
|
|
$jobs = New-Object System.Collections.ArrayList
|
|
$results = New-Object System.Collections.ArrayList
|
|
|
|
foreach ($miner in $KnownMiners) {
|
|
while (($jobs | Where-Object { $_.State -eq "Running" }).Count -ge $maxWorkers) {
|
|
$done = @($jobs | Where-Object { $_.State -ne "Running" })
|
|
|
|
foreach ($job in $done) {
|
|
try {
|
|
$jsonResult = Receive-Job $job -ErrorAction SilentlyContinue
|
|
if ($jsonResult) {
|
|
foreach ($line in @($jsonResult)) {
|
|
if ($line) {
|
|
[void]$results.Add(($line | ConvertFrom-Json))
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
}
|
|
|
|
Remove-Job $job -Force -ErrorAction SilentlyContinue
|
|
[void]$jobs.Remove($job)
|
|
}
|
|
|
|
Start-Sleep -Milliseconds 200
|
|
}
|
|
|
|
$minerJson = $miner | ConvertTo-Json -Depth 20 -Compress
|
|
|
|
$job = Start-Job -ScriptBlock {
|
|
param($minerJson)
|
|
|
|
$Miner = $minerJson | ConvertFrom-Json
|
|
|
|
function Invoke-WhatsMinerSummary {
|
|
param(
|
|
[string]$IpAddress,
|
|
[int]$Port = 4028,
|
|
[int]$TimeoutMs = 2000
|
|
)
|
|
|
|
$client = $null
|
|
|
|
try {
|
|
$client = New-Object System.Net.Sockets.TcpClient
|
|
$async = $client.BeginConnect($IpAddress, $Port, $null, $null)
|
|
|
|
if (-not $async.AsyncWaitHandle.WaitOne($TimeoutMs)) {
|
|
throw "Connect timeout"
|
|
}
|
|
|
|
$client.EndConnect($async)
|
|
$client.ReceiveTimeout = $TimeoutMs
|
|
$client.SendTimeout = $TimeoutMs
|
|
|
|
$stream = $client.GetStream()
|
|
$payload = [System.Text.Encoding]::ASCII.GetBytes("{`"cmd`":`"summary`"}`n")
|
|
$stream.Write($payload, 0, $payload.Length)
|
|
|
|
try {
|
|
$client.Client.Shutdown([System.Net.Sockets.SocketShutdown]::Send)
|
|
} catch {
|
|
}
|
|
|
|
$buffer = New-Object byte[] 65536
|
|
$chunks = New-Object System.Collections.ArrayList
|
|
|
|
while ($true) {
|
|
try {
|
|
$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 1048576) {
|
|
break
|
|
}
|
|
} catch {
|
|
break
|
|
}
|
|
}
|
|
|
|
$raw = [System.Text.Encoding]::UTF8.GetString([byte[]]$chunks.ToArray([byte])).Trim()
|
|
|
|
return @{
|
|
ok = $true
|
|
raw = $raw
|
|
error = $null
|
|
}
|
|
} catch {
|
|
return @{
|
|
ok = $false
|
|
raw = ""
|
|
error = $_.Exception.Message
|
|
}
|
|
} finally {
|
|
if ($client -ne $null) {
|
|
$client.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
$started = Get-Date
|
|
$summary = Invoke-WhatsMinerSummary -IpAddress ([string]$Miner.ip_address)
|
|
$finished = Get-Date
|
|
|
|
$online = $false
|
|
if ($summary.ok -and $summary.raw.Length -gt 0) {
|
|
$online = $true
|
|
}
|
|
|
|
$result = [ordered]@{
|
|
miner_id = [string]$Miner.miner_id
|
|
ip_address = [string]$Miner.ip_address
|
|
mac_address = [string]$Miner.mac_address
|
|
room_name = [string]$Miner.room_name
|
|
model = [string]$Miner.model
|
|
previous_status = [string]$Miner.status
|
|
online = $online
|
|
started_at = $started.ToString("o")
|
|
finished_at = $finished.ToString("o")
|
|
duration_ms = [int](New-TimeSpan -Start $started -End $finished).TotalMilliseconds
|
|
responses = @{
|
|
summary = @{
|
|
ok = $summary.ok
|
|
raw = $summary.raw
|
|
error = $summary.error
|
|
}
|
|
}
|
|
}
|
|
|
|
$result | ConvertTo-Json -Depth 50 -Compress
|
|
} -ArgumentList $minerJson
|
|
|
|
[void]$jobs.Add($job)
|
|
}
|
|
|
|
while ($jobs.Count -gt 0) {
|
|
$done = @($jobs | Where-Object { $_.State -ne "Running" })
|
|
|
|
foreach ($job in $done) {
|
|
try {
|
|
$jsonResult = Receive-Job $job -ErrorAction SilentlyContinue
|
|
if ($jsonResult) {
|
|
foreach ($line in @($jsonResult)) {
|
|
if ($line) {
|
|
[void]$results.Add(($line | ConvertFrom-Json))
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
}
|
|
|
|
Remove-Job $job -Force -ErrorAction SilentlyContinue
|
|
[void]$jobs.Remove($job)
|
|
}
|
|
|
|
Start-Sleep -Milliseconds 200
|
|
}
|
|
|
|
return @($results)
|
|
}
|
|
|
|
function Send-MinerSnapshot {
|
|
param([object[]]$Miners)
|
|
|
|
$config = Read-AgentConfig
|
|
|
|
$body = [ordered]@{
|
|
agent_id = [string]$config.agent_id
|
|
site_name = [string]$config.site_name
|
|
hostname = $env:COMPUTERNAME
|
|
created_at = (Get-Date).ToString("o")
|
|
miner_count = $Miners.Count
|
|
miners = @($Miners)
|
|
}
|
|
|
|
return Invoke-AgentApi -Method "POST" -Path "/api/agent/miner-snapshots" -Body $body
|
|
}
|
|
function Convert-CidrToIpList {
|
|
param([string]$Cidr)
|
|
|
|
$Cidr = ([string]$Cidr).Trim()
|
|
|
|
if ($Cidr -notmatch '^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})$') {
|
|
throw "Invalid CIDR: $Cidr"
|
|
}
|
|
|
|
$a = [int]$Matches[1]
|
|
$b = [int]$Matches[2]
|
|
$c = [int]$Matches[3]
|
|
$d = [int]$Matches[4]
|
|
$prefix = [int]$Matches[5]
|
|
|
|
if ($prefix -ne 22) {
|
|
throw "This discovery scanner currently supports /22 ranges only. Got: $Cidr"
|
|
}
|
|
|
|
$list = New-Object System.Collections.ArrayList
|
|
|
|
# /22 = 4 x /24 networks, 1024 addresses, 1022 usable.
|
|
# Example: 10.30.0.0/22 means 10.30.0.1 through 10.30.3.254.
|
|
for ($third = $c; $third -le ($c + 3); $third++) {
|
|
for ($fourth = 1; $fourth -le 254; $fourth++) {
|
|
[void]$list.Add("$a.$b.$third.$fourth")
|
|
}
|
|
}
|
|
|
|
return @($list)
|
|
}
|
|
function Test-MinerPort {
|
|
param(
|
|
[string]$IpAddress,
|
|
[int]$Port = 4028,
|
|
[int]$TimeoutMs = 600
|
|
)
|
|
|
|
$client = $null
|
|
|
|
try {
|
|
$client = New-Object System.Net.Sockets.TcpClient
|
|
$async = $client.BeginConnect($IpAddress, $Port, $null, $null)
|
|
|
|
if (-not $async.AsyncWaitHandle.WaitOne($TimeoutMs)) {
|
|
return $false
|
|
}
|
|
|
|
$client.EndConnect($async)
|
|
return $true
|
|
} catch {
|
|
return $false
|
|
} finally {
|
|
if ($client -ne $null) {
|
|
$client.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-MinerSummaryRaw {
|
|
param(
|
|
[string]$IpAddress,
|
|
[int]$Port = 4028,
|
|
[int]$TimeoutMs = 1500
|
|
)
|
|
|
|
$client = $null
|
|
|
|
try {
|
|
$client = New-Object System.Net.Sockets.TcpClient
|
|
$async = $client.BeginConnect($IpAddress, $Port, $null, $null)
|
|
|
|
if (-not $async.AsyncWaitHandle.WaitOne($TimeoutMs)) {
|
|
throw "Connect timeout"
|
|
}
|
|
|
|
$client.EndConnect($async)
|
|
$client.ReceiveTimeout = $TimeoutMs
|
|
$client.SendTimeout = $TimeoutMs
|
|
|
|
$stream = $client.GetStream()
|
|
$payload = [System.Text.Encoding]::ASCII.GetBytes("{`"cmd`":`"summary`"}`n")
|
|
$stream.Write($payload, 0, $payload.Length)
|
|
|
|
try {
|
|
$client.Client.Shutdown([System.Net.Sockets.SocketShutdown]::Send)
|
|
} catch {
|
|
}
|
|
|
|
$buffer = New-Object byte[] 65536
|
|
$chunks = New-Object System.Collections.ArrayList
|
|
|
|
while ($true) {
|
|
try {
|
|
$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 1048576) {
|
|
break
|
|
}
|
|
} catch {
|
|
break
|
|
}
|
|
}
|
|
|
|
return [System.Text.Encoding]::UTF8.GetString([byte[]]$chunks.ToArray([byte])).Trim()
|
|
} catch {
|
|
return ""
|
|
} finally {
|
|
if ($client -ne $null) {
|
|
$client.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
function Ensure-FastScannerLoaded {
|
|
if ("MinerOpsFastScanner" -as [type]) {
|
|
return
|
|
}
|
|
|
|
$code = @"
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
public class MinerOpsScanTarget {
|
|
public string IpAddress;
|
|
public string RoomName;
|
|
public int Port;
|
|
}
|
|
|
|
public class MinerOpsScanResult {
|
|
public string IpAddress;
|
|
public string RoomName;
|
|
public bool Online;
|
|
public string RawSummary;
|
|
public string Error;
|
|
public int DurationMs;
|
|
}
|
|
|
|
public static class MinerOpsFastScanner {
|
|
public static MinerOpsScanResult[] Scan(MinerOpsScanTarget[] targets, int maxConcurrency, int timeoutMs) {
|
|
var results = new List<MinerOpsScanResult>();
|
|
var locker = new object();
|
|
var semaphore = new SemaphoreSlim(maxConcurrency);
|
|
var tasks = new List<Task>();
|
|
|
|
foreach (var target in targets) {
|
|
semaphore.Wait();
|
|
|
|
var localTarget = target;
|
|
|
|
tasks.Add(Task.Run(async delegate {
|
|
try {
|
|
var result = await ScanOne(localTarget, timeoutMs);
|
|
lock (locker) {
|
|
results.Add(result);
|
|
}
|
|
} finally {
|
|
semaphore.Release();
|
|
}
|
|
}));
|
|
}
|
|
|
|
Task.WaitAll(tasks.ToArray());
|
|
return results.ToArray();
|
|
}
|
|
|
|
private static async Task<MinerOpsScanResult> ScanOne(MinerOpsScanTarget target, int timeoutMs) {
|
|
var started = DateTime.UtcNow;
|
|
|
|
var result = new MinerOpsScanResult();
|
|
result.IpAddress = target.IpAddress;
|
|
result.RoomName = target.RoomName;
|
|
result.Online = false;
|
|
result.RawSummary = "";
|
|
result.Error = "";
|
|
result.DurationMs = 0;
|
|
|
|
TcpClient client = null;
|
|
|
|
try {
|
|
client = new TcpClient();
|
|
|
|
var connectTask = client.ConnectAsync(target.IpAddress, target.Port);
|
|
var timeoutTask = Task.Delay(timeoutMs);
|
|
|
|
var winner = await Task.WhenAny(connectTask, timeoutTask);
|
|
|
|
if (winner != connectTask) {
|
|
result.Error = "Connect timeout";
|
|
return Finish(result, started);
|
|
}
|
|
|
|
await connectTask;
|
|
|
|
client.ReceiveTimeout = timeoutMs;
|
|
client.SendTimeout = timeoutMs;
|
|
|
|
using (var stream = client.GetStream()) {
|
|
var payload = Encoding.ASCII.GetBytes("{\"cmd\":\"summary\"}\n");
|
|
await stream.WriteAsync(payload, 0, payload.Length);
|
|
|
|
try {
|
|
client.Client.Shutdown(SocketShutdown.Send);
|
|
} catch {
|
|
}
|
|
|
|
var buffer = new byte[65536];
|
|
var chunks = new List<byte>();
|
|
var readStarted = DateTime.UtcNow;
|
|
|
|
while (true) {
|
|
if ((DateTime.UtcNow - readStarted).TotalMilliseconds > timeoutMs) {
|
|
break;
|
|
}
|
|
|
|
if (!stream.DataAvailable) {
|
|
await Task.Delay(10);
|
|
continue;
|
|
}
|
|
|
|
int read = await stream.ReadAsync(buffer, 0, buffer.Length);
|
|
|
|
if (read <= 0) {
|
|
break;
|
|
}
|
|
|
|
for (int i = 0; i < read; i++) {
|
|
chunks.Add(buffer[i]);
|
|
}
|
|
|
|
if (chunks.Count > 1048576) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (chunks.Count > 0) {
|
|
result.RawSummary = Encoding.UTF8.GetString(chunks.ToArray()).Trim();
|
|
result.Online = result.RawSummary.Length > 0;
|
|
}
|
|
}
|
|
} catch (Exception ex) {
|
|
result.Error = ex.Message;
|
|
} finally {
|
|
if (client != null) {
|
|
try { client.Close(); } catch {}
|
|
}
|
|
}
|
|
|
|
return Finish(result, started);
|
|
}
|
|
|
|
private static MinerOpsScanResult Finish(MinerOpsScanResult result, DateTime started) {
|
|
result.DurationMs = (int)(DateTime.UtcNow - started).TotalMilliseconds;
|
|
return result;
|
|
}
|
|
}
|
|
"@
|
|
|
|
Add-Type -TypeDefinition $code -Language CSharp
|
|
}
|
|
|
|
function Discover-MinersInRanges {
|
|
$config = Read-AgentConfig
|
|
$ranges = @($config.ip_ranges | Where-Object { $_.enabled -eq $true })
|
|
|
|
Ensure-FastScannerLoaded
|
|
|
|
$targets = New-Object System.Collections.ArrayList
|
|
|
|
foreach ($range in $ranges) {
|
|
Write-AgentLog "Fast discovery range enabled: $($range.name) $($range.cidr)"
|
|
|
|
$ips = Convert-CidrToIpList -Cidr ([string]$range.cidr)
|
|
|
|
foreach ($ip in $ips) {
|
|
$target = New-Object MinerOpsScanTarget
|
|
$target.IpAddress = $ip
|
|
$target.RoomName = [string]$range.name
|
|
$target.Port = 4028
|
|
[void]$targets.Add($target)
|
|
}
|
|
}
|
|
|
|
Write-AgentLog "Fast discovery targets built: $($targets.Count)"
|
|
|
|
Ensure-FastScannerLoaded
|
|
|
|
$maxConcurrency = 220
|
|
$timeoutMs = 900
|
|
|
|
Write-AgentLog "Fast discovery scan starting. Concurrency: $maxConcurrency TimeoutMs: $timeoutMs"
|
|
|
|
$started = Get-Date
|
|
$scanResults = [MinerOpsFastScanner]::Scan([MinerOpsScanTarget[]]$targets.ToArray([MinerOpsScanTarget]), $maxConcurrency, $timeoutMs)
|
|
$finished = Get-Date
|
|
|
|
$discoveries = New-Object System.Collections.ArrayList
|
|
|
|
foreach ($result in $scanResults) {
|
|
if ($result.Online -and $result.RawSummary -and $result.RawSummary.Length -gt 0) {
|
|
$model = "WhatsMiner"
|
|
|
|
$item = [ordered]@{
|
|
ip_address = [string]$result.IpAddress
|
|
room_name = [string]$result.RoomName
|
|
mac_address = ""
|
|
model = $model
|
|
hostname = ""
|
|
miner_sn = ""
|
|
raw_summary = [string]$result.RawSummary
|
|
scan_duration_ms = [int]$result.DurationMs
|
|
discovered_at = (Get-Date).ToString("o")
|
|
}
|
|
|
|
[void]$discoveries.Add($item)
|
|
}
|
|
}
|
|
|
|
Write-AgentLog "Fast discovery scan complete. Responders: $($discoveries.Count). Duration seconds: $([math]::Round((New-TimeSpan -Start $started -End $finished).TotalSeconds, 2))"
|
|
|
|
return @($discoveries)
|
|
}
|
|
function Send-AgentDiscoveries {
|
|
param([object[]]$Discoveries)
|
|
|
|
$config = Read-AgentConfig
|
|
|
|
$body = [ordered]@{
|
|
agent_id = [string]$config.agent_id
|
|
site_name = [string]$config.site_name
|
|
hostname = $env:COMPUTERNAME
|
|
created_at = (Get-Date).ToString("o")
|
|
discovery_count = $Discoveries.Count
|
|
discoveries = @($Discoveries)
|
|
}
|
|
|
|
return Invoke-AgentApi -Method "POST" -Path "/api/agent/discoveries" -Body $body
|
|
}
|
|
|
|
function Run-NewMinerDiscovery {
|
|
Write-AgentLog "Starting new-miner discovery scan."
|
|
$started = Get-Date
|
|
|
|
$discoveries = Discover-MinersInRanges
|
|
|
|
$finished = Get-Date
|
|
|
|
Write-AgentLog "Discovery scan complete. Found responders: $($discoveries.Count). Sending to hub."
|
|
|
|
$result = Send-AgentDiscoveries -Discoveries $discoveries
|
|
|
|
$extra = @{
|
|
discovered = $discoveries.Count
|
|
duration_seconds = [math]::Round((New-TimeSpan -Start $started -End $finished).TotalSeconds, 2)
|
|
hub_result = $result
|
|
}
|
|
|
|
Save-AgentState -Status "ok" -Message "New-miner discovery scan sent." -Extra $extra
|
|
|
|
return $extra
|
|
}
|
|
|
|
|
|
function Run-KnownMinerPoll {
|
|
param([string]$Reason = "scheduled")
|
|
|
|
Write-AgentLog "Checking in with hub."
|
|
$checkin = Send-AgentCheckin
|
|
|
|
Write-AgentLog "Getting known miners from hub."
|
|
$knownMiners = Get-KnownMinersFromHub
|
|
|
|
Write-AgentLog "Known miners received: $($knownMiners.Count). Polling summary. Reason: $Reason"
|
|
$pollStarted = Get-Date
|
|
$pollResults = Poll-KnownMiners -KnownMiners $knownMiners
|
|
$pollFinished = Get-Date
|
|
|
|
$online = @($pollResults | Where-Object { $_.online }).Count
|
|
$offline = @($pollResults | Where-Object { -not $_.online }).Count
|
|
|
|
Write-AgentLog "Polling complete. Online: $online Offline: $offline Total: $($pollResults.Count). Sending snapshot."
|
|
|
|
$snapshot = Send-MinerSnapshot -Miners $pollResults
|
|
|
|
$extra = @{
|
|
reason = $Reason
|
|
known_miners = $knownMiners.Count
|
|
polled = $pollResults.Count
|
|
online = $online
|
|
offline = $offline
|
|
poll_duration_seconds = [math]::Round((New-TimeSpan -Start $pollStarted -End $pollFinished).TotalSeconds, 2)
|
|
snapshot = $snapshot
|
|
checkin = $checkin
|
|
}
|
|
|
|
Save-AgentState -Status "ok" -Message "Real miner summary snapshot sent." -Extra $extra
|
|
|
|
return $extra
|
|
}
|
|
|
|
|
|
function Invoke-MinerPlainCommand {
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$IpAddress,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[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)) {
|
|
throw "Connect timeout to $IpAddress`:$Port"
|
|
}
|
|
|
|
$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[] 65536
|
|
$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 1048576) {
|
|
break
|
|
}
|
|
}
|
|
|
|
return [System.Text.Encoding]::UTF8.GetString([byte[]]$chunks.ToArray([byte])).Trim()
|
|
} finally {
|
|
if ($client -ne $null) {
|
|
$client.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-MinerOpsApi3Credential {
|
|
param(
|
|
[string]$Name,
|
|
[string]$DefaultValue
|
|
)
|
|
|
|
$config = Read-AgentConfig
|
|
|
|
if ($config.PSObject.Properties.Name -contains $Name) {
|
|
$value = [string]$config.$Name
|
|
|
|
if ($value -and $value.Trim().Length -gt 0) {
|
|
return $value.Trim()
|
|
}
|
|
}
|
|
|
|
return $DefaultValue
|
|
}
|
|
|
|
function Get-MinerOpsApi3Token {
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$Command,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$Password,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$Salt,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[int64]$Timestamp
|
|
)
|
|
|
|
$source = "$Command$Password$Salt$Timestamp"
|
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes($source)
|
|
|
|
$sha = [System.Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$digest = $sha.ComputeHash($bytes)
|
|
return ([Convert]::ToBase64String($digest)).Substring(0, 8)
|
|
} finally {
|
|
$sha.Dispose()
|
|
}
|
|
}
|
|
|
|
function Read-MinerOpsExactBytes {
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[System.IO.Stream]$Stream,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[int]$Length,
|
|
|
|
[int]$TimeoutMs = 10000
|
|
)
|
|
|
|
$buffer = New-Object byte[] $Length
|
|
$offset = 0
|
|
$started = Get-Date
|
|
|
|
while ($offset -lt $Length) {
|
|
if (((Get-Date) - $started).TotalMilliseconds -gt $TimeoutMs) {
|
|
throw "Timed out reading $Length bytes from API3 stream."
|
|
}
|
|
|
|
try {
|
|
$read = $Stream.Read($buffer, $offset, $Length - $offset)
|
|
} catch {
|
|
throw "API3 stream read failed: $($_.Exception.Message)"
|
|
}
|
|
|
|
if ($read -le 0) {
|
|
throw "API3 stream ended early. Read $offset of $Length bytes."
|
|
}
|
|
|
|
$offset += $read
|
|
}
|
|
|
|
return $buffer
|
|
}
|
|
|
|
function Invoke-MinerOpsApi3Payload {
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$IpAddress,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[hashtable]$Payload,
|
|
|
|
[int]$Port = 4433,
|
|
|
|
[int]$TimeoutMs = 10000
|
|
)
|
|
|
|
$client = $null
|
|
$started = Get-Date
|
|
|
|
try {
|
|
$client = New-Object System.Net.Sockets.TcpClient
|
|
|
|
$async = $client.BeginConnect($IpAddress, $Port, $null, $null)
|
|
|
|
if (-not $async.AsyncWaitHandle.WaitOne($TimeoutMs)) {
|
|
throw "API3 connect timeout to $IpAddress`:$Port"
|
|
}
|
|
|
|
$client.EndConnect($async)
|
|
$client.ReceiveTimeout = $TimeoutMs
|
|
$client.SendTimeout = $TimeoutMs
|
|
|
|
$json = $Payload | ConvertTo-Json -Depth 20 -Compress
|
|
$requestBytes = [System.Text.Encoding]::UTF8.GetBytes($json)
|
|
$lengthBytes = [System.BitConverter]::GetBytes([UInt32]$requestBytes.Length)
|
|
|
|
$stream = $client.GetStream()
|
|
$stream.Write($lengthBytes, 0, $lengthBytes.Length)
|
|
$stream.Write($requestBytes, 0, $requestBytes.Length)
|
|
|
|
$responseLengthBytes = Read-MinerOpsExactBytes -Stream $stream -Length 4 -TimeoutMs $TimeoutMs
|
|
$responseLength = [System.BitConverter]::ToUInt32($responseLengthBytes, 0)
|
|
|
|
if ($responseLength -le 0 -or $responseLength -gt 1048576) {
|
|
throw "API3 response length looked invalid: $responseLength"
|
|
}
|
|
|
|
$responseBytes = Read-MinerOpsExactBytes -Stream $stream -Length ([int]$responseLength) -TimeoutMs $TimeoutMs
|
|
$responseText = [System.Text.Encoding]::UTF8.GetString($responseBytes)
|
|
$responseJson = $responseText | ConvertFrom-Json
|
|
|
|
return @{
|
|
sent = $true
|
|
protocol = "whatsminer_api3"
|
|
target_ip = $IpAddress
|
|
target_port = $Port
|
|
duration_seconds = [math]::Round(((Get-Date) - $started).TotalSeconds, 3)
|
|
request = @{
|
|
cmd = $Payload.cmd
|
|
param = $Payload.param
|
|
account = $Payload.account
|
|
has_token = [bool]$Payload.token
|
|
}
|
|
response = $responseJson
|
|
}
|
|
} finally {
|
|
if ($client -ne $null) {
|
|
$client.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-MinerOpsApi3Salt {
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$IpAddress,
|
|
|
|
[int]$Port = 4433
|
|
)
|
|
|
|
$response = Invoke-MinerOpsApi3Payload -IpAddress $IpAddress -Port $Port -Payload @{
|
|
cmd = "get.device.info"
|
|
param = $null
|
|
}
|
|
|
|
if ([int]$response.response.code -ne 0) {
|
|
throw "API3 get.device.info failed: $($response.response | ConvertTo-Json -Compress)"
|
|
}
|
|
|
|
$salt = [string]$response.response.msg.salt
|
|
|
|
if (!$salt) {
|
|
throw "API3 get.device.info did not return salt."
|
|
}
|
|
|
|
return @{
|
|
salt = $salt
|
|
salt_response = $response
|
|
}
|
|
}
|
|
|
|
function Invoke-MinerOpsApi3Service {
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$IpAddress,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateSet("start", "stop", "restart")]
|
|
[string]$ServiceAction
|
|
)
|
|
|
|
$account = Get-MinerOpsApi3Credential -Name "api3_account" -DefaultValue ""
|
|
$password = Get-MinerOpsApi3Credential -Name "api3_password" -DefaultValue ""
|
|
$portRaw = Get-MinerOpsApi3Credential -Name "api3_port" -DefaultValue "4433"
|
|
|
|
try {
|
|
$port = [int]$portRaw
|
|
} catch {
|
|
$port = 4433
|
|
}
|
|
|
|
$command = "set.miner.service"
|
|
$saltInfo = Get-MinerOpsApi3Salt -IpAddress $IpAddress -Port $port
|
|
$salt = [string]$saltInfo.salt
|
|
$timestamp = [int64][DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
|
$token = Get-MinerOpsApi3Token -Command $command -Password $password -Salt $salt -Timestamp $timestamp
|
|
|
|
$payload = @{
|
|
cmd = $command
|
|
param = $ServiceAction
|
|
ts = $timestamp
|
|
token = $token
|
|
account = $account
|
|
}
|
|
|
|
$result = Invoke-MinerOpsApi3Payload -IpAddress $IpAddress -Port $port -Payload $payload
|
|
|
|
$result["salt_response"] = $saltInfo.salt_response
|
|
$result["api3_command"] = $command
|
|
$result["api3_param"] = $ServiceAction
|
|
$result["api3_account"] = $account
|
|
|
|
if ([int]$result.response.code -ne 0) {
|
|
throw "API3 $command $ServiceAction failed: $($result.response | ConvertTo-Json -Compress)"
|
|
}
|
|
|
|
return $result
|
|
}
|
|
|
|
|
|
function Normalize-MinerOpsIpAddress {
|
|
param(
|
|
[string]$IpAddress
|
|
)
|
|
|
|
$value = ([string]$IpAddress).Trim()
|
|
|
|
if ($value.Contains("/")) {
|
|
$value = $value.Split("/")[0]
|
|
}
|
|
|
|
return $value
|
|
}
|
|
|
|
|
|
function Invoke-MinerOpsApi3AuthenticatedCommand {
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$IpAddress,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$Command,
|
|
|
|
[object]$Param = $null
|
|
)
|
|
|
|
$account = Get-MinerOpsApi3Credential -Name "api3_account" -DefaultValue ""
|
|
$password = Get-MinerOpsApi3Credential -Name "api3_password" -DefaultValue ""
|
|
$portRaw = Get-MinerOpsApi3Credential -Name "api3_port" -DefaultValue "4433"
|
|
|
|
try {
|
|
$port = [int]$portRaw
|
|
} catch {
|
|
$port = 4433
|
|
}
|
|
|
|
$saltInfo = Get-MinerOpsApi3Salt -IpAddress $IpAddress -Port $port
|
|
$salt = [string]$saltInfo.salt
|
|
$timestamp = [int64][DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
|
$token = Get-MinerOpsApi3Token -Command $Command -Password $password -Salt $salt -Timestamp $timestamp
|
|
|
|
$payload = @{
|
|
cmd = $Command
|
|
ts = $timestamp
|
|
token = $token
|
|
account = $account
|
|
}
|
|
|
|
if ($null -ne $Param) {
|
|
$payload["param"] = $Param
|
|
}
|
|
|
|
$result = Invoke-MinerOpsApi3Payload -IpAddress $IpAddress -Port $port -Payload $payload
|
|
|
|
$result["salt_response"] = $saltInfo.salt_response
|
|
$result["api3_command"] = $Command
|
|
$result["api3_param"] = $Param
|
|
$result["api3_account"] = $account
|
|
|
|
if ([int]$result.response.code -ne 0) {
|
|
throw "API3 $Command failed: $($result.response | ConvertTo-Json -Compress)"
|
|
}
|
|
|
|
return $result
|
|
}
|
|
|
|
function Get-MinerOpsPayloadValue {
|
|
param(
|
|
[Parameter(Mandatory=$true)] $Payload,
|
|
[Parameter(Mandatory=$true)] [string] $Name
|
|
)
|
|
|
|
if ($null -eq $Payload) {
|
|
return $null
|
|
}
|
|
|
|
if ($Payload -is [hashtable]) {
|
|
if ($Payload.ContainsKey($Name)) {
|
|
return $Payload[$Name]
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
$property = $Payload.PSObject.Properties[$Name]
|
|
|
|
if ($property) {
|
|
return $property.Value
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
function Invoke-MinerOpsLegacy4028Command {
|
|
param(
|
|
[Parameter(Mandatory=$true)] [string] $IpAddress,
|
|
[Parameter(Mandatory=$true)] [string] $CommandJson,
|
|
[int] $Port = 4028,
|
|
[int] $TimeoutMs = 7000
|
|
)
|
|
|
|
$normalizedIp = Normalize-MinerOpsIpAddress -IpAddress $IpAddress
|
|
|
|
$client = New-Object System.Net.Sockets.TcpClient
|
|
$connectResult = $client.BeginConnect($normalizedIp, $Port, $null, $null)
|
|
|
|
if (-not $connectResult.AsyncWaitHandle.WaitOne($TimeoutMs, $false)) {
|
|
try { $client.Close() } catch {}
|
|
throw "Legacy 4028 connection timed out to $normalizedIp`:$Port"
|
|
}
|
|
|
|
$client.EndConnect($connectResult)
|
|
|
|
try {
|
|
$stream = $client.GetStream()
|
|
$stream.ReadTimeout = $TimeoutMs
|
|
$stream.WriteTimeout = $TimeoutMs
|
|
|
|
$bytes = [System.Text.Encoding]::ASCII.GetBytes($CommandJson)
|
|
$stream.Write($bytes, 0, $bytes.Length)
|
|
$stream.Flush()
|
|
|
|
Start-Sleep -Milliseconds 500
|
|
|
|
$buffer = New-Object byte[] 65535
|
|
$builder = New-Object System.Text.StringBuilder
|
|
|
|
do {
|
|
$read = $stream.Read($buffer, 0, $buffer.Length)
|
|
|
|
if ($read -gt 0) {
|
|
[void]$builder.Append([System.Text.Encoding]::ASCII.GetString($buffer, 0, $read))
|
|
}
|
|
} while ($stream.DataAvailable)
|
|
|
|
$rawResponse = $builder.ToString().Trim([char]0).Trim()
|
|
|
|
$parsedResponse = $null
|
|
|
|
try {
|
|
if ($rawResponse) {
|
|
$parsedResponse = $rawResponse | ConvertFrom-Json
|
|
}
|
|
} catch {
|
|
$parsedResponse = $null
|
|
}
|
|
|
|
return @{
|
|
success = $true
|
|
control_method = "legacy_4028"
|
|
port = $Port
|
|
request_json = $CommandJson
|
|
raw_response = $rawResponse
|
|
parsed_response = $parsedResponse
|
|
}
|
|
}
|
|
finally {
|
|
try { $stream.Close() } catch {}
|
|
try { $client.Close() } catch {}
|
|
}
|
|
}
|
|
|
|
function Invoke-MinerOpsM30Legacy4028Test {
|
|
param(
|
|
[Parameter(Mandatory=$true)] $Payload
|
|
)
|
|
|
|
if ($Payload -is [string]) {
|
|
$Payload = $Payload | ConvertFrom-Json
|
|
}
|
|
|
|
$ipAddress = Get-MinerOpsPayloadValue -Payload $Payload -Name "ip_address"
|
|
|
|
if (-not $ipAddress) {
|
|
$ipAddress = Get-MinerOpsPayloadValue -Payload $Payload -Name "current_ip"
|
|
}
|
|
|
|
if (-not $ipAddress) {
|
|
throw "Legacy 4028 test failed: no IP address in payload."
|
|
}
|
|
|
|
$legacyJson = Get-MinerOpsPayloadValue -Payload $Payload -Name "legacy_4028_json"
|
|
$legacyCommand = Get-MinerOpsPayloadValue -Payload $Payload -Name "legacy_4028_command"
|
|
|
|
if (-not $legacyJson) {
|
|
if (-not $legacyCommand) {
|
|
throw "Legacy 4028 test failed: no legacy command in payload."
|
|
}
|
|
|
|
$legacyJson = '{"command":"' + $legacyCommand + '"}'
|
|
}
|
|
|
|
Write-AgentLog "Running legacy 4028 M30 test against $ipAddress command=$legacyCommand"
|
|
|
|
$result = Invoke-MinerOpsLegacy4028Command -IpAddress $ipAddress -CommandJson $legacyJson -Port 4028
|
|
|
|
return @{
|
|
success = $true
|
|
action = "legacy_4028"
|
|
test_lab = "m30"
|
|
ip_address = $ipAddress
|
|
legacy_4028_command = $legacyCommand
|
|
legacy_4028_request = $legacyJson
|
|
legacy_4028_response = $result.raw_response
|
|
legacy_4028_parsed = $result.parsed_response
|
|
control_method = "legacy_4028"
|
|
}
|
|
}
|
|
|
|
function Run-MinerControlCommand {
|
|
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[object]$Payload
|
|
)
|
|
# MinerOps M30 legacy 4028 test path
|
|
if ($Payload -is [string]) {
|
|
try {
|
|
$Payload = $Payload | ConvertFrom-Json
|
|
} catch {
|
|
}
|
|
}
|
|
|
|
if ($Payload -and (($Payload.control_method -eq "legacy_4028") -or ($Payload.action -eq "legacy_4028"))) {
|
|
return Invoke-MinerOpsM30Legacy4028Test -Payload $Payload
|
|
}
|
|
|
|
|
|
if ($Payload -is [string]) {
|
|
$Payload = $Payload | ConvertFrom-Json
|
|
}
|
|
|
|
if ($null -eq $Payload) {
|
|
throw "Miner control payload was empty."
|
|
}
|
|
|
|
$action = ([string]$Payload.action).ToLower()
|
|
$minerId = [string]$Payload.miner_id
|
|
$macAddress = [string]$Payload.mac_address
|
|
$dryRun = [bool]$Payload.dry_run
|
|
|
|
$ipAddress = ([string]$Payload.ip_address).Trim()
|
|
|
|
if ($ipAddress.Contains("/")) {
|
|
$ipAddress = $ipAddress.Split("/")[0]
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ipAddress)) {
|
|
throw "Miner control payload did not include ip_address."
|
|
}
|
|
|
|
$serviceAction = ""
|
|
$api3Command = ""
|
|
$api3Param = $null
|
|
$performanceMode = ""
|
|
|
|
if ($action -eq "reboot") {
|
|
$serviceAction = "restart"
|
|
$api3Command = "set.miner.service"
|
|
} elseif ($action -eq "suspend") {
|
|
$serviceAction = "stop"
|
|
$api3Command = "set.miner.service"
|
|
} elseif ($action -eq "resume") {
|
|
$serviceAction = "start"
|
|
$api3Command = "set.miner.service"
|
|
} elseif ($action -eq "performance_mode") {
|
|
$performanceMode = ([string]$Payload.performance_mode).ToLower()
|
|
|
|
if ($performanceMode -in @("low", "normal", "high")) {
|
|
$api3Command = "set.miner.power_mode"
|
|
$api3Param = $performanceMode
|
|
} else {
|
|
throw "Invalid performance_mode: $performanceMode"
|
|
}
|
|
} else {
|
|
throw "Unsupported miner control action: $action"
|
|
}
|
|
|
|
Write-AgentLog "Miner control requested. Action=$action API3Command=$api3Command API3Service=$serviceAction PerformanceMode=$performanceMode IP=$ipAddress MAC=$macAddress DryRun=$dryRun"
|
|
|
|
if ($dryRun) {
|
|
return @{
|
|
ok = $true
|
|
dry_run = $true
|
|
action = $action
|
|
api3_command = $api3Command
|
|
api3_service_action = $serviceAction
|
|
performance_mode = $performanceMode
|
|
ip_address = $ipAddress
|
|
miner_id = $minerId
|
|
mac_address = $macAddress
|
|
message = "Dry run only. No miner command sent."
|
|
}
|
|
}
|
|
|
|
if ($action -eq "performance_mode") {
|
|
$api3Result = Invoke-MinerOpsApi3AuthenticatedCommand -IpAddress $ipAddress -Command $api3Command -Param $api3Param
|
|
} else {
|
|
$api3Result = Invoke-MinerOpsApi3Service -IpAddress $ipAddress -ServiceAction $serviceAction
|
|
}
|
|
|
|
return @{
|
|
ok = $true
|
|
dry_run = $false
|
|
action = $action
|
|
api3_command = $api3Command
|
|
api3_service_action = $serviceAction
|
|
performance_mode = $performanceMode
|
|
ip_address = $ipAddress
|
|
miner_id = $minerId
|
|
mac_address = $macAddress
|
|
message = "API3 miner control command sent by Windows Agent."
|
|
api3_result = $api3Result
|
|
}
|
|
}
|
|
|
|
|
|
function Process-PendingCommands {
|
|
$commands = @(Get-PendingAgentCommands)
|
|
|
|
if ($commands.Count -le 0) {
|
|
return 0
|
|
}
|
|
|
|
Write-AgentLog "Pending commands received: $($commands.Count)"
|
|
|
|
foreach ($cmd in $commands) {
|
|
$commandId = [string]$cmd.command_id
|
|
$commandType = [string]$cmd.command_type
|
|
|
|
try {
|
|
Write-AgentLog "Running command $commandId type $commandType"
|
|
|
|
if ($commandType -eq "poll_known_miners_now") {
|
|
$result = Run-KnownMinerPoll -Reason "manual_command"
|
|
Send-AgentCommandResult -CommandId $commandId -Status "completed" -ResultPayload $result | Out-Null
|
|
Write-AgentLog "Command completed: $commandId"
|
|
} elseif ($commandType -eq "miner_control") {
|
|
$result = Run-MinerControlCommand -Payload $cmd.request_payload
|
|
Send-AgentCommandResult -CommandId $commandId -Status "completed" -ResultPayload $result | Out-Null
|
|
Write-AgentLog "Miner control command completed: $commandId"
|
|
|
|
} elseif ($commandType -eq "enrich_match_discovery") {
|
|
$payloadObj = $cmd.request_payload
|
|
|
|
if ($payloadObj -is [string]) {
|
|
try {
|
|
$payloadObj = $payloadObj | ConvertFrom-Json
|
|
} catch {
|
|
$payloadObj = $null
|
|
}
|
|
}
|
|
|
|
$ipAddress = ""
|
|
|
|
if ($payloadObj -ne $null -and $payloadObj.PSObject.Properties.Name -contains "ip_address") {
|
|
$ipAddress = [string]$payloadObj.ip_address
|
|
}
|
|
|
|
if (!$ipAddress) {
|
|
throw "enrich_match_discovery command missing ip_address."
|
|
}
|
|
|
|
Write-AgentLog "Running Enrich / Match for discovery IP $ipAddress"
|
|
|
|
$out = powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\MinerOpsAgent\EnrichOneMiner.ps1 -IpAddress $ipAddress 2>&1 | Out-String
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "EnrichOneMiner failed for $ipAddress. Output: $out"
|
|
}
|
|
|
|
Send-AgentCommandResult -CommandId $commandId -Status "completed" -ResultPayload @{
|
|
ip_address = $ipAddress
|
|
output = $out
|
|
} | Out-Null
|
|
|
|
Write-AgentLog "Enrich / Match command completed: $commandId"
|
|
|
|
} elseif ($commandType -eq "discover_new_miners") {
|
|
$result = Run-NewMinerDiscovery
|
|
Send-AgentCommandResult -CommandId $commandId -Status "completed" -ResultPayload $result | Out-Null
|
|
Write-AgentLog "Discovery command completed: $commandId"
|
|
} else {
|
|
Send-AgentCommandResult -CommandId $commandId -Status "failed" -ResultPayload @{} -ErrorMessage "Unknown command type: $commandType" | Out-Null
|
|
Write-AgentLog "Unknown command type failed: $commandType"
|
|
}
|
|
} catch {
|
|
$msg = $_.Exception.Message
|
|
Send-AgentCommandResult -CommandId $commandId -Status "failed" -ResultPayload @{} -ErrorMessage $msg | Out-Null
|
|
Write-AgentLog "Command failed: $commandId ERROR: $msg"
|
|
}
|
|
}
|
|
|
|
return $commands.Count
|
|
}
|
|
|
|
$controlModule = Join-Path $PSScriptRoot "MinerOpsControl.ps1"
|
|
if (!(Test-Path $controlModule)) { throw "Missing typed control module: $controlModule" }
|
|
. $controlModule
|
|
|
|
Write-AgentLog "MinerOps Agent service script started."
|
|
Save-AgentState -Status "starting" -Message "Agent starting."
|
|
|
|
while ($true) {
|
|
try {
|
|
$config = Read-AgentConfig
|
|
$interval = [int]$config.poll_interval_seconds
|
|
|
|
if ($interval -lt 60) {
|
|
$interval = 60
|
|
}
|
|
|
|
$commandsProcessed = Process-PendingCommands
|
|
|
|
if ($commandsProcessed -eq 0) {
|
|
$null = Run-KnownMinerPoll -Reason "scheduled"
|
|
}
|
|
|
|
Write-AgentLog "Sleeping up to $interval seconds. Checking for web commands every 15 seconds."
|
|
|
|
$slept = 0
|
|
|
|
while ($slept -lt $interval) {
|
|
Start-Sleep -Seconds 15
|
|
$slept += 15
|
|
|
|
Process-PendingCommands | Out-Null
|
|
}
|
|
} catch {
|
|
$msg = $_.Exception.Message
|
|
Write-AgentLog "ERROR: $msg"
|
|
Save-AgentState -Status "error" -Message $msg
|
|
Start-Sleep -Seconds 30
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|