Files
edubox/agent/instance.go
T
EduBox Dev 33d89c66c0 fix(agent): v0.3.10 cleanup orphan instance dirs on startup
- Add cleanupOrphanInstanceDirs() to remove leftover instance directories
  after failed deletes (common on Windows when compose.log is locked)
- Log RemoveAll errors in dockerComposeRm for better visibility
- Bump version to 0.3.10 and rebuild binaries
2026-06-27 21:36:02 +00:00

165 lines
4.1 KiB
Go

package main
import (
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
)
type InstanceInfo struct {
ID string `json:"id"`
TemplateName string `json:"templateName,omitempty"`
Port int `json:"port"`
Status string `json:"status"`
}
func instancesFile(dataDir string) string {
return filepath.Join(dataDir, "instances.json")
}
func loadInstances(dataDir string) (map[string]*InstanceInfo, error) {
f := instancesFile(dataDir)
data, err := os.ReadFile(f)
if err != nil {
if os.IsNotExist(err) {
return make(map[string]*InstanceInfo), nil
}
return nil, err
}
var list []*InstanceInfo
if err := json.Unmarshal(data, &list); err != nil {
return nil, err
}
m := make(map[string]*InstanceInfo)
for _, inst := range list {
m[inst.ID] = inst
}
return m, nil
}
func saveInstances(dataDir string, instances map[string]*InstanceInfo) error {
if err := os.MkdirAll(dataDir, 0755); err != nil {
return err
}
list := make([]*InstanceInfo, 0, len(instances))
for _, inst := range instances {
list = append(list, inst)
}
data, err := json.MarshalIndent(list, "", " ")
if err != nil {
return err
}
return os.WriteFile(instancesFile(dataDir), data, 0644)
}
func upsertInstance(dataDir string, inst *InstanceInfo) error {
instances, err := loadInstances(dataDir)
if err != nil {
return err
}
instances[inst.ID] = inst
return saveInstances(dataDir, instances)
}
func removeInstance(dataDir, instanceID string) error {
instances, err := loadInstances(dataDir)
if err != nil {
return err
}
delete(instances, instanceID)
return saveInstances(dataDir, instances)
}
func instanceURL(inst *InstanceInfo) string {
return fmt.Sprintf("http://localhost:%d", inst.Port)
}
func getInstanceStatus(dataDir, instanceID string) string {
dir := instanceDir(dataDir, instanceID)
composeFile := filepath.Join(dir, "docker-compose.yml")
if _, err := os.Stat(composeFile); err != nil {
return "stopped"
}
engine := getContainerEngine()
// Try modern JSON format first
cmd := exec.Command(engine, "compose", "-f", composeFile, "ps", "--format", "json")
hideWindow(cmd)
out, err := cmd.Output()
if err == nil {
outStr := strings.TrimSpace(string(out))
if outStr == "" || outStr == "[]" {
return "stopped"
}
if strings.HasPrefix(outStr, "[") {
var containers []map[string]interface{}
if err := json.Unmarshal(out, &containers); err == nil {
for _, c := range containers {
state, _ := c["State"].(string)
if state == "running" {
return "running"
}
}
return "stopped"
}
} else {
var c map[string]interface{}
if err := json.Unmarshal(out, &c); err == nil {
state, _ := c["State"].(string)
if state == "running" {
return "running"
}
return "stopped"
}
}
}
// Fallback: use "ps -q" which is supported by all docker-compose versions
cmd = exec.Command(engine, "compose", "-f", composeFile, "ps", "-q")
hideWindow(cmd)
out, err = cmd.Output()
if err != nil {
return "error"
}
if strings.TrimSpace(string(out)) != "" {
return "running"
}
return "stopped"
}
// cleanupOrphanInstanceDirs removes instance directories that have no entry in
// instances.json. This typically happens on Windows when a delete operation
// could not fully remove the directory because compose.log was locked.
func cleanupOrphanInstanceDirs(dataDir string) {
instancesDir := filepath.Join(dataDir, "instances")
inst, err := loadInstances(dataDir)
if err != nil {
log.Printf("cleanupOrphanInstanceDirs: loadInstances error: %v", err)
return
}
entries, err := os.ReadDir(instancesDir)
if err != nil {
if !os.IsNotExist(err) {
log.Printf("cleanupOrphanInstanceDirs: ReadDir error: %v", err)
}
return
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
if _, ok := inst[entry.Name()]; !ok {
dir := filepath.Join(instancesDir, entry.Name())
log.Printf("cleanupOrphanInstanceDirs: removing orphan directory %s", dir)
if err := os.RemoveAll(dir); err != nil {
log.Printf("cleanupOrphanInstanceDirs: RemoveAll error for %s: %v", dir, err)
}
}
}
}