Renaming to vssm and adding a readme
This commit is contained in:
49
README.md
Normal file
49
README.md
Normal 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
|
||||||
@@ -25,7 +25,7 @@ type AppConfig struct {
|
|||||||
|
|
||||||
func DefaultConfig() *AppConfig {
|
func DefaultConfig() *AppConfig {
|
||||||
home, _ := os.UserHomeDir()
|
home, _ := os.UserHomeDir()
|
||||||
basePath := filepath.Join(home, ".local", "share", "vs-manager")
|
basePath := filepath.Join(home, ".local", "share", "vssm")
|
||||||
|
|
||||||
cfg := &AppConfig{}
|
cfg := &AppConfig{}
|
||||||
cfg.Storage.InstallDir = filepath.Join(basePath, "installs")
|
cfg.Storage.InstallDir = filepath.Join(basePath, "installs")
|
||||||
|
|||||||
26
daemon.go
26
daemon.go
@@ -41,9 +41,22 @@ func StartDaemon(cfg *AppConfig) error {
|
|||||||
mux.HandleFunc("/instances/command", ds.handleCommand)
|
mux.HandleFunc("/instances/command", ds.handleCommand)
|
||||||
mux.HandleFunc("/instances/list", ds.handleList)
|
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{
|
server := &http.Server{
|
||||||
Addr: cfg.Daemon.ListenAddress,
|
Addr: cfg.Daemon.ListenAddress,
|
||||||
Handler: mux,
|
Handler: corsWrappedHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
@@ -58,8 +71,10 @@ func StartDaemon(cfg *AppConfig) error {
|
|||||||
|
|
||||||
<-sigChan
|
<-sigChan
|
||||||
fmt.Println("\n[Daemon] Shutdown signal caught! Initializing graceful teardown sequence...")
|
fmt.Println("\n[Daemon] Shutdown signal caught! Initializing graceful teardown sequence...")
|
||||||
|
|
||||||
_ = server.Close()
|
_ = server.Close()
|
||||||
ds.shutdownAllRunningServers()
|
ds.shutdownAllRunningServers()
|
||||||
|
|
||||||
fmt.Println("[Daemon] All threads gracefully shut down. Exiting supervisor cleanly.")
|
fmt.Println("[Daemon] All threads gracefully shut down. Exiting supervisor cleanly.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -185,7 +200,14 @@ func (ds *DaemonServer) handleStart(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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 {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Process startup failed: %v", err), http.StatusInternalServerError)
|
http.Error(w, fmt.Sprintf("Process startup failed: %v", err), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -21,38 +21,39 @@ type VsServerConfigOptions struct {
|
|||||||
PreApprovedRole string `json:"PreApprovedRole"`
|
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")
|
templatePath := filepath.Join(cfg.Storage.ConfigTemplatesDir, templateVersion, "serverconfig.json")
|
||||||
|
|
||||||
if _, err := os.Stat(instanceConfigPath); err == nil {
|
if _, err := os.Stat(instanceConfigPath); os.IsNotExist(err) {
|
||||||
return nil
|
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 {
|
source, err := os.Open(templatePath)
|
||||||
return fmt.Errorf("failed creating instance directory tree: %w", err)
|
if err != nil {
|
||||||
}
|
return fmt.Errorf("failed opening baseline template file: %w", err)
|
||||||
|
}
|
||||||
|
defer source.Close()
|
||||||
|
|
||||||
source, err := os.Open(templatePath)
|
destination, err := os.Create(instanceConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed opening baseline template file: %w", err)
|
return fmt.Errorf("failed creating target instance configuration: %w", err)
|
||||||
}
|
}
|
||||||
defer source.Close()
|
|
||||||
|
|
||||||
destination, err := os.Create(instanceConfigPath)
|
_, err = io.Copy(destination, source)
|
||||||
if err != nil {
|
destination.Close()
|
||||||
return fmt.Errorf("failed creating target instance configuration: %w", err)
|
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)
|
data, err := os.ReadFile(instanceConfigPath)
|
||||||
if err != nil {
|
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{}
|
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)
|
return fmt.Errorf("failed parsing configuration JSON payload: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
rawConfig["ServerName"] = config.ServerName
|
rawConfig["ServerName"] = options.ServerName
|
||||||
rawConfig["Port"] = config.Port
|
rawConfig["Port"] = options.Port
|
||||||
rawConfig["MaxClients"] = config.MaxClients
|
rawConfig["MaxClients"] = options.MaxClients
|
||||||
|
|
||||||
if config.Password != "" {
|
if options.Password != "" {
|
||||||
rawConfig["Password"] = config.Password
|
rawConfig["Password"] = options.Password
|
||||||
} else {
|
} else {
|
||||||
rawConfig["Password"] = nil
|
rawConfig["Password"] = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
instanceDir := filepath.Dir(instanceConfigPath)
|
instanceDir := filepath.Dir(instanceConfigPath)
|
||||||
|
|
||||||
if worldConfig, ok := rawConfig["WorldConfig"].(map[string]interface{}); ok {
|
if worldConfig, ok := rawConfig["WorldConfig"].(map[string]interface{}); ok {
|
||||||
worldConfig["SaveFileLocation"] = filepath.Join(instanceDir, "Saves", "default.vcdbs")
|
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)
|
return fmt.Errorf("failed marshaling updated configuration adjustments: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(instanceConfigPath, updatedData, 0644); err != nil {
|
return os.WriteFile(instanceConfigPath, updatedData, 0644)
|
||||||
return fmt.Errorf("failed committing updated configuration to disk: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
14
main.go
14
main.go
@@ -19,7 +19,7 @@ func main() {
|
|||||||
log.Fatalf("Could not locate user home dir: %v", err)
|
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)
|
cfg, err := LoadOrCreateConfig(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Initialization failed: %v", err)
|
log.Fatalf("Initialization failed: %v", err)
|
||||||
@@ -139,11 +139,11 @@ func fetchAndPrintStatus(cfg *AppConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printUsage() {
|
func printUsage() {
|
||||||
fmt.Println("Vintage Story Server Manager")
|
fmt.Println("VintageStory Server Manager")
|
||||||
fmt.Println("\nUsage:")
|
fmt.Println("\nUsage:")
|
||||||
fmt.Println(" go run . daemon Starts the background process supervisor")
|
fmt.Println(" vssm daemon Starts the background process supervisor")
|
||||||
fmt.Println(" go run . create <name> <version> Provisions baseline configuration and stores instance profile")
|
fmt.Println(" vssm 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(" vssm start <name> Launches an existing server instance using stored profile")
|
||||||
fmt.Println(" go run . stop <name> Gracefully shuts down a server instance")
|
fmt.Println(" vssm 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 cmd <name> \"<command>\" Dispatches a terminal console command down the pipe")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user