From ba5dd4f1ca70d5f7838a92d972fc42670d2a36a4 Mon Sep 17 00:00:00 2001 From: chris bell Date: Fri, 5 Jun 2026 19:39:32 -0500 Subject: [PATCH] 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") }