392 lines
10 KiB
Go
392 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"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 {
|
|
sync.RWMutex
|
|
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) reloadConfig() error {
|
|
cfg, err := LoadOrCreateConfig(ds.configPath)
|
|
if err != nil {
|
|
fmt.Printf("Could not reload configuration file: %v\n", err)
|
|
return err
|
|
}
|
|
|
|
ds.Lock()
|
|
defer ds.Unlock()
|
|
ds.cfg = cfg
|
|
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 ds.reloadConfig() != nil {
|
|
http.Error(w, "Could not reload config", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
ds.Lock()
|
|
defer ds.Unlock()
|
|
|
|
if _, exists := ds.cfg.Instances[name]; exists {
|
|
http.Error(w, fmt.Sprintf("Instance '%s' already exists in configuration", name), http.StatusConflict)
|
|
return
|
|
}
|
|
|
|
for name, options := range ds.cfg.Instances {
|
|
if options.Port == converted_port {
|
|
http.Error(w, fmt.Sprintf("Port already occupied by instance '%s'", 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
|
|
}
|
|
|
|
if ds.reloadConfig() != nil {
|
|
http.Error(w, "Could not reload config", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
ds.RLock()
|
|
defer ds.RUnlock()
|
|
|
|
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 := filepath.Join(ds.cfg.Storage.InstancesDir, name, "serverconfig.json")
|
|
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
|
|
}
|
|
|
|
if ds.reloadConfig() != nil {
|
|
http.Error(w, "Could not reload config", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
ds.RLock()
|
|
defer ds.RUnlock()
|
|
|
|
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)
|
|
}
|
|
}
|