From 0a5cfe83fc8f558ea644701f3cf87e4dc8708f6b Mon Sep 17 00:00:00 2001 From: chris bell Date: Tue, 9 Jun 2026 18:46:54 -0500 Subject: [PATCH 1/5] Forced 'list' to return instance list sorted alphebetically --- daemon.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/daemon.go b/daemon.go index b8059a5..8542d9b 100644 --- a/daemon.go +++ b/daemon.go @@ -7,6 +7,7 @@ import ( "os" "os/signal" "path/filepath" + "slices" "strconv" "sync" "syscall" @@ -357,6 +358,16 @@ func (ds *DaemonServer) handleList(w http.ResponseWriter, r *http.Request) { }) } + slices.SortFunc(responseList, func(a, b InstanceStatusResponse) int { + if a.Name < b.Name { + return -1 + } + if a.Name > b.Name { + return 1 + } + return 0 + }) + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(responseList) -- 2.52.0 From 318d473fec1401528aaf315d4d342f5120c4ca37 Mon Sep 17 00:00:00 2001 From: chris bell Date: Tue, 9 Jun 2026 23:32:31 -0500 Subject: [PATCH 2/5] Added a modify endpoint to change name and port --- daemon.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 9 +++++++ 2 files changed, 84 insertions(+) diff --git a/daemon.go b/daemon.go index 8542d9b..582bd69 100644 --- a/daemon.go +++ b/daemon.go @@ -47,6 +47,7 @@ func StartDaemon(cfg *AppConfig, configPath string) error { mux.HandleFunc("/instances/list", ds.handleList) mux.HandleFunc("/instances/logs", ds.handleGetLogs) mux.HandleFunc("/instances/delete", ds.handleDelete) + mux.HandleFunc("/instances/modify", ds.handleModify) corsWrappedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") @@ -474,3 +475,77 @@ func (ds *DaemonServer) handleDelete(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Successfully deleted instance %s", name) } + +func (ds *DaemonServer) handleModify(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 + } + + newName := r.URL.Query().Get("newName") + if newName == "" { + http.Error(w, "Missing newName parameter", http.StatusBadRequest) + return + } + + newPort := r.URL.Query().Get("newPort") + if newPort == "" { + http.Error(w, "Missing newPort parameter", http.StatusBadRequest) + return + } + + convertedPort, err := strconv.Atoi(newPort) + if err != nil { + http.Error(w, "Could not convert provided port to a valid port number", http.StatusBadRequest) + return + } + + ds.Lock() + defer ds.Unlock() + + ds.procManager.RLock() + defer ds.procManager.RUnlock() + + instance, exists := ds.cfg.Instances[name] + if !exists { + http.Error(w, fmt.Sprintf("Cannot modify instance '%s'; Does not exist.", name), http.StatusBadRequest) + return + } + + if _, running := ds.procManager.ActiveInstances[name]; running { + http.Error(w, fmt.Sprintf("Cannot modify instance '%s'; It is currently running.", name), http.StatusBadRequest) + return + } + + instance.ServerName = newName + instance.Port = convertedPort + ds.cfg.Instances[name] = instance + + 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 + } + + instanceConfigPath := filepath.Join(ds.cfg.Storage.InstancesDir, name, "serverconfig.json") + err = SyncInstanceConfig(instance.Version, instanceConfigPath, instance, ds.cfg) + if err != nil { + http.Error(w, "Failed to sync config: "+err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Successfully modified instance %s", name) + +} diff --git a/main.go b/main.go index a069b8e..67d9c37 100644 --- a/main.go +++ b/main.go @@ -129,6 +129,15 @@ func main() { sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/delete?name=%s&purge=%s", url.QueryEscape(name), url.QueryEscape(strconv.FormatBool(*purge))), nil) + case "modify": + if len(args) < 4 { + log.Fatalf("Usage: vssm modify ") + } + name := args[1] + newName := args[2] + newPort := args[3] + sendIPCRequest(cfg, "POST", fmt.Sprintf("/instances/modify?name=%s&newName=%s&newPort=%s", url.QueryEscape(name), url.QueryEscape(newName), url.QueryEscape(newPort)), nil) + default: printUsage() } -- 2.52.0 From 2497b37d21db669e85f0054af2d07edebb9589c0 Mon Sep 17 00:00:00 2001 From: chris bell Date: Wed, 10 Jun 2026 00:01:18 -0500 Subject: [PATCH 3/5] Update the README --- README.md | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- main.go | 2 +- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e763193..5b4dffb 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,69 @@ Setup is easy from source, all you need is: - 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.** +The default configuration file is stored in `~/.local/share/vssm/config.json`, however you can pass the `--config ` flag to any command to use a custom configuration. You can also set the `VSSM_CONFIG_PATH` environment variable in lieu of using the config flag for every command. + +The default configuration: +```json +{ + "storage": { + "app_data_dir": "", + "install_dir": "/home/user/.local/share/vssm/vssm_data/installs", + "instances_dir": "/home/user/.local/share/vssm/vssm_data/instances", + "backup_dir": "/home/user/.local/share/vssm/vssm_data/backups", + "config_templates_dir": "/home/user/.local/share/vssm/vssm_data/config_templates" + }, + "daemon": { + "listen_address": "127.0.0.1", + "port": 65000 + }, + "instances": { + "test_server": { + "Version": "1.22.3", + "ServerName": "test_server", + "Port": 12345, + "config": null + } + } +``` + +- Storage + - install_dir - The directory where versioned server binaries are stored + - instances_dir - Directory for instances and their data + - backup_dir - Directory for automatic backups of instances + - config_templates_dir - Directory for versioned server configuration templates + +- Daemon + - port - The port the daemon listens on for http requests + - listen_address - The IP address the daemon listens on + +- Instances + - Version - The game version + - ServerName - The name of the instance + - Port - The port the instance is listening on + - config - This allows you to set ANY configuration item from the config, example below: + +Example instance with extra configuration: +```json +"test_server": { + "Version": "1.22.3", + "ServerName": "test_server", + "Port": 12345, + "config": { + "AllowPvP": true, + "WhiteListMode": 1, + "WelcomeMessage": "Welcome to the server, {0}!" + "WorldConfig": { + "Seed": "superawesomeseed", + "WorldName": "A new world", + "PlayStyle": "surviveandbuild" + } + } +} +``` + +**NOTE:** Most settings that are filepaths should be left alone in the config, and will probably be overwritten anyways (like the ModPaths setting). + ## 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). @@ -32,9 +94,10 @@ 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`) - [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] Daemon listen port - [x] Declarative Server configuration (Create servers in config) - [x] All configuration options + - [ ] Endpoint for modifying instance configuration (for web ui) - [ ] Server management - [x] Create servers diff --git a/main.go b/main.go index 67d9c37..b4a8341 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,7 @@ func main() { log.Fatalf("Could not locate user home dir: %v", err) } - defaultConfigPath := filepath.Join(home, ".config", "vssm", "config.json") + defaultConfigPath := filepath.Join(home, ".local", "share", "vssm", "config.json") configFlag := flag.String("config", defaultConfigPath, "Explicit path targeting a custom vssm config.json profile") -- 2.52.0 From c52e505c18e04e89faed906fd4fc546f131fac05 Mon Sep 17 00:00:00 2001 From: chrisbell Date: Wed, 10 Jun 2026 01:03:21 -0400 Subject: [PATCH 4/5] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5b4dffb..4a8675f 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ The default configuration: - config - This allows you to set ANY configuration item from the config, example below: Example instance with extra configuration: +**NOTE:** Most settings that are filepaths should be left alone in the instance config settings, and will probably be overwritten anyways (like the ModPaths setting). ```json "test_server": { "Version": "1.22.3", @@ -73,7 +74,7 @@ Example instance with extra configuration: "config": { "AllowPvP": true, "WhiteListMode": 1, - "WelcomeMessage": "Welcome to the server, {0}!" + "WelcomeMessage": "Welcome to the server, {0}!", "WorldConfig": { "Seed": "superawesomeseed", "WorldName": "A new world", @@ -83,7 +84,7 @@ Example instance with extra configuration: } ``` -**NOTE:** Most settings that are filepaths should be left alone in the config, and will probably be overwritten anyways (like the ModPaths setting). + ## Web Interface -- 2.52.0 From 61934bb2f49b8a63841b41bf72715ab286525c4c Mon Sep 17 00:00:00 2001 From: chris bell Date: Wed, 10 Jun 2026 10:55:59 -0500 Subject: [PATCH 5/5] Cleanup before merging to main --- downloader.go | 4 +--- instance.go | 2 +- main.go | 21 +++++++++++---------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/downloader.go b/downloader.go index 967e69c..37b36a4 100644 --- a/downloader.go +++ b/downloader.go @@ -98,14 +98,12 @@ func patchBinaryForNixos(binaryPath string) error { return nil } - fmt.Println("[NixOS Detected] Patching server interpreter pathway...") + fmt.Println("[NixOS Detected] Patching server binary...") dotnetRoot := os.Getenv("DOTNET_ROOT") var interpreter string if dotnetRoot != "" { - // Use the glibc version that matches the active .NET runtime exactly - // Finds the underlying ld-linux-x86-64.so.2 link within the .NET store closure out, err := exec.Command("patchelf", "--print-interpreter", filepath.Join(dotnetRoot, "dotnet")).Output() if err == nil && len(out) > 0 { interpreter = strings.TrimSpace(string(out)) diff --git a/instance.go b/instance.go index 5563e73..defb400 100644 --- a/instance.go +++ b/instance.go @@ -30,7 +30,7 @@ func CreateNewInstance(name string, version string, meta InstanceMetadata, cfg * instanceConfigPath := filepath.Join(instanceDir, "serverconfig.json") if err := PrepareInstanceConfig(version, instanceConfigPath, meta, cfg); err != nil { - return fmt.Errorf("Failed provisioning server baseline configuration: %w", err) + return fmt.Errorf("Failed to create baseline configuration: %w", err) } fmt.Printf("Instance '%s' (v%s) successfully initialized\n", name, version) diff --git a/main.go b/main.go index b4a8341..b715cdc 100644 --- a/main.go +++ b/main.go @@ -71,7 +71,7 @@ func main() { switch subCommand { case "daemon": - fmt.Printf("Initializing VS server manager background supervisor [Config: %s]...\n", absConfigPath) + fmt.Printf("Initializing VSSM Daemon [Config: %s]...\n", absConfigPath) if err := StartDaemon(cfg, absConfigPath); err != nil { log.Fatalf("Daemon runtime fatal error: %v", err) } @@ -149,7 +149,7 @@ func sendIPCRequest(cfg *AppConfig, method string, path string, body io.Reader) 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) + log.Fatalf("Failed to construct IPC request: %v", err) } if body != nil { @@ -158,7 +158,7 @@ func sendIPCRequest(cfg *AppConfig, method string, path string, body io.Reader) resp, err := http.DefaultClient.Do(req) if err != nil { - log.Fatalf("IPC connection failed. Is the vs-manager daemon running? Error: %v", err) + log.Fatalf("IPC connection failed. Is the VSSM daemon running? Error: %v", err) } defer resp.Body.Close() @@ -176,7 +176,7 @@ func fetchAndPrintStatus(cfg *AppConfig) { 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) + log.Fatalf("IPC connection failed. Is the VSSM daemon running? Error: %v", err) } defer resp.Body.Close() @@ -191,7 +191,7 @@ func fetchAndPrintStatus(cfg *AppConfig) { } if len(instances) == 0 { - fmt.Println("No instances configured yet. Use 'create' to provision one.") + fmt.Println("No instances configured yet. Use 'create' to create one.") return } @@ -210,10 +210,11 @@ func printUsage() { 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 daemon Starts the daemon") + fmt.Println(" vssm create Creates the instance configuration and profile") + fmt.Println(" vssm delete Deletes an instance profile, optionally deletes all files with the `--purge` flag") + fmt.Println(" vssm start Launches an existing server instance from a 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") + fmt.Println(" vssm cmd \"\" Sends a command to an instance") + fmt.Println(" vssm list Displays a list of all instances and their status") } -- 2.52.0