33d89c66c0
- 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
165 lines
4.1 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|
|
}
|