From 70483037c90710e382a143a3ef0f8c6e536cbe08 Mon Sep 17 00:00:00 2001 From: chris bell Date: Fri, 5 Jun 2026 17:12:24 -0500 Subject: [PATCH] Made the service into a daemon that you can route IPC commands to --- config.go | 9 ++- daemon.go | 178 +++++++++++++++++++++++++++++++++++++++++++++ instance_config.go | 24 +----- main.go | 129 +++++++++++++++++++++----------- process.go | 5 ++ 5 files changed, 280 insertions(+), 65 deletions(-) create mode 100644 daemon.go diff --git a/config.go b/config.go index 9a927b6..9b54e6d 100644 --- a/config.go +++ b/config.go @@ -16,8 +16,11 @@ type AppConfig struct { } `json:"storage"` Daemon struct { - UseNixOs bool `json:"use_nixos"` + // UseNixOs bool `json:"use_nixos"` + ListenAddress string `json:"listen_address"` } + + Instances map[string]VsServerConfigOptions `json:"instances"` } func DefaultConfig() *AppConfig { @@ -30,6 +33,10 @@ func DefaultConfig() *AppConfig { cfg.Storage.BackupDir = filepath.Join(basePath, "backups") cfg.Storage.ConfigTemplatesDir = filepath.Join(basePath, "config_templates") + cfg.Daemon.ListenAddress = "127.0.0.1:12345" + + cfg.Instances = make(map[string]VsServerConfigOptions) + return cfg } diff --git a/daemon.go b/daemon.go new file mode 100644 index 0000000..421191f --- /dev/null +++ b/daemon.go @@ -0,0 +1,178 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "os/signal" + "path/filepath" + "syscall" +) + +type CommandPayload struct { + Command string `json:"command"` +} + +type DaemonServer struct { + cfg *AppConfig + procManager *ProcessManager +} + +func StartDaemon(cfg *AppConfig) error { + ds := &DaemonServer{ + cfg: cfg, + 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) + + server := &http.Server{ + Addr: cfg.Daemon.ListenAddress, + Handler: mux, + } + + 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.Errorf("Daemon runtime failure: %v", err) + } + }() + + <-sigChan + fmt.Println("\nShutting down supervisor daemon threads...") + return nil +} + +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") + if name == "" || version == "" { + http.Error(w, "Missing name or version parameters", 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: 42424, // Pull from custom parameters later if desired + 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 + + home, _ := os.UserHomeDir() + configPath := filepath.Join(home, ".config", "vs-manager", "config.json") + data, _ := json.MarshalIndent(ds.cfg, "", " ") + _ = os.WriteFile(configPath, data, 0644) + + 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 + } + + 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) +} diff --git a/instance_config.go b/instance_config.go index faa9b61..fb6dbef 100644 --- a/instance_config.go +++ b/instance_config.go @@ -10,6 +10,7 @@ import ( ) type VsServerConfigOptions struct { + Version string `json:"Version"` ServerName string `json:"ServerName"` Port int `json:"Port"` IpAddress string `json:"IpAddress"` @@ -20,29 +21,6 @@ type VsServerConfigOptions struct { PreApprovedRole string `json:"PreApprovedRole"` } -// func WriteInstanceConfig(instanceDir string, settings VsServerConfig) error { -// configPath := filepath.Join(instanceDir, "serverconfig.json") - -// if settings.IpAddress == "" { -// settings.IpAddress = "0.0.0.0" -// } -// if settings.MaxClients == 0 { -// settings.MaxClients = 16 -// } - -// data, err := json.MarshalIndent(settings, "", " ") -// if err != nil { -// return fmt.Errorf("failed to marshal server configuration: %w", err) -// } - -// err = os.WriteFile(configPath, data, 0644) -// if err != nil { -// return fmt.Errorf("failed writing serverconfig.json to instance: %w", err) -// } - -// return nil -// } - func PrepareInstanceConfig(templateVersion string, instanceConfigPath string, config VsServerConfigOptions, cfg *AppConfig) error { templatePath := filepath.Join(cfg.Storage.ConfigTemplatesDir, templateVersion, "serverconfig.json") diff --git a/main.go b/main.go index 92779a4..68f72b5 100644 --- a/main.go +++ b/main.go @@ -1,65 +1,112 @@ package main import ( + "bytes" + "encoding/json" "fmt" + "io" "log" + "net/http" + "net/url" "os" - "os/signal" "path/filepath" - "syscall" ) func main() { - fmt.Println("Initializing VS server manager...") - home, err := os.UserHomeDir() if err != nil { - log.Fatalf("Could not locate user home dir") + log.Fatalf("Could not locate user home dir: %v", err) } configPath := filepath.Join(home, ".config", "vs-manager", "config.json") - cfg, err := LoadOrCreateConfig(configPath) if err != nil { log.Fatalf("Initialization failed: %v", err) } - instanceName := "sample_server" - targetVersion := "1.22.3" - - options := VsServerConfigOptions{ - ServerName: "Sample Server", - Port: 4000, - MaxClients: 100, - Password: "", + if len(os.Args) < 2 { + printUsage() + return } - err = DownloadAndExtractServer(targetVersion, cfg.Storage.InstallDir) - if err != nil { - log.Fatalf("Downloader module encountered an error: %v", err) + subCommand := os.Args[1] + + switch subCommand { + case "daemon": + fmt.Println("Initializing VS server manager background supervisor...") + if err := StartDaemon(cfg); err != nil { + log.Fatalf("Daemon runtime fatal error: %v", err) + } + + case "create": + if len(os.Args) < 4 { + log.Fatalf("Usage: go run . create ") + } + name := os.Args[2] + version := os.Args[3] + sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/create?name=%s&version=%s", url.QueryEscape(name), url.QueryEscape(version)), nil) + + case "start": + if len(os.Args) < 3 { + log.Fatalf("Usage: go run . start ") + } + name := os.Args[2] + sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/start?name=%s", url.QueryEscape(name)), nil) + + case "stop": + if len(os.Args) < 3 { + log.Fatalf("Usage: go run . stop ") + } + name := os.Args[2] + sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/stop?name=%s", url.QueryEscape(name)), nil) + + case "cmd": + if len(os.Args) < 4 { + log.Fatalf("Usage: go run . cmd \"\"") + } + name := os.Args[2] + serverCmd := os.Args[3] + + payload := CommandPayload{Command: serverCmd} + body, _ := json.Marshal(payload) + sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/command?name=%s", url.QueryEscape(name)), bytes.NewBuffer(body)) + + default: + printUsage() } - - err = CreateNewInstance(instanceName, targetVersion, options, cfg) - if err != nil { - log.Fatalf("Initialization abort: %v", err) - } - - procManager := NewProcessManager() - - fmt.Printf("\n⚡ Launching Instance: %s (v%s)\n", instanceName, targetVersion) - err = procManager.StartInstance(instanceName, targetVersion, options, cfg) - if err != nil { - log.Fatalf("Process startup failed: %v", err) - } - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - - fmt.Println("\nServer running. Press Ctrl+C to cleanly shut down...") - <-sigChan - - fmt.Println("\n\nTriggering graceful server termination sequence...") - _ = procManager.SendCommand(instanceName, "stop") - - fmt.Println("Manager exited successfully.") +} + +func sendIPCRequest(cfg *AppConfig, method string, path string, body io.Reader) { + targetUrl := fmt.Sprintf("http://%s%s", cfg.Daemon.ListenAddress, path) + req, err := http.NewRequest(method, targetUrl, body) + if err != nil { + log.Fatalf("Failed to construct IPC frame: %v", err) + } + + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatalf("IPC connection failed. Is the vs-manager daemon running? Error: %v", err) + } + defer resp.Body.Close() + + respBody, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + log.Fatalf("Error from daemon: %s", string(respBody)) + } + + fmt.Println(string(respBody)) +} + +func printUsage() { + fmt.Println("Vintage Story Server Manager") + fmt.Println("\nUsage:") + fmt.Println(" go run . daemon Starts the background process supervisor") + fmt.Println(" go run . create Provisions baseline configuration and stores instance profile") + fmt.Println(" go run . start Launches an existing server instance using stored profile") + fmt.Println(" go run . stop Gracefully shuts down a server instance") + fmt.Println(" go run . cmd \"\" Dispatches a terminal console command down the pipe") } diff --git a/process.go b/process.go index 20f5f98..d2924ce 100644 --- a/process.go +++ b/process.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "sync" ) @@ -107,6 +108,10 @@ func (pm *ProcessManager) SendCommand(name string, command string) error { return fmt.Errorf("Cannot send command, instance with name '%s' is not running", name) } + if command != "" && !strings.HasPrefix(command, "/") { + command = "/" + command + } + _, err := io.WriteString(pipe, command+"\n") if err != nil { return fmt.Errorf("failed writing to server process stdin conduit: %w", err)