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 /.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 }