panel — open-source game server manager (public release)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
package dispatch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// instanceMeta is the on-disk sidecar written per instance after successful
|
||||
// Create. It lets the agent rehydrate its in-memory records after a restart,
|
||||
// so operator commands (stop, etc.) continue to work against instances that
|
||||
// outlived the agent process.
|
||||
//
|
||||
// Stored at <metaDir>/<instance_id>.json. Mode 0600 because it carries the
|
||||
// RCON password. Deleted on stop.
|
||||
type instanceMeta struct {
|
||||
InstanceID string `json:"instance_id"`
|
||||
ContainerID string `json:"container_id"`
|
||||
ModuleID string `json:"module_id"`
|
||||
// Branch is the normalized Steam branch the install came from (warm-seed
|
||||
// matching key). Empty on sidecars written before this feature.
|
||||
Branch string `json:"branch,omitempty"`
|
||||
DataPath string `json:"data_path"`
|
||||
BrowseableRoot string `json:"browseable_root,omitempty"`
|
||||
RCONAddr string `json:"rcon_addr,omitempty"`
|
||||
RCONPassword string `json:"rcon_password,omitempty"`
|
||||
ConfigValues map[string]string `json:"config_values,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (d *Dispatcher) metaPath(instanceID string) string {
|
||||
return filepath.Join(d.metaDir, instanceID+".json")
|
||||
}
|
||||
|
||||
func (d *Dispatcher) writeMeta(rec *instanceRecord) error {
|
||||
if d.metaDir == "" {
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(d.metaDir, 0o755); err != nil {
|
||||
return fmt.Errorf("mkdir meta dir: %w", err)
|
||||
}
|
||||
m := instanceMeta{
|
||||
InstanceID: rec.InstanceID,
|
||||
ContainerID: rec.ContainerID,
|
||||
ModuleID: rec.ModuleID,
|
||||
Branch: rec.Branch,
|
||||
DataPath: rec.DataPath,
|
||||
BrowseableRoot: rec.BrowseableRoot,
|
||||
RCONAddr: rec.RCONAddr,
|
||||
RCONPassword: rec.RCONPassword,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
data, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal meta: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(d.metaPath(rec.InstanceID), data, 0o600); err != nil {
|
||||
return fmt.Errorf("write meta: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Dispatcher) deleteMeta(instanceID string) {
|
||||
if d.metaDir == "" {
|
||||
return
|
||||
}
|
||||
if err := os.Remove(d.metaPath(instanceID)); err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
d.log.Warn("delete meta failed", "instance_id", instanceID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dispatcher) loadAllMeta() ([]instanceMeta, error) {
|
||||
if d.metaDir == "" {
|
||||
return nil, nil
|
||||
}
|
||||
entries, err := os.ReadDir(d.metaDir)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var out []instanceMeta
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := e.Name()
|
||||
if filepath.Ext(name) != ".json" {
|
||||
continue
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(d.metaDir, name))
|
||||
if err != nil {
|
||||
d.log.Warn("read meta failed", "file", name, "err", err)
|
||||
continue
|
||||
}
|
||||
var m instanceMeta
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
d.log.Warn("bad meta file", "file", name, "err", err)
|
||||
continue
|
||||
}
|
||||
if m.InstanceID == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in New Issue
Block a user