From 70483037c90710e382a143a3ef0f8c6e536cbe08 Mon Sep 17 00:00:00 2001 From: chris bell Date: Fri, 5 Jun 2026 17:12:24 -0500 Subject: [PATCH 01/13] 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) -- 2.52.0 From d29d51cede5f7e7e33cb556d23491302614e45da Mon Sep 17 00:00:00 2001 From: chris bell Date: Fri, 5 Jun 2026 17:21:40 -0500 Subject: [PATCH 02/13] Added a status command --- daemon.go | 40 +++++++++++++++++++++++++++++++++++++++- main.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/daemon.go b/daemon.go index 421191f..f1d651b 100644 --- a/daemon.go +++ b/daemon.go @@ -19,6 +19,13 @@ type DaemonServer struct { procManager *ProcessManager } +type InstanceStatusResponse struct { + Name string `json:"name"` + Version string `json:"version"` + Port int `json:"port"` + Status string `json:"status"` +} + func StartDaemon(cfg *AppConfig) error { ds := &DaemonServer{ cfg: cfg, @@ -30,6 +37,7 @@ func StartDaemon(cfg *AppConfig) error { mux.HandleFunc("/instances/start", ds.handleStart) mux.HandleFunc("/instances/stop", ds.handleStop) mux.HandleFunc("/instances/command", ds.handleCommand) + mux.HandleFunc("/instances/list", ds.handleList) server := &http.Server{ Addr: cfg.Daemon.ListenAddress, @@ -72,7 +80,7 @@ func (ds *DaemonServer) handleCreate(w http.ResponseWriter, r *http.Request) { options := VsServerConfigOptions{ Version: version, ServerName: name, - Port: 42424, // Pull from custom parameters later if desired + Port: 42424, MaxClients: 10, } @@ -176,3 +184,33 @@ func (ds *DaemonServer) handleCommand(w http.ResponseWriter, r *http.Request) { 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) +} diff --git a/main.go b/main.go index 68f72b5..715f8df 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "net/url" "os" "path/filepath" + "text/tabwriter" ) func main() { @@ -71,6 +72,9 @@ func main() { body, _ := json.Marshal(payload) sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/command?name=%s", url.QueryEscape(name)), bytes.NewBuffer(body)) + case "list", "status": + fetchAndPrintStatus(cfg) + default: printUsage() } @@ -101,6 +105,39 @@ func sendIPCRequest(cfg *AppConfig, method string, path string, body io.Reader) fmt.Println(string(respBody)) } +func fetchAndPrintStatus(cfg *AppConfig) { + targetUrl := fmt.Sprintf("http://%s/instances/list", cfg.Daemon.ListenAddress) + resp, err := http.Get(targetUrl) + if err != nil { + log.Fatalf("IPC connection failed. Is the vs-manager daemon running? Error: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + respBody, _ := io.ReadAll(resp.Body) + log.Fatalf("Error from daemon: %s", string(respBody)) + } + + var instances []InstanceStatusResponse + if err := json.NewDecoder(resp.Body).Decode(&instances); err != nil { + log.Fatalf("Failed to decode daemon status matrix: %v", err) + } + + if len(instances) == 0 { + fmt.Println("No instances configured yet. Use 'create' to provision one.") + return + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) + fmt.Fprintln(w, "INSTANCE NAME\tVERSION\tPORT\tSTATUS") + fmt.Fprintln(w, "-------------\t-------\t----\t------") + + for _, inst := range instances { + fmt.Fprintf(w, "%s\t%s\t%d\t%s\n", inst.Name, inst.Version, inst.Port, inst.Status) + } + w.Flush() +} + func printUsage() { fmt.Println("Vintage Story Server Manager") fmt.Println("\nUsage:") -- 2.52.0 From 8e575cab6ed3dcc3401c2db1ae8b69217a15b78f Mon Sep 17 00:00:00 2001 From: chris bell Date: Fri, 5 Jun 2026 17:27:05 -0500 Subject: [PATCH 03/13] Sanity check to make sure servers stop gracefully if daemon closes --- daemon.go | 74 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/daemon.go b/daemon.go index f1d651b..a59fab6 100644 --- a/daemon.go +++ b/daemon.go @@ -7,18 +7,15 @@ import ( "os" "os/signal" "path/filepath" + "sync" "syscall" + "time" ) type CommandPayload struct { Command string `json:"command"` } -type DaemonServer struct { - cfg *AppConfig - procManager *ProcessManager -} - type InstanceStatusResponse struct { Name string `json:"name"` Version string `json:"version"` @@ -26,6 +23,11 @@ type InstanceStatusResponse struct { Status string `json:"status"` } +type DaemonServer struct { + cfg *AppConfig + procManager *ProcessManager +} + func StartDaemon(cfg *AppConfig) error { ds := &DaemonServer{ cfg: cfg, @@ -50,15 +52,73 @@ func StartDaemon(cfg *AppConfig) error { 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) + fmt.Printf("Daemon runtime failure: %v\n", err) } }() <-sigChan - fmt.Println("\nShutting down supervisor daemon threads...") + 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) -- 2.52.0 From ba5dd4f1ca70d5f7838a92d972fc42670d2a36a4 Mon Sep 17 00:00:00 2001 From: chris bell Date: Fri, 5 Jun 2026 19:39:32 -0500 Subject: [PATCH 04/13] Renaming to vssm and adding a readme --- README.md | 49 +++++++++++++++++++++++++++++++++++ config.go | 2 +- daemon.go | 26 +++++++++++++++++-- go.mod | 2 +- instance_config.go | 64 ++++++++++++++++++++++------------------------ main.go | 14 +++++----- 6 files changed, 112 insertions(+), 45 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a050e4 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# VSSM (VintageStory Server Manager) +*A server manager for VintageStory* + +## IMPORTANT INFORMATION +This project is currently in development, and thus everything is subject to change + +## About +VSSM allows you to create and manage multiple VS servers + +## Installation and Usage +There are no binaries just yet, so you have to download the source files. + +Setup is easy from source, all you need is: +- Linux +- Go +- .NET 10 Runtime + +**If you are using NixOS, a flake.nix is provided, just run `nix develop` for a full development enviroment** + +- Once you have the source code downloaded or cloned, you can run `go build` to generate an executable. +- Run `./vssm daemon` to start the daemon - *(For now, theres no background service created by default, but you can run it in the background with `vssm daemon &` and kill the process with `pkill vssm`)* +- Run `./vssm` with no arguments to show usage + +## Configuration +**For now, the configuration file is stored in `~/.config/vssm/config.json`, but this will be configurable later.** + + + +## Roadmap + +- [-] Configuration + - [x] Application data path + - [x] Declarative Servers + +- [-] Server management + - [x] Create servers + - [x] Start/Stop servers + - [x] Multiple servers support + - [x] List servers and their status + - [ ] Delete servers + - [ ] Automated server backups + +- [ ] Binary releases + +- [-] Other + - [-] First class NixOS support + - [x] Patch downloaded server binaries for NixOS + - [ ] Official nix package or flake + - [ ] All configuration options diff --git a/config.go b/config.go index 9b54e6d..5e15c98 100644 --- a/config.go +++ b/config.go @@ -25,7 +25,7 @@ type AppConfig struct { func DefaultConfig() *AppConfig { home, _ := os.UserHomeDir() - basePath := filepath.Join(home, ".local", "share", "vs-manager") + basePath := filepath.Join(home, ".local", "share", "vssm") cfg := &AppConfig{} cfg.Storage.InstallDir = filepath.Join(basePath, "installs") diff --git a/daemon.go b/daemon.go index a59fab6..292cb27 100644 --- a/daemon.go +++ b/daemon.go @@ -41,9 +41,22 @@ func StartDaemon(cfg *AppConfig) error { mux.HandleFunc("/instances/command", ds.handleCommand) mux.HandleFunc("/instances/list", ds.handleList) + 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: mux, + Handler: corsWrappedHandler, } sigChan := make(chan os.Signal, 1) @@ -58,8 +71,10 @@ func StartDaemon(cfg *AppConfig) error { <-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 } @@ -185,7 +200,14 @@ func (ds *DaemonServer) handleStart(w http.ResponseWriter, r *http.Request) { return } - err := ds.procManager.StartInstance(name, options.Version, options, ds.cfg) + 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 diff --git a/go.mod b/go.mod index 047dd0b..3bbbc00 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module vs-manager +module vssm go 1.26.3 diff --git a/instance_config.go b/instance_config.go index fb6dbef..1494ea5 100644 --- a/instance_config.go +++ b/instance_config.go @@ -21,38 +21,39 @@ type VsServerConfigOptions struct { PreApprovedRole string `json:"PreApprovedRole"` } -func PrepareInstanceConfig(templateVersion string, instanceConfigPath string, config VsServerConfigOptions, cfg *AppConfig) error { +func PrepareInstanceConfig(templateVersion string, instanceConfigPath string, options VsServerConfigOptions, cfg *AppConfig) error { + return SyncInstanceConfig(templateVersion, instanceConfigPath, options, cfg) +} + +func SyncInstanceConfig(templateVersion string, instanceConfigPath string, options VsServerConfigOptions, cfg *AppConfig) error { templatePath := filepath.Join(cfg.Storage.ConfigTemplatesDir, templateVersion, "serverconfig.json") - if _, err := os.Stat(instanceConfigPath); err == nil { - return nil - } + if _, err := os.Stat(instanceConfigPath); os.IsNotExist(err) { + if err := os.MkdirAll(filepath.Dir(instanceConfigPath), 0755); err != nil { + return fmt.Errorf("failed creating instance directory tree: %w", err) + } - if err := os.MkdirAll(filepath.Dir(instanceConfigPath), 0755); err != nil { - return fmt.Errorf("failed creating instance directory tree: %w", err) - } + source, err := os.Open(templatePath) + if err != nil { + return fmt.Errorf("failed opening baseline template file: %w", err) + } + defer source.Close() - source, err := os.Open(templatePath) - if err != nil { - return fmt.Errorf("failed opening baseline template file: %w", err) - } - defer source.Close() + destination, err := os.Create(instanceConfigPath) + if err != nil { + return fmt.Errorf("failed creating target instance configuration: %w", err) + } - destination, err := os.Create(instanceConfigPath) - if err != nil { - return fmt.Errorf("failed creating target instance configuration: %w", err) + _, err = io.Copy(destination, source) + destination.Close() + if err != nil { + return fmt.Errorf("failed cloning configuration template payload: %w", err) + } } - defer destination.Close() - - if _, err := io.Copy(destination, source); err != nil { - return fmt.Errorf("failed cloning configuration template payload: %w", err) - } - - destination.Close() data, err := os.ReadFile(instanceConfigPath) if err != nil { - return fmt.Errorf("failed reading cloned configuration data: %w", err) + return fmt.Errorf("failed reading configuration data: %w", err) } var rawConfig map[string]interface{} @@ -60,18 +61,17 @@ func PrepareInstanceConfig(templateVersion string, instanceConfigPath string, co return fmt.Errorf("failed parsing configuration JSON payload: %w", err) } - rawConfig["ServerName"] = config.ServerName - rawConfig["Port"] = config.Port - rawConfig["MaxClients"] = config.MaxClients + rawConfig["ServerName"] = options.ServerName + rawConfig["Port"] = options.Port + rawConfig["MaxClients"] = options.MaxClients - if config.Password != "" { - rawConfig["Password"] = config.Password + if options.Password != "" { + rawConfig["Password"] = options.Password } else { rawConfig["Password"] = nil } instanceDir := filepath.Dir(instanceConfigPath) - if worldConfig, ok := rawConfig["WorldConfig"].(map[string]interface{}); ok { worldConfig["SaveFileLocation"] = filepath.Join(instanceDir, "Saves", "default.vcdbs") } @@ -89,9 +89,5 @@ func PrepareInstanceConfig(templateVersion string, instanceConfigPath string, co return fmt.Errorf("failed marshaling updated configuration adjustments: %w", err) } - if err := os.WriteFile(instanceConfigPath, updatedData, 0644); err != nil { - return fmt.Errorf("failed committing updated configuration to disk: %w", err) - } - - return nil + return os.WriteFile(instanceConfigPath, updatedData, 0644) } diff --git a/main.go b/main.go index 715f8df..26de878 100644 --- a/main.go +++ b/main.go @@ -19,7 +19,7 @@ func main() { log.Fatalf("Could not locate user home dir: %v", err) } - configPath := filepath.Join(home, ".config", "vs-manager", "config.json") + configPath := filepath.Join(home, ".config", "vssm", "config.json") cfg, err := LoadOrCreateConfig(configPath) if err != nil { log.Fatalf("Initialization failed: %v", err) @@ -139,11 +139,11 @@ func fetchAndPrintStatus(cfg *AppConfig) { } func printUsage() { - fmt.Println("Vintage Story Server Manager") + fmt.Println("VintageStory 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") + fmt.Println(" vssm daemon Starts the background process supervisor") + fmt.Println(" vssm create Provisions baseline configuration and stores instance profile") + fmt.Println(" vssm start Launches an existing server instance using stored profile") + fmt.Println(" vssm stop Gracefully shuts down a server instance") + fmt.Println(" vssm cmd \"\" Dispatches a terminal console command down the pipe") } -- 2.52.0 From 82029bf143fcb5b4003363ed0d2d78f181c5a30f Mon Sep 17 00:00:00 2001 From: chris bell Date: Fri, 5 Jun 2026 20:11:06 -0500 Subject: [PATCH 05/13] Update readme --- README.md | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7a050e4..d461fb3 100644 --- a/README.md +++ b/README.md @@ -24,26 +24,38 @@ Setup is easy from source, all you need is: ## Configuration **For now, the configuration file is stored in `~/.config/vssm/config.json`, but this will be configurable later.** - +## Web Interface +Alongside this project, I am developing a seperate web-based dashboard that can be used to manage you servers with a very simple UI. You can get it [here](https://git.bellsworne.tech/chrisbell/vssm_web). ## Roadmap -- [-] Configuration - - [x] Application data path - - [x] Declarative Servers +- [ ] Configuration + - [ ] Custom configuration path (passed to the daemon with a flag `--config-path`) + - [ ] Custom application data path (*for templates, etc, and to be used as a base for the other config directories*) + - [ ] Daemon listen port (kind of implemented, could be cleaner) + - [x] Declarative Server configuration (Create servers in config) -- [-] Server management +- [ ] Server management - [x] Create servers + - [x] Automatic server binary downloading by version (*Caveat: This works BUT the configuration template downloader needs to be figured out, otherwise you have to manually add a template for each version before it can actually be ran*) + - [ ] Delete servers - [x] Start/Stop servers + - [ ] Server configuration + - [ ] Download correct configuration template for version + - [x] Version + - [x] Server Name + - [x] Port - [x] Multiple servers support - [x] List servers and their status - - [ ] Delete servers + - [x] Send commands to servers - [ ] Automated server backups - [ ] Binary releases -- [-] Other - - [-] First class NixOS support +- [ ] Automatically set up background service + +- [ ] Other + - [ ] First class NixOS support - [x] Patch downloaded server binaries for NixOS - [ ] Official nix package or flake - [ ] All configuration options -- 2.52.0 From 17117e2376f5a5f4a0167dfdb3b1ac8ce0ac843a Mon Sep 17 00:00:00 2001 From: chris bell Date: Fri, 5 Jun 2026 20:58:50 -0500 Subject: [PATCH 06/13] Can now set custom config path flag when starting daemon --- .gitignore | 2 ++ config.go | 8 +++++--- instance_config.go | 45 ++++++++++++++++++++++++++++++++++++++++ main.go | 51 +++++++++++++++++++++++++++------------------- 4 files changed, 82 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index c9d31a5..b7893aa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .direnv .cache .vscode +vssm +test/ \ No newline at end of file diff --git a/config.go b/config.go index 5e15c98..9b04b4d 100644 --- a/config.go +++ b/config.go @@ -9,6 +9,7 @@ import ( type AppConfig struct { Storage struct { + AppDataDir string `json:"app_data_dir"` InstallDir string `json:"install_dir"` InstancesDir string `json:"instances_dir"` BackupDir string `json:"backup_dir"` @@ -16,18 +17,18 @@ type AppConfig struct { } `json:"storage"` Daemon struct { - // UseNixOs bool `json:"use_nixos"` ListenAddress string `json:"listen_address"` } Instances map[string]VsServerConfigOptions `json:"instances"` } -func DefaultConfig() *AppConfig { +func CreateConfigWithDefaults(configPath string) *AppConfig { home, _ := os.UserHomeDir() basePath := filepath.Join(home, ".local", "share", "vssm") cfg := &AppConfig{} + cfg.Storage.AppDataDir = basePath cfg.Storage.InstallDir = filepath.Join(basePath, "installs") cfg.Storage.InstancesDir = filepath.Join(basePath, "instances") cfg.Storage.BackupDir = filepath.Join(basePath, "backups") @@ -42,7 +43,7 @@ func DefaultConfig() *AppConfig { func LoadOrCreateConfig(configPath string) (*AppConfig, error) { if _, err := os.Stat(configPath); os.IsNotExist(err) { - cfg := DefaultConfig() + cfg := CreateConfigWithDefaults(configPath) if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil { return nil, fmt.Errorf("Failed to create config directory: %w", err) @@ -69,6 +70,7 @@ func LoadOrCreateConfig(configPath string) (*AppConfig, error) { } dirs := []string{ + cfg.Storage.AppDataDir, cfg.Storage.InstallDir, cfg.Storage.InstancesDir, cfg.Storage.BackupDir, diff --git a/instance_config.go b/instance_config.go index 1494ea5..a4d368d 100644 --- a/instance_config.go +++ b/instance_config.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "net/http" "os" "path/filepath" "strings" @@ -28,6 +29,10 @@ func PrepareInstanceConfig(templateVersion string, instanceConfigPath string, op func SyncInstanceConfig(templateVersion string, instanceConfigPath string, options VsServerConfigOptions, cfg *AppConfig) error { templatePath := filepath.Join(cfg.Storage.ConfigTemplatesDir, templateVersion, "serverconfig.json") + if err := ensureTemplateExists(templateVersion, templatePath, cfg); err != nil { + return fmt.Errorf("failed ensuring configuration template availability: %w", err) + } + if _, err := os.Stat(instanceConfigPath); os.IsNotExist(err) { if err := os.MkdirAll(filepath.Dir(instanceConfigPath), 0755); err != nil { return fmt.Errorf("failed creating instance directory tree: %w", err) @@ -91,3 +96,43 @@ func SyncInstanceConfig(templateVersion string, instanceConfigPath string, optio return os.WriteFile(instanceConfigPath, updatedData, 0644) } + +func ensureTemplateExists(version string, targetPath string, cfg *AppConfig) error { + if _, err := os.Stat(targetPath); err == nil { + return nil + } + + fmt.Printf("Template for version %s not found locally. Fetching remote layout from git server...\n", version) + + if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { + return fmt.Errorf("failed to create local template cache directory: %w", err) + } + + remoteURL := fmt.Sprintf("https://git.bellsworne.tech/chrisbell/vssm_config_templates/raw/branch/main/%s/serverconfig.json", version) + + resp, err := http.Get(remoteURL) + if err != nil { + return fmt.Errorf("network error attempting to pull config template: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("version template '%s' does not exist in the remote git repository asset tree", version) + } else if resp.StatusCode != http.StatusOK { + return fmt.Errorf("remote git mirror returned unexpected HTTP status: %s", resp.Status) + } + + out, err := os.Create(targetPath) + if err != nil { + return fmt.Errorf("failed creating cache file hook on system storage: %w", err) + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + if err != nil { + return fmt.Errorf("failed stream-writing network payload cache frame to disk: %w", err) + } + + fmt.Printf("Successfully cached configuration layout template for version %s\n", version) + return nil +} diff --git a/main.go b/main.go index 26de878..70cd0e1 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "bytes" "encoding/json" + "flag" "fmt" "io" "log" @@ -19,20 +20,28 @@ func main() { log.Fatalf("Could not locate user home dir: %v", err) } - configPath := filepath.Join(home, ".config", "vssm", "config.json") - cfg, err := LoadOrCreateConfig(configPath) - if err != nil { - log.Fatalf("Initialization failed: %v", err) - } + defaultConfigPath := filepath.Join(home, ".config", "vssm", "config.json") - if len(os.Args) < 2 { + configFlag := flag.String("config", defaultConfigPath, "Explicit path targeting a custom vssm config.json profile") + + flag.Usage = printUsage + flag.Parse() + + args := flag.Args() + if len(args) < 1 { printUsage() return } - subCommand := os.Args[1] + cfg, err := LoadOrCreateConfig(*configFlag) + if err != nil { + log.Fatalf("Initialization failed: %v", err) + } + + subCommand := args[0] switch subCommand { + case "daemon": fmt.Println("Initializing VS server manager background supervisor...") if err := StartDaemon(cfg); err != nil { @@ -40,33 +49,33 @@ func main() { } case "create": - if len(os.Args) < 4 { - log.Fatalf("Usage: go run . create ") + if len(args) < 3 { + log.Fatalf("Usage: vssm create ") } - name := os.Args[2] - version := os.Args[3] + name := args[1] + version := args[2] 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 ") + if len(args) < 2 { + log.Fatalf("Usage: vssm start ") } - name := os.Args[2] + name := args[1] 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 ") + if len(args) < 2 { + log.Fatalf("Usage: vssm stop ") } - name := os.Args[2] + name := args[1] 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 \"\"") + if len(args) < 4 { + log.Fatalf("Usage: vssm cmd \"\"") } - name := os.Args[2] - serverCmd := os.Args[3] + name := args[1] + serverCmd := args[2] payload := CommandPayload{Command: serverCmd} body, _ := json.Marshal(payload) -- 2.52.0 From 53b9f30d50313162ed1d09bfff68d8afde8eec6c Mon Sep 17 00:00:00 2001 From: chris bell Date: Fri, 5 Jun 2026 21:48:25 -0500 Subject: [PATCH 07/13] Fixed bugs with --config flag --- README.md | 8 ++++---- config.go | 2 +- daemon.go | 21 ++++++++++++++------- main.go | 24 ++++++++++++++++++------ 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index d461fb3..82a8dff 100644 --- a/README.md +++ b/README.md @@ -30,18 +30,18 @@ Alongside this project, I am developing a seperate web-based dashboard that can ## Roadmap - [ ] Configuration - - [ ] Custom configuration path (passed to the daemon with a flag `--config-path`) - - [ ] Custom application data path (*for templates, etc, and to be used as a base for the other config directories*) + - [x] Custom configuration path (passed to the daemon with a flag `--config`) + - [ ] Daemon listen port (kind of implemented, could be cleaner) - [x] Declarative Server configuration (Create servers in config) - [ ] Server management - [x] Create servers - - [x] Automatic server binary downloading by version (*Caveat: This works BUT the configuration template downloader needs to be figured out, otherwise you have to manually add a template for each version before it can actually be ran*) + - [x] Automatic server binary downloading by version - [ ] Delete servers - [x] Start/Stop servers - [ ] Server configuration - - [ ] Download correct configuration template for version + - [x] Download correct configuration template for version (see availible templates [here](https://git.bellsworne.tech/chrisbell/vssm_config_templates)) - [x] Version - [x] Server Name - [x] Port diff --git a/config.go b/config.go index 9b04b4d..46af439 100644 --- a/config.go +++ b/config.go @@ -18,7 +18,7 @@ type AppConfig struct { Daemon struct { ListenAddress string `json:"listen_address"` - } + } `json:"daemon"` Instances map[string]VsServerConfigOptions `json:"instances"` } diff --git a/daemon.go b/daemon.go index 292cb27..535a300 100644 --- a/daemon.go +++ b/daemon.go @@ -6,7 +6,6 @@ import ( "net/http" "os" "os/signal" - "path/filepath" "sync" "syscall" "time" @@ -25,12 +24,14 @@ type InstanceStatusResponse struct { type DaemonServer struct { cfg *AppConfig + configPath string procManager *ProcessManager } -func StartDaemon(cfg *AppConfig) error { +func StartDaemon(cfg *AppConfig, configPath string) error { ds := &DaemonServer{ cfg: cfg, + configPath: configPath, procManager: NewProcessManager(), } @@ -173,10 +174,16 @@ func (ds *DaemonServer) handleCreate(w http.ResponseWriter, r *http.Request) { 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) + 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) @@ -200,7 +207,7 @@ func (ds *DaemonServer) handleStart(w http.ResponseWriter, r *http.Request) { return } - instanceConfigPath := filepath.Join(ds.cfg.Storage.InstancesDir, name, "serverconfig.json") + 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) diff --git a/main.go b/main.go index 70cd0e1..2a66c03 100644 --- a/main.go +++ b/main.go @@ -33,7 +33,12 @@ func main() { return } - cfg, err := LoadOrCreateConfig(*configFlag) + absConfigPath, err := filepath.Abs(*configFlag) + if err != nil { + log.Fatalf("Failed to resolve absolute configuration path target: %v", err) + } + + cfg, err := LoadOrCreateConfig(absConfigPath) if err != nil { log.Fatalf("Initialization failed: %v", err) } @@ -43,8 +48,8 @@ func main() { switch subCommand { case "daemon": - fmt.Println("Initializing VS server manager background supervisor...") - if err := StartDaemon(cfg); err != nil { + fmt.Printf("Initializing VS server manager background supervisor [Config: %s]...\n", absConfigPath) + if err := StartDaemon(cfg, absConfigPath); err != nil { log.Fatalf("Daemon runtime fatal error: %v", err) } @@ -71,7 +76,7 @@ func main() { sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/stop?name=%s", url.QueryEscape(name)), nil) case "cmd": - if len(args) < 4 { + if len(args) < 3 { log.Fatalf("Usage: vssm cmd \"\"") } name := args[1] @@ -84,6 +89,9 @@ func main() { case "list", "status": fetchAndPrintStatus(cfg) + case "show-config": + fmt.Printf("%v", cfg.Storage.AppDataDir) + default: printUsage() } @@ -107,7 +115,8 @@ func sendIPCRequest(cfg *AppConfig, method string, path string, body io.Reader) defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) - if resp.StatusCode != http.StatusOK { + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { log.Fatalf("Error from daemon: %s", string(respBody)) } @@ -149,10 +158,13 @@ func fetchAndPrintStatus(cfg *AppConfig) { func printUsage() { fmt.Println("VintageStory Server Manager") - fmt.Println("\nUsage:") + fmt.Println("\nGlobal Options:") + fmt.Println(" --config Explicitly target a non-default config structure file location") + fmt.Println("\nUsage Commands:") fmt.Println(" vssm daemon Starts the background process supervisor") fmt.Println(" vssm create Provisions baseline configuration and stores instance profile") fmt.Println(" vssm start Launches an existing server instance using stored profile") fmt.Println(" vssm stop Gracefully shuts down a server instance") fmt.Println(" vssm cmd \"\" Dispatches a terminal console command down the pipe") + fmt.Println(" vssm list Displays the operational matrix of all instances") } -- 2.52.0 From 9f4e27869b47237b277ab44745970c14c0ee7ff6 Mon Sep 17 00:00:00 2001 From: chris bell Date: Fri, 5 Jun 2026 23:38:42 -0500 Subject: [PATCH 08/13] Made the create command require a port number --- daemon.go | 47 +++++++++++++++++++++++++++++++++++++++++++---- main.go | 7 ++++--- process.go | 51 ++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 95 insertions(+), 10 deletions(-) diff --git a/daemon.go b/daemon.go index 535a300..1d72c87 100644 --- a/daemon.go +++ b/daemon.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "os/signal" + "strconv" "sync" "syscall" "time" @@ -41,6 +42,7 @@ func StartDaemon(cfg *AppConfig, configPath string) error { 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", "*") @@ -143,8 +145,15 @@ func (ds *DaemonServer) handleCreate(w http.ResponseWriter, r *http.Request) { 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) + 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 } @@ -156,11 +165,11 @@ func (ds *DaemonServer) handleCreate(w http.ResponseWriter, r *http.Request) { options := VsServerConfigOptions{ Version: version, ServerName: name, - Port: 42424, + Port: converted_port, MaxClients: 10, } - err := DownloadAndExtractServer(version, ds.cfg.Storage.InstallDir) + err = DownloadAndExtractServer(version, ds.cfg.Storage.InstallDir) if err != nil { http.Error(w, fmt.Sprintf("Installation failed: %v", err), http.StatusInternalServerError) return @@ -303,3 +312,33 @@ func (ds *DaemonServer) handleList(w http.ResponseWriter, r *http.Request) { 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) + } +} diff --git a/main.go b/main.go index 2a66c03..24be728 100644 --- a/main.go +++ b/main.go @@ -54,12 +54,13 @@ func main() { } case "create": - if len(args) < 3 { - log.Fatalf("Usage: vssm create ") + if len(args) < 4 { + log.Fatalf("Usage: vssm create ") } name := args[1] version := args[2] - sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/create?name=%s&version=%s", url.QueryEscape(name), url.QueryEscape(version)), nil) + port := args[3] + sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/create?name=%s&version=%s&port=%s", url.QueryEscape(name), url.QueryEscape(version), url.QueryEscape(port)), nil) case "start": if len(args) < 2 { diff --git a/process.go b/process.go index d2924ce..d455d21 100644 --- a/process.go +++ b/process.go @@ -15,12 +15,20 @@ type ProcessManager struct { sync.RWMutex ActiveInstances map[string]*exec.Cmd StdinPipes map[string]io.WriteCloser + LogBuffers map[string]*InstanceLogBuffer +} + +type InstanceLogBuffer struct { + sync.RWMutex + Lines []string + MaxLines int } func NewProcessManager() *ProcessManager { return &ProcessManager{ ActiveInstances: make(map[string]*exec.Cmd), StdinPipes: make(map[string]io.WriteCloser), + LogBuffers: map[string]*InstanceLogBuffer{}, } } @@ -66,6 +74,7 @@ func (pm *ProcessManager) StartInstance(name string, version string, options VsS pm.ActiveInstances[name] = cmd pm.StdinPipes[name] = stdinPipe + pm.LogBuffers[name] = NewInstanceLogBuffer(200) go pm.streamLogs(name, stdoutPipe) go pm.watchProcessExit(name, cmd) @@ -79,9 +88,17 @@ func (pm *ProcessManager) streamLogs(name string, stdout io.ReadCloser) { scanner := bufio.NewScanner(stdout) for scanner.Scan() { - //stream straight to the manager console terminal, - // but later dispatch payloads to our web UI or a log file - fmt.Printf("[%s]: %s\n", name, scanner.Text()) + line := scanner.Text() + + fmt.Printf("[%s]: %s\n", name, line) + + pm.RLock() + buf, exists := pm.LogBuffers[name] + pm.RUnlock() + + if exists { + buf.Append(line) + } } } @@ -119,3 +136,31 @@ func (pm *ProcessManager) SendCommand(name string, command string) error { return nil } + +func NewInstanceLogBuffer(maxLines int) *InstanceLogBuffer { + return &InstanceLogBuffer{ + Lines: make([]string, 0, maxLines), + MaxLines: maxLines, + } +} + +func (lb *InstanceLogBuffer) Append(line string) { + lb.Lock() + defer lb.Unlock() + + if len(lb.Lines) >= lb.MaxLines { + // Shift array out by dropping index 0 + lb.Lines = lb.Lines[1:] + } + lb.Lines = append(lb.Lines, line) +} + +func (lb *InstanceLogBuffer) GetSnapshot() []string { + lb.RLock() + defer lb.RUnlock() + + // Return a copy so the caller can safely iterate or serialize to JSON without lock conflicts + snapshot := make([]string, len(lb.Lines)) + copy(snapshot, lb.Lines) + return snapshot +} -- 2.52.0 From 3a01e835a19d2c10aafbad83eb5e11a0c2fc4254 Mon Sep 17 00:00:00 2001 From: chris bell Date: Sat, 6 Jun 2026 15:48:43 -0500 Subject: [PATCH 09/13] Cleanup; Adding locks where needed, and checking port availibility --- daemon.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- instance.go | 11 ----------- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/daemon.go b/daemon.go index 1d72c87..5de396b 100644 --- a/daemon.go +++ b/daemon.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "os/signal" + "path/filepath" "strconv" "sync" "syscall" @@ -24,6 +25,7 @@ type InstanceStatusResponse struct { } type DaemonServer struct { + sync.RWMutex cfg *AppConfig configPath string procManager *ProcessManager @@ -82,6 +84,19 @@ func StartDaemon(cfg *AppConfig, configPath string) error { 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() @@ -157,11 +172,26 @@ func (ds *DaemonServer) handleCreate(w http.ResponseWriter, r *http.Request) { 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, @@ -199,6 +229,7 @@ func (ds *DaemonServer) handleCreate(w http.ResponseWriter, r *http.Request) { } func (ds *DaemonServer) handleStart(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return @@ -210,13 +241,21 @@ func (ds *DaemonServer) handleStart(w http.ResponseWriter, r *http.Request) { 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 := ds.configPath + 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) @@ -289,6 +328,14 @@ func (ds *DaemonServer) handleList(w http.ResponseWriter, r *http.Request) { 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() diff --git a/instance.go b/instance.go index 6746000..3d5c13f 100644 --- a/instance.go +++ b/instance.go @@ -3,7 +3,6 @@ package main import ( "fmt" "os" - "os/exec" "path/filepath" ) @@ -14,16 +13,6 @@ const ( StateRunning InstanceState = "RUNNING" ) -type ManagedInstance struct { - Name string `json:"name"` - Version string `json:"version"` - Port int `json:"port"` - Status InstanceState `json:"status"` - - Cmd *exec.Cmd `json:"-"` - Stdin interface{} `json:"-"` -} - func CreateNewInstance(name string, version string, options VsServerConfigOptions, cfg *AppConfig) error { instanceDir := filepath.Join(cfg.Storage.InstancesDir, name) -- 2.52.0 From 0e4d96b1be63c568ec2eb2fcb6f1705b8f84094b Mon Sep 17 00:00:00 2001 From: chris bell Date: Sat, 6 Jun 2026 16:39:48 -0500 Subject: [PATCH 10/13] Added `delete` functionality --- README.md | 5 ++-- daemon.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 14 +++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 82a8dff..841ecc7 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,15 @@ Alongside this project, I am developing a seperate web-based dashboard that can - [ ] Configuration - [x] Custom configuration path (passed to the daemon with a flag `--config`) - + - [ ] Daemon instance remembers config path (*so you don't have to pass `--config` everytime you run an ipc command*) - [ ] Daemon listen port (kind of implemented, could be cleaner) - [x] Declarative Server configuration (Create servers in config) + - [ ] More configuration options - [ ] Server management - [x] Create servers - [x] Automatic server binary downloading by version - - [ ] Delete servers + - [x] Delete servers - [x] Start/Stop servers - [ ] Server configuration - [x] Download correct configuration template for version (see availible templates [here](https://git.bellsworne.tech/chrisbell/vssm_config_templates)) diff --git a/daemon.go b/daemon.go index 5de396b..45c6da9 100644 --- a/daemon.go +++ b/daemon.go @@ -45,6 +45,7 @@ func StartDaemon(cfg *AppConfig, configPath string) error { mux.HandleFunc("/instances/command", ds.handleCommand) mux.HandleFunc("/instances/list", ds.handleList) mux.HandleFunc("/instances/logs", ds.handleGetLogs) + mux.HandleFunc("/instances/delete", ds.handleDelete) corsWrappedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") @@ -389,3 +390,75 @@ func (ds *DaemonServer) handleGetLogs(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprintf("Failed encoding log matrix: %v", err), http.StatusInternalServerError) } } + +func (ds *DaemonServer) handleDelete(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 + } + + purge := false + purgeParam := r.URL.Query().Get("purge") + if purgeParam != "" { + result, err := strconv.ParseBool(purgeParam) + if err != nil { + http.Error(w, "Invalid value for 'purge' parameter", http.StatusBadRequest) + return + } + purge = result + } + + if ds.reloadConfig() != nil { + http.Error(w, "Could not reload config", http.StatusInternalServerError) + return + } + + ds.Lock() + defer ds.Unlock() + + ds.procManager.RLock() + defer ds.procManager.RUnlock() + + _, exists := ds.cfg.Instances[name] + if !exists { + http.Error(w, fmt.Sprintf("Cannot delete instance '%s'; Does not exist.", name), http.StatusBadRequest) + return + } + + if _, running := ds.procManager.ActiveInstances[name]; running { + http.Error(w, fmt.Sprintf("Cannot delete instance '%s'; It is currently running.", name), http.StatusBadRequest) + return + } + + delete(ds.cfg.Instances, name) + + 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 + } + + if purge { + path := filepath.Join(ds.cfg.Storage.InstancesDir, name) + err := os.RemoveAll(path) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to delete instance from disk: %v", err), http.StatusInternalServerError) + return + } + } + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Successfully deleted instance %s", name) + +} diff --git a/main.go b/main.go index 24be728..abc40a6 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "net/url" "os" "path/filepath" + "strconv" "text/tabwriter" ) @@ -93,6 +94,19 @@ func main() { case "show-config": fmt.Printf("%v", cfg.Storage.AppDataDir) + case "delete": + if len(args) < 2 { + log.Fatalf("Usage: vssm delete ") + } + + name := args[1] + + deleteCmd := flag.NewFlagSet("delete", flag.ExitOnError) + purge := deleteCmd.Bool("purge", false, "Delete instance files from disk") + deleteCmd.Parse(args[2:]) + + sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/delete?name=%s&purge=%s", url.QueryEscape(name), url.QueryEscape(strconv.FormatBool(*purge))), nil) + default: printUsage() } -- 2.52.0 From 336973443deb0781062ce1cd52ab71d42d0f0ec0 Mon Sep 17 00:00:00 2001 From: chris bell Date: Sat, 6 Jun 2026 17:03:54 -0500 Subject: [PATCH 11/13] Daemon remembers the config path if started with --config flag --- README.md | 2 +- main.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 841ecc7..f9b2ed8 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Alongside this project, I am developing a seperate web-based dashboard that can - [ ] Configuration - [x] Custom configuration path (passed to the daemon with a flag `--config`) - - [ ] Daemon instance remembers config path (*so you don't have to pass `--config` everytime you run an ipc command*) + - [x] Daemon instance remembers config path (*so you don't have to pass `--config` everytime you run an ipc command*) - [ ] Daemon listen port (kind of implemented, could be cleaner) - [x] Declarative Server configuration (Create servers in config) - [ ] More configuration options diff --git a/main.go b/main.go index abc40a6..8b83490 100644 --- a/main.go +++ b/main.go @@ -34,11 +34,30 @@ func main() { return } + configExplicitlySet := false + flag.Visit(func(f *flag.Flag) { + if f.Name == "config" { + configExplicitlySet = true + } + }) + + if !configExplicitlySet { + if envConfig := os.Getenv("VSSM_CONFIG_PATH"); envConfig != "" { + configFlag = &envConfig + } + } + absConfigPath, err := filepath.Abs(*configFlag) if err != nil { log.Fatalf("Failed to resolve absolute configuration path target: %v", err) } + if configExplicitlySet { + if err := os.Setenv("VSSM_CONFIG_PATH", absConfigPath); err != nil { + log.Fatalf("Warning: could not set VSSM_CONFIG_PATH: %v", err) + } + } + cfg, err := LoadOrCreateConfig(absConfigPath) if err != nil { log.Fatalf("Initialization failed: %v", err) -- 2.52.0 From c3e17f9a80769fc0368fd1c8621606bf5dc47f6e Mon Sep 17 00:00:00 2001 From: chris bell Date: Sat, 6 Jun 2026 18:09:07 -0500 Subject: [PATCH 12/13] Can now change all configuration options per instance --- README.md | 5 +++-- config.go | 4 ++-- daemon.go | 7 +++---- instance.go | 4 ++-- instance_config.go | 34 +++++++++++++--------------------- process.go | 2 +- 6 files changed, 24 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index f9b2ed8..e763193 100644 --- a/README.md +++ b/README.md @@ -34,18 +34,19 @@ Alongside this project, I am developing a seperate web-based dashboard that can - [x] Daemon instance remembers config path (*so you don't have to pass `--config` everytime you run an ipc command*) - [ ] Daemon listen port (kind of implemented, could be cleaner) - [x] Declarative Server configuration (Create servers in config) - - [ ] More configuration options + - [x] All configuration options - [ ] Server management - [x] Create servers - [x] Automatic server binary downloading by version - [x] Delete servers - [x] Start/Stop servers - - [ ] Server configuration + - [x] Server configuration - [x] Download correct configuration template for version (see availible templates [here](https://git.bellsworne.tech/chrisbell/vssm_config_templates)) - [x] Version - [x] Server Name - [x] Port + - [x] Full declarative configuration support in main app config - [x] Multiple servers support - [x] List servers and their status - [x] Send commands to servers diff --git a/config.go b/config.go index 46af439..f1b2350 100644 --- a/config.go +++ b/config.go @@ -20,7 +20,7 @@ type AppConfig struct { ListenAddress string `json:"listen_address"` } `json:"daemon"` - Instances map[string]VsServerConfigOptions `json:"instances"` + Instances map[string]InstanceMetadata `json:"instances"` } func CreateConfigWithDefaults(configPath string) *AppConfig { @@ -36,7 +36,7 @@ func CreateConfigWithDefaults(configPath string) *AppConfig { cfg.Daemon.ListenAddress = "127.0.0.1:12345" - cfg.Instances = make(map[string]VsServerConfigOptions) + cfg.Instances = make(map[string]InstanceMetadata) return cfg } diff --git a/daemon.go b/daemon.go index 45c6da9..4e4f789 100644 --- a/daemon.go +++ b/daemon.go @@ -193,11 +193,10 @@ func (ds *DaemonServer) handleCreate(w http.ResponseWriter, r *http.Request) { } } - options := VsServerConfigOptions{ + metadata := InstanceMetadata{ Version: version, ServerName: name, Port: converted_port, - MaxClients: 10, } err = DownloadAndExtractServer(version, ds.cfg.Storage.InstallDir) @@ -206,13 +205,13 @@ func (ds *DaemonServer) handleCreate(w http.ResponseWriter, r *http.Request) { return } - err = CreateNewInstance(name, version, options, ds.cfg) + err = CreateNewInstance(name, version, metadata, ds.cfg) if err != nil { http.Error(w, fmt.Sprintf("Instance provisioning failed: %v", err), http.StatusInternalServerError) return } - ds.cfg.Instances[name] = options + ds.cfg.Instances[name] = metadata data, err := json.MarshalIndent(ds.cfg, "", " ") if err != nil { diff --git a/instance.go b/instance.go index 3d5c13f..5563e73 100644 --- a/instance.go +++ b/instance.go @@ -13,7 +13,7 @@ const ( StateRunning InstanceState = "RUNNING" ) -func CreateNewInstance(name string, version string, options VsServerConfigOptions, cfg *AppConfig) error { +func CreateNewInstance(name string, version string, meta InstanceMetadata, cfg *AppConfig) error { instanceDir := filepath.Join(cfg.Storage.InstancesDir, name) dirs := []string{ @@ -29,7 +29,7 @@ func CreateNewInstance(name string, version string, options VsServerConfigOption } instanceConfigPath := filepath.Join(instanceDir, "serverconfig.json") - if err := PrepareInstanceConfig(version, instanceConfigPath, options, cfg); err != nil { + if err := PrepareInstanceConfig(version, instanceConfigPath, meta, cfg); err != nil { return fmt.Errorf("Failed provisioning server baseline configuration: %w", err) } diff --git a/instance_config.go b/instance_config.go index a4d368d..126015b 100644 --- a/instance_config.go +++ b/instance_config.go @@ -10,23 +10,18 @@ import ( "strings" ) -type VsServerConfigOptions struct { - Version string `json:"Version"` - ServerName string `json:"ServerName"` - Port int `json:"Port"` - IpAddress string `json:"IpAddress"` - MaxClients int `json:"MaxClients"` - Password string `json:"Password"` - DefaultRole string `json:"DefaultRole"` - GuestRole string `json:"GuestRole"` - PreApprovedRole string `json:"PreApprovedRole"` +type InstanceMetadata struct { + Version string `json:"Version"` + ServerName string `json:"ServerName"` + Port int `json:"Port"` + Config map[string]interface{} `json:"config"` } -func PrepareInstanceConfig(templateVersion string, instanceConfigPath string, options VsServerConfigOptions, cfg *AppConfig) error { - return SyncInstanceConfig(templateVersion, instanceConfigPath, options, cfg) +func PrepareInstanceConfig(templateVersion string, instanceConfigPath string, meta InstanceMetadata, cfg *AppConfig) error { + return SyncInstanceConfig(templateVersion, instanceConfigPath, meta, cfg) } -func SyncInstanceConfig(templateVersion string, instanceConfigPath string, options VsServerConfigOptions, cfg *AppConfig) error { +func SyncInstanceConfig(templateVersion string, instanceConfigPath string, meta InstanceMetadata, cfg *AppConfig) error { templatePath := filepath.Join(cfg.Storage.ConfigTemplatesDir, templateVersion, "serverconfig.json") if err := ensureTemplateExists(templateVersion, templatePath, cfg); err != nil { @@ -66,16 +61,13 @@ func SyncInstanceConfig(templateVersion string, instanceConfigPath string, optio return fmt.Errorf("failed parsing configuration JSON payload: %w", err) } - rawConfig["ServerName"] = options.ServerName - rawConfig["Port"] = options.Port - rawConfig["MaxClients"] = options.MaxClients - - if options.Password != "" { - rawConfig["Password"] = options.Password - } else { - rawConfig["Password"] = nil + for key, value := range meta.Config { + rawConfig[key] = value } + rawConfig["ServerName"] = meta.ServerName + rawConfig["Port"] = meta.Port + instanceDir := filepath.Dir(instanceConfigPath) if worldConfig, ok := rawConfig["WorldConfig"].(map[string]interface{}); ok { worldConfig["SaveFileLocation"] = filepath.Join(instanceDir, "Saves", "default.vcdbs") diff --git a/process.go b/process.go index d455d21..46645aa 100644 --- a/process.go +++ b/process.go @@ -32,7 +32,7 @@ func NewProcessManager() *ProcessManager { } } -func (pm *ProcessManager) StartInstance(name string, version string, options VsServerConfigOptions, config *AppConfig) error { +func (pm *ProcessManager) StartInstance(name string, version string, meta InstanceMetadata, config *AppConfig) error { pm.Lock() defer pm.Unlock() -- 2.52.0 From 988bdfcf6c9228d33ed02efe61f606db4d6513f8 Mon Sep 17 00:00:00 2001 From: chris bell Date: Sat, 6 Jun 2026 23:42:54 -0500 Subject: [PATCH 13/13] Fixed IPC calls in main to use new ip and port seperation in the config --- config.go | 9 ++++----- daemon.go | 6 ++++-- main.go | 10 ++++++++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/config.go b/config.go index f1b2350..6c28f85 100644 --- a/config.go +++ b/config.go @@ -18,23 +18,23 @@ type AppConfig struct { Daemon struct { ListenAddress string `json:"listen_address"` + Port int `json:"port"` } `json:"daemon"` Instances map[string]InstanceMetadata `json:"instances"` } func CreateConfigWithDefaults(configPath string) *AppConfig { - home, _ := os.UserHomeDir() - basePath := filepath.Join(home, ".local", "share", "vssm") + basePath := filepath.Join(filepath.Dir(configPath), "vssm_data") cfg := &AppConfig{} - cfg.Storage.AppDataDir = basePath cfg.Storage.InstallDir = filepath.Join(basePath, "installs") cfg.Storage.InstancesDir = filepath.Join(basePath, "instances") cfg.Storage.BackupDir = filepath.Join(basePath, "backups") cfg.Storage.ConfigTemplatesDir = filepath.Join(basePath, "config_templates") - cfg.Daemon.ListenAddress = "127.0.0.1:12345" + cfg.Daemon.ListenAddress = "127.0.0.1" + cfg.Daemon.Port = 65000 cfg.Instances = make(map[string]InstanceMetadata) @@ -70,7 +70,6 @@ func LoadOrCreateConfig(configPath string) (*AppConfig, error) { } dirs := []string{ - cfg.Storage.AppDataDir, cfg.Storage.InstallDir, cfg.Storage.InstancesDir, cfg.Storage.BackupDir, diff --git a/daemon.go b/daemon.go index 4e4f789..b8059a5 100644 --- a/daemon.go +++ b/daemon.go @@ -60,8 +60,10 @@ func StartDaemon(cfg *AppConfig, configPath string) error { mux.ServeHTTP(w, r) }) + listenAddress := cfg.Daemon.ListenAddress + ":" + strconv.Itoa(cfg.Daemon.Port) + server := &http.Server{ - Addr: cfg.Daemon.ListenAddress, + Addr: listenAddress, Handler: corsWrappedHandler, } @@ -69,7 +71,7 @@ func StartDaemon(cfg *AppConfig, configPath string) error { signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) go func() { - fmt.Printf("Engine daemon actively listening on http://%s\n", cfg.Daemon.ListenAddress) + fmt.Printf("Engine daemon actively listening on http://%s\n", listenAddress) if err := server.ListenAndServe(); err != http.ErrServerClosed { fmt.Printf("Daemon runtime failure: %v\n", err) } diff --git a/main.go b/main.go index 8b83490..a069b8e 100644 --- a/main.go +++ b/main.go @@ -63,6 +63,9 @@ func main() { log.Fatalf("Initialization failed: %v", err) } + listenAddress := cfg.Daemon.ListenAddress + ":" + strconv.Itoa(cfg.Daemon.Port) + fmt.Printf("Got listen address from config: %s\n", listenAddress) + subCommand := args[0] switch subCommand { @@ -132,7 +135,9 @@ func main() { } func sendIPCRequest(cfg *AppConfig, method string, path string, body io.Reader) { - targetUrl := fmt.Sprintf("http://%s%s", cfg.Daemon.ListenAddress, path) + listenAddress := cfg.Daemon.ListenAddress + ":" + strconv.Itoa(cfg.Daemon.Port) + targetUrl := fmt.Sprintf("http://%s%s", listenAddress, path) + fmt.Printf("Listen Addr: %s; Target URL: %s\n", listenAddress, targetUrl) req, err := http.NewRequest(method, targetUrl, body) if err != nil { log.Fatalf("Failed to construct IPC frame: %v", err) @@ -158,7 +163,8 @@ func sendIPCRequest(cfg *AppConfig, method string, path string, body io.Reader) } func fetchAndPrintStatus(cfg *AppConfig) { - targetUrl := fmt.Sprintf("http://%s/instances/list", cfg.Daemon.ListenAddress) + listenAddress := cfg.Daemon.ListenAddress + ":" + strconv.Itoa(cfg.Daemon.Port) + targetUrl := fmt.Sprintf("http://%s/instances/list", listenAddress) resp, err := http.Get(targetUrl) if err != nil { log.Fatalf("IPC connection failed. Is the vs-manager daemon running? Error: %v", err) -- 2.52.0