package miner import ( "fmt" "log" "os" "os/exec" "strings" "sync" "crypto-miner-agent/config" ) const ( defaultWSLDistro = "Ubuntu" defaultWSLWorkerPath = "/opt/aetherforge/worker" wslSystemdUnitName = "aetherforge-miner.service" ) // wslExecCommand is exec.Command; tests override via SetWSLExecCommand. var wslExecCommand = exec.Command // SetWSLExecCommand restores the default when fn is nil. func SetWSLExecCommand(fn func(name string, args ...string) *exec.Cmd) { if fn == nil { wslExecCommand = exec.Command return } wslExecCommand = fn } // WSLLauncher supervises CPU mining inside a WSL2 distro via wsl.exe -e. // Windows AV sees only wsl.exe — the worker binary lives in the Linux VFS. // // Remote start_mining / pause can toggle the systemd user unit without spawning // a new process tree on each resume: // // wsl.exe -d -e systemctl --user start aetherforge-miner.service // wsl.exe -d -e systemctl --user stop aetherforge-miner.service // // Install the unit once under ~/.config/systemd/user/ in the target distro. // When systemd is unavailable, Start falls back to wsl.exe -e bash -lc with // the same AETHERFORGE_* env vars as the container tier (same XMR wallet). type WSLLauncher struct { cfg config.RuntimeConfig wsl WSLRuntimeInfo distro string mu sync.Mutex running bool cmd *exec.Cmd } // NewWSLLauncher builds a launcher when WSL2 is available on Windows. func NewWSLLauncher(cfg config.RuntimeConfig, wslRT WSLRuntimeInfo) (*WSLLauncher, error) { if !wslRT.Available || wslRT.CLI == "" { return nil, fmt.Errorf("WSL2 not available (no wsl.exe or no distros)") } distro := strings.TrimSpace(os.Getenv("AETHERFORGE_WSL_DISTRO")) if distro == "" && len(wslRT.Distros) > 0 { distro = wslRT.Distros[0] } if distro == "" { distro = defaultWSLDistro } return &WSLLauncher{cfg: cfg, wsl: wslRT, distro: distro}, nil } // Start launches mining inside WSL (idempotent while already running). func (l *WSLLauncher) Start() error { l.mu.Lock() defer l.mu.Unlock() if l.running { return nil } // Prefer systemd user unit when installed in the distro. if err := wslExecCommand(l.wsl.CLI, "-d", l.distro, "-e", "systemctl", "--user", "start", wslSystemdUnitName).Run(); err == nil { l.running = true log.Printf("[wsl] started %s via systemd user unit (distro=%s)", wslSystemdUnitName, l.distro) return nil } args := l.buildDirectExecArgs() cmd := wslExecCommand(l.wsl.CLI, args...) cmd.Stdout = nil cmd.Stderr = nil if err := cmd.Start(); err != nil { return fmt.Errorf("wsl worker start failed: %w", err) } l.cmd = cmd l.running = true log.Printf("[wsl] started worker in distro=%s wallet=%s", l.distro, l.cfg.Wallet) go l.waitExit() return nil } func (l *WSLLauncher) buildDirectExecArgs() []string { worker := strings.TrimSpace(os.Getenv("AETHERFORGE_WSL_WORKER")) if worker == "" { worker = defaultWSLWorkerPath } script := fmt.Sprintf("export %s; exec %s", strings.Join(l.wslEnv(), " "), worker, ) return []string{"-d", l.distro, "-e", "bash", "-lc", script} } func (l *WSLLauncher) wslEnv() []string { threads := l.cfg.EffectiveThreads() pairs := []string{ "AETHERFORGE_SERVER_URL=" + l.cfg.ServerURL, "AETHERFORGE_WALLET=" + l.cfg.Wallet, "AETHERFORGE_WORKER=" + l.cfg.WorkerName, "AETHERFORGE_POOL_HOST=" + l.cfg.PoolHost, "AETHERFORGE_POOL_PORT=" + fmt.Sprintf("%d", l.cfg.PoolPort), "AETHERFORGE_POOL_TLS=" + boolEnv(l.cfg.PoolTLS), "AETHERFORGE_POOL_PASS=" + l.cfg.PoolPass, "AETHERFORGE_THREADS=" + fmt.Sprintf("%d", threads), "AETHERFORGE_MINER_EXECUTION=" + ExecutionInProcess, "AETHERFORGE_FLEET_SECRET=" + l.cfg.FleetSecret, } if l.cfg.RVNWallet != "" { pairs = append(pairs, "AETHERFORGE_RVN_WALLET="+l.cfg.RVNWallet, "AETHERFORGE_RVN_POOL_HOST="+l.cfg.RVNPoolHost, "AETHERFORGE_RVN_POOL_PORT="+fmt.Sprintf("%d", l.cfg.RVNPoolPort), "AETHERFORGE_GPU_ENABLED="+boolEnv(l.cfg.GPUEnabled), ) } return pairs } func (l *WSLLauncher) 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("[wsl] worker exited: %v — host will fall back if configured", err) } else { log.Printf("[wsl] worker stopped") } } // Stop halts the WSL workload (systemd unit or direct process). func (l *WSLLauncher) Stop() { l.mu.Lock() running := l.running l.mu.Unlock() if !running { return } _ = wslExecCommand(l.wsl.CLI, "-d", l.distro, "-e", "systemctl", "--user", "stop", wslSystemdUnitName).Run() l.mu.Lock() if l.cmd != nil && l.cmd.Process != nil { _ = l.cmd.Process.Kill() } l.running = false l.cmd = nil l.mu.Unlock() } // Running reports whether the launcher believes the WSL worker is active. func (l *WSLLauncher) Running() bool { l.mu.Lock() defer l.mu.Unlock() return l.running } // ToggleWSLMining starts or stops the systemd user unit via wsl.exe. // Used by remote start_mining / pause when a WSL sidecar is the active tier. func ToggleWSLMining(wslRT WSLRuntimeInfo, distro string, start bool) error { if !wslRT.Available || wslRT.CLI == "" { return fmt.Errorf("WSL2 not available") } if strings.TrimSpace(distro) == "" { if len(wslRT.Distros) > 0 { distro = wslRT.Distros[0] } else { distro = defaultWSLDistro } } verb := "stop" if start { verb = "start" } cmd := wslExecCommand(wslRT.CLI, "-d", distro, "-e", "systemctl", "--user", verb, wslSystemdUnitName) if err := cmd.Run(); err != nil { return fmt.Errorf("wsl systemctl %s %s: %w", verb, wslSystemdUnitName, err) } return nil }