327 lines
7.1 KiB
Go
Executable File
327 lines
7.1 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"`
|
|
Classification string `json:"classification"`
|
|
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 ResetCrashCount() {
|
|
global.mu.Lock()
|
|
defer global.mu.Unlock()
|
|
global.crashCount = 0
|
|
}
|
|
|
|
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 ©Info
|
|
}
|
|
|
|
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 = ©Info
|
|
}
|
|
|
|
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
|
|
}
|