package main import ( "encoding/json" "fmt" "net/http" "os" "os/signal" "strconv" "sync" "syscall" "time" ) type CommandPayload struct { Command string `json:"command"` } type InstanceStatusResponse struct { Name string `json:"name"` Version string `json:"version"` Port int `json:"port"` Status string `json:"status"` } type DaemonServer struct { cfg *AppConfig configPath string procManager *ProcessManager } func StartDaemon(cfg *AppConfig, configPath string) error { ds := &DaemonServer{ cfg: cfg, configPath: configPath, procManager: NewProcessManager(), } mux := http.NewServeMux() mux.HandleFunc("/instances/create", ds.handleCreate) mux.HandleFunc("/instances/start", ds.handleStart) mux.HandleFunc("/instances/stop", ds.handleStop) mux.HandleFunc("/instances/command", ds.handleCommand) mux.HandleFunc("/instances/list", ds.handleList) mux.HandleFunc("/instances/logs", ds.handleGetLogs) corsWrappedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusOK) return } mux.ServeHTTP(w, r) }) server := &http.Server{ Addr: cfg.Daemon.ListenAddress, Handler: corsWrappedHandler, } sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) go func() { fmt.Printf("Engine daemon actively listening on http://%s\n", cfg.Daemon.ListenAddress) if err := server.ListenAndServe(); err != http.ErrServerClosed { fmt.Printf("Daemon runtime failure: %v\n", err) } }() <-sigChan fmt.Println("\n[Daemon] Shutdown signal caught! Initializing graceful teardown sequence...") _ = server.Close() ds.shutdownAllRunningServers() fmt.Println("[Daemon] All threads gracefully shut down. Exiting supervisor cleanly.") return nil } func (ds *DaemonServer) shutdownAllRunningServers() { ds.procManager.Lock() var activeNames []string for name := range ds.procManager.ActiveInstances { activeNames = append(activeNames, name) } ds.procManager.Unlock() if len(activeNames) == 0 { fmt.Println("[Daemon] No active server instances to tear down.") return } fmt.Printf("[Daemon] Flushing stop instructions to %d running instance(s)...\n", len(activeNames)) var wg sync.WaitGroup for _, name := range activeNames { wg.Add(1) go func(instanceName string) { defer wg.Done() fmt.Printf("[Daemon] Sending graceful /stop to instance '%s'...\n", instanceName) err := ds.procManager.SendCommand(instanceName, "/stop") if err != nil { fmt.Printf("[Daemon Error] Could not send stop to %s: %v\n", instanceName, err) return } ticker := time.NewTicker(250 * time.Millisecond) defer ticker.Stop() timeout := time.After(15 * time.Second) for { select { case <-ticker.C: ds.procManager.RLock() _, running := ds.procManager.ActiveInstances[instanceName] ds.procManager.RUnlock() if !running { fmt.Printf("[Daemon] Instance '%s' has successfully exited.\n", instanceName) return } case <-timeout: fmt.Printf("[Daemon Warning] Instance '%s' timed out while trying to stop safely.\n", instanceName) return } } }(name) } wg.Wait() } func (ds *DaemonServer) handleCreate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } name := r.URL.Query().Get("name") version := r.URL.Query().Get("version") port := r.URL.Query().Get("port") if name == "" || version == "" || port == "" { http.Error(w, "Missing name, version, or port parameters", http.StatusBadRequest) return } converted_port, err := strconv.Atoi(port) if err != nil { http.Error(w, "Could not convert provided port to a valid port number", http.StatusBadRequest) return } if _, exists := ds.cfg.Instances[name]; exists { http.Error(w, fmt.Sprintf("Instance '%s' already exists in configuration", name), http.StatusConflict) return } options := VsServerConfigOptions{ Version: version, ServerName: name, Port: converted_port, MaxClients: 10, } err = DownloadAndExtractServer(version, ds.cfg.Storage.InstallDir) if err != nil { http.Error(w, fmt.Sprintf("Installation failed: %v", err), http.StatusInternalServerError) return } err = CreateNewInstance(name, version, options, ds.cfg) if err != nil { http.Error(w, fmt.Sprintf("Instance provisioning failed: %v", err), http.StatusInternalServerError) return } ds.cfg.Instances[name] = options data, err := json.MarshalIndent(ds.cfg, "", " ") if err != nil { http.Error(w, fmt.Sprintf("Failed processing profile adjustments: %v", err), http.StatusInternalServerError) return } if err := os.WriteFile(ds.configPath, data, 0644); err != nil { http.Error(w, fmt.Sprintf("Failed saving configuration adjustments to disk: %v", err), http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) fmt.Fprintf(w, "Successfully created and stored profile for instance %s", name) } func (ds *DaemonServer) handleStart(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } name := r.URL.Query().Get("name") if name == "" { http.Error(w, "Missing name parameter", http.StatusBadRequest) return } options, exists := ds.cfg.Instances[name] if !exists { http.Error(w, fmt.Sprintf("Instance '%s' does not exist. Run 'create' first", name), http.StatusNotFound) return } instanceConfigPath := ds.configPath err := SyncInstanceConfig(options.Version, instanceConfigPath, options, ds.cfg) if err != nil { http.Error(w, "Failed to sync config: "+err.Error(), http.StatusInternalServerError) return } err = ds.procManager.StartInstance(name, options.Version, options, ds.cfg) if err != nil { http.Error(w, fmt.Sprintf("Process startup failed: %v", err), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "Successfully started instance %s", name) } func (ds *DaemonServer) handleStop(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } name := r.URL.Query().Get("name") if name == "" { http.Error(w, "Missing name parameter", http.StatusBadRequest) return } err := ds.procManager.SendCommand(name, "/stop") if err != nil { http.Error(w, fmt.Sprintf("Failed to dispatch stop command: %v", err), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "Termination signal routed to instance %s", name) } func (ds *DaemonServer) handleCommand(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } name := r.URL.Query().Get("name") if name == "" { http.Error(w, "Missing name parameter", http.StatusBadRequest) return } var payload CommandPayload if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { http.Error(w, "Malformed JSON body", http.StatusBadRequest) return } err := ds.procManager.SendCommand(name, payload.Command) if err != nil { http.Error(w, fmt.Sprintf("Command delivery failed: %v", err), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "Command delivered successfully to %s", name) } func (ds *DaemonServer) handleList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } ds.procManager.RLock() defer ds.procManager.RUnlock() var responseList []InstanceStatusResponse for name, options := range ds.cfg.Instances { status := "STOPPED" if _, running := ds.procManager.ActiveInstances[name]; running { status = "RUNNING" } responseList = append(responseList, InstanceStatusResponse{ Name: name, Version: options.Version, Port: options.Port, Status: status, }) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(responseList) } func (ds *DaemonServer) handleGetLogs(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } name := r.URL.Query().Get("name") if name == "" { http.Error(w, "Missing name parameter", http.StatusBadRequest) return } ds.procManager.RLock() buf, exists := ds.procManager.LogBuffers[name] ds.procManager.RUnlock() if !exists || buf == nil { w.Header().Set("Content-Type", "application/json") w.Write([]byte("[]")) return } logLines := buf.GetSnapshot() w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(logLines); err != nil { http.Error(w, fmt.Sprintf("Failed encoding log matrix: %v", err), http.StatusInternalServerError) } }