zlh-agent/internal/state/state.go
2026-03-15 11:06:08 +00:00

320 lines
7.0 KiB
Go
Executable File

package state
import (
"encoding/json"
"fmt"
"log"
"os"
"strconv"
"sync"
"time"
)
/* --------------------------------------------------------------------------
CONFIG STRUCT
----------------------------------------------------------------------------*/
type Config struct {
VMID int `json:"vmid"`
// Container identity
ContainerType string `json:"container_type,omitempty"`
// Dev runtime (only for dev containers)
Runtime string `json:"runtime,omitempty"`
Version string `json:"version"`
// OPTIONAL addons (role-agnostic)
Addons []string `json:"addons,omitempty"`
EnableCodeServer bool `json:"enable_code_server,omitempty"`
Game string `json:"game"`
Variant string `json:"variant"`
World string `json:"world"`
Ports []int `json:"ports"`
ArtifactPath string `json:"artifact_path"`
JavaPath string `json:"java_path"`
MemoryMB int `json:"memory_mb"`
// Steam + admin credentials
SteamUser string `json:"steam_user,omitempty"`
SteamPass string `json:"steam_pass,omitempty"`
SteamAuth string `json:"steam_auth,omitempty"`
AdminUser string `json:"admin_user,omitempty"`
AdminPass string `json:"admin_pass,omitempty"`
}
/* --------------------------------------------------------------------------
AGENT STATE ENUM
----------------------------------------------------------------------------*/
type AgentState string
const (
StateIdle AgentState = "idle"
StateInstalling AgentState = "installing"
StateStarting AgentState = "starting"
StateRunning AgentState = "running"
StateStopping AgentState = "stopping"
StateCrashed AgentState = "crashed"
StateError AgentState = "error"
)
/* --------------------------------------------------------------------------
GLOBAL STATE STRUCT
----------------------------------------------------------------------------*/
type agentStatus struct {
mu sync.Mutex
state AgentState
lastChange time.Time
installStep string
lastError error
crashCount int
lastCrash time.Time
lastCrashInfo *CrashInfo
intentionalStop bool
ready bool
readySource string
readyError string
lastReadyAt time.Time
}
type CrashInfo struct {
Time time.Time `json:"time"`
ExitCode int `json:"exitCode"`
Signal int `json:"signal"`
UptimeSeconds int64 `json:"uptimeSeconds"`
LogTail []string `json:"logTail"`
}
var global = &agentStatus{
state: StateIdle,
lastChange: time.Now(),
}
func stateLogf(format string, args ...any) {
if cfg, err := LoadConfig(); err == nil && cfg != nil {
log.Printf("[state] vmid=%d "+format, append([]any{cfg.VMID}, args...)...)
return
}
log.Printf("[state] "+format, args...)
}
/* --------------------------------------------------------------------------
STATE GETTERS
----------------------------------------------------------------------------*/
func GetState() AgentState {
global.mu.Lock()
defer global.mu.Unlock()
return global.state
}
func GetInstallStep() string {
global.mu.Lock()
defer global.mu.Unlock()
return global.installStep
}
func GetError() error {
global.mu.Lock()
defer global.mu.Unlock()
return global.lastError
}
func GetCrashCount() int {
global.mu.Lock()
defer global.mu.Unlock()
return global.crashCount
}
func GetLastChange() time.Time {
global.mu.Lock()
defer global.mu.Unlock()
return global.lastChange
}
func GetReady() bool {
global.mu.Lock()
defer global.mu.Unlock()
return global.ready
}
func GetReadySource() string {
global.mu.Lock()
defer global.mu.Unlock()
return global.readySource
}
func GetReadyError() string {
global.mu.Lock()
defer global.mu.Unlock()
return global.readyError
}
func GetLastReadyAt() time.Time {
global.mu.Lock()
defer global.mu.Unlock()
return global.lastReadyAt
}
func GetLastCrash() *CrashInfo {
global.mu.Lock()
defer global.mu.Unlock()
if global.lastCrashInfo == nil {
return nil
}
copyInfo := *global.lastCrashInfo
if global.lastCrashInfo.LogTail != nil {
copyInfo.LogTail = append([]string(nil), global.lastCrashInfo.LogTail...)
}
return &copyInfo
}
func IsIntentionalStop() bool {
global.mu.Lock()
defer global.mu.Unlock()
return global.intentionalStop
}
/* --------------------------------------------------------------------------
STATE SETTERS — unified with logging
----------------------------------------------------------------------------*/
func SetState(s AgentState) {
global.mu.Lock()
defer global.mu.Unlock()
if global.state != s {
stateLogf("%s -> %s", global.state, s)
global.state = s
global.lastChange = time.Now()
}
}
func SetInstallStep(step string) {
global.mu.Lock()
defer global.mu.Unlock()
global.installStep = step
}
func SetError(err error) {
global.mu.Lock()
defer global.mu.Unlock()
global.lastError = err
}
func MarkIntentionalStop() {
global.mu.Lock()
defer global.mu.Unlock()
global.intentionalStop = true
}
func ClearIntentionalStop() {
global.mu.Lock()
defer global.mu.Unlock()
global.intentionalStop = false
}
func SetLastCrash(info *CrashInfo) {
global.mu.Lock()
defer global.mu.Unlock()
if info == nil {
global.lastCrashInfo = nil
return
}
copyInfo := *info
if info.LogTail != nil {
copyInfo.LogTail = append([]string(nil), info.LogTail...)
}
global.lastCrashInfo = &copyInfo
}
func RecordCrash(err error) {
global.mu.Lock()
defer global.mu.Unlock()
stateLogf("crash recorded err=%v", err)
global.state = StateCrashed
global.lastError = err
global.crashCount++
global.lastCrash = time.Now()
global.intentionalStop = false
global.ready = false
global.readySource = ""
global.readyError = fmt.Sprintf("%v", err)
global.lastReadyAt = time.Time{}
}
func SetReadyState(ready bool, source, errText string) {
global.mu.Lock()
defer global.mu.Unlock()
global.ready = ready
global.readySource = source
global.readyError = errText
if ready {
global.lastReadyAt = time.Now()
} else {
global.lastReadyAt = time.Time{}
}
}
/* --------------------------------------------------------------------------
CONFIG SAVE / LOAD
----------------------------------------------------------------------------*/
const configPath = "/opt/zlh-agent/config/payload.json"
func SaveConfig(cfg *Config) error {
if err := os.MkdirAll("/opt/zlh-agent/config", 0o755); err != nil {
return err
}
b, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(configPath, b, 0o644)
}
func LoadConfig() (*Config, error) {
b, err := os.ReadFile(configPath)
if err != nil {
return nil, err
}
var cfg Config
if err := json.Unmarshal(b, &cfg); err != nil {
return nil, err
}
if cfg.ContainerType == "" {
vmidStr := strconv.Itoa(cfg.VMID)
if len(vmidStr) > 0 {
switch vmidStr[0] {
case '6':
cfg.ContainerType = "dev"
case '5':
cfg.ContainerType = "game"
}
}
if cfg.ContainerType == "" && cfg.Runtime != "" {
cfg.ContainerType = "dev"
}
if cfg.ContainerType == "" {
cfg.ContainerType = "game"
}
}
return &cfg, nil
}