Merge dev into master #1

Merged
chrisbell merged 15 commits from dev into master 2026-06-07 00:44:41 -04:00
6 changed files with 112 additions and 45 deletions
Showing only changes of commit ba5dd4f1ca - Show all commits

49
README.md Normal file
View File

@@ -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

View File

@@ -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")

View File

@@ -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

2
go.mod
View File

@@ -1,3 +1,3 @@
module vs-manager
module vssm
go 1.26.3

View File

@@ -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)
}

14
main.go
View File

@@ -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 <name> <version> Provisions baseline configuration and stores instance profile")
fmt.Println(" go run . start <name> Launches an existing server instance using stored profile")
fmt.Println(" go run . stop <name> Gracefully shuts down a server instance")
fmt.Println(" go run . cmd <name> \"<command>\" Dispatches a terminal console command down the pipe")
fmt.Println(" vssm daemon Starts the background process supervisor")
fmt.Println(" vssm create <name> <version> Provisions baseline configuration and stores instance profile")
fmt.Println(" vssm start <name> Launches an existing server instance using stored profile")
fmt.Println(" vssm stop <name> Gracefully shuts down a server instance")
fmt.Println(" vssm cmd <name> \"<command>\" Dispatches a terminal console command down the pipe")
}