Adding current project state
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.envrc
|
||||||
|
.direnv
|
||||||
|
.cache
|
||||||
|
.vscode
|
||||||
77
config.go
Normal file
77
config.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppConfig struct {
|
||||||
|
Storage struct {
|
||||||
|
InstallDir string `json:"install_dir"`
|
||||||
|
InstancesDir string `json:"instances_dir"`
|
||||||
|
BackupDir string `json:"backup_dir"`
|
||||||
|
ConfigTemplatesDir string `json:"config_templates_dir"`
|
||||||
|
} `json:"storage"`
|
||||||
|
|
||||||
|
Daemon struct {
|
||||||
|
UseNixOs bool `json:"use_nixos"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultConfig() *AppConfig {
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
basePath := filepath.Join(home, ".local", "share", "vs-manager")
|
||||||
|
|
||||||
|
cfg := &AppConfig{}
|
||||||
|
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")
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadOrCreateConfig(configPath string) (*AppConfig, error) {
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to create config directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(cfg, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to write config file at %s\n", configPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg AppConfig
|
||||||
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("Invalid JSON layout in config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dirs := []string{
|
||||||
|
cfg.Storage.InstallDir,
|
||||||
|
cfg.Storage.InstancesDir,
|
||||||
|
cfg.Storage.BackupDir,
|
||||||
|
cfg.Storage.ConfigTemplatesDir,
|
||||||
|
}
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed creating workspace directory %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
125
downloader.go
Normal file
125
downloader.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DownloadAndExtractServer(version string, installBaseDir string) error {
|
||||||
|
targetDir := filepath.Join(installBaseDir, version)
|
||||||
|
|
||||||
|
if _, err := os.Stat(filepath.Join(targetDir, "VintagestoryServer")); err == nil {
|
||||||
|
fmt.Printf("[Downloader] Server version %s is already installed\n", version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("https://cdn.vintagestory.at/gamefiles/stable/vs_server_linux-x64_%s.tar.gz", version)
|
||||||
|
fmt.Printf("Establishing connection to '%s'\n", url)
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error connecting to '%s', %w", url, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
return fmt.Errorf("Version %s not found on the stable CDN branch (404)", version)
|
||||||
|
} else if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("Unexpected CDN response status: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Downloading and extracting archive to '%s'\n", targetDir)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("Failed to create version installation directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gzipReader, err := gzip.NewReader(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to initialize gzip decompressor: %w", err)
|
||||||
|
}
|
||||||
|
defer gzipReader.Close()
|
||||||
|
|
||||||
|
tarReader := tar.NewReader(gzipReader)
|
||||||
|
|
||||||
|
for {
|
||||||
|
header, err := tarReader.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error reading TAR stream: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetFilePath := filepath.Join(targetDir, header.Name)
|
||||||
|
|
||||||
|
switch header.Typeflag {
|
||||||
|
case tar.TypeDir:
|
||||||
|
if err := os.MkdirAll(targetFilePath, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed creating archive directory entry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case tar.TypeReg:
|
||||||
|
if err := os.MkdirAll(filepath.Dir(targetFilePath), 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed creating parent path for file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile, err := os.OpenFile(targetFilePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, header.FileInfo().Mode())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed creating target system file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||||
|
outFile.Close()
|
||||||
|
return fmt.Errorf("failed streaming file payload extraction: %w", err)
|
||||||
|
}
|
||||||
|
outFile.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binaryExecutor := filepath.Join(targetDir, "VintagestoryServer")
|
||||||
|
if err := patchBinaryForNixos(binaryExecutor); err != nil {
|
||||||
|
return fmt.Errorf("failed fixing platform compatibility gates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Server version %s successfully installed\n", version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func patchBinaryForNixos(binaryPath string) error {
|
||||||
|
if _, err := os.Stat("/etc/NIXOS"); os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("[NixOS Detected] Patching server interpreter pathway...")
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if interpreter == "" {
|
||||||
|
interpreter = "/lib/ld-linux-x86-64.so.2"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("patchelf", "--set-interpreter", interpreter, binaryPath)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("patchelf failed to link binary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1780243769,
|
||||||
|
"narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "331800de5053fcebacf6813adb5db9c9dca22a0c",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"utils": "utils"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
44
flake.nix
Normal file
44
flake.nix
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
description = "Dev shell";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, utils }:
|
||||||
|
utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
config = {
|
||||||
|
allowUnfree = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
dotnetRuntime = pkgs.dotnet-runtime_10.passthru.unwrapped;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
go
|
||||||
|
gopls
|
||||||
|
gotools
|
||||||
|
delve
|
||||||
|
golangci-lint
|
||||||
|
|
||||||
|
pkgs.dotnet-runtime_10
|
||||||
|
patchelf
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
export DOTNET_ROOT="${dotnetRuntime}/share/dotnet"
|
||||||
|
export DOTNET_ROOT_X64="${dotnetRuntime}/share/dotnet"
|
||||||
|
export LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib}/lib:${pkgs.zlib}/lib:/run/opengl-driver/lib:$LD_LIBRARY_PATH"
|
||||||
|
|
||||||
|
echo " == Dev shell loaded == "
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
49
instance.go
Normal file
49
instance.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InstanceState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateStopped InstanceState = "STOPPED"
|
||||||
|
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)
|
||||||
|
|
||||||
|
dirs := []string{
|
||||||
|
instanceDir,
|
||||||
|
filepath.Join(instanceDir, "Saves"),
|
||||||
|
filepath.Join(instanceDir, "Mods"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("Failed creating core instance directory layout: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceConfigPath := filepath.Join(instanceDir, "serverconfig.json")
|
||||||
|
if err := PrepareInstanceConfig(version, instanceConfigPath, options, cfg); err != nil {
|
||||||
|
return fmt.Errorf("Failed provisioning server baseline configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Instance '%s' (v%s) successfully initialized\n", name, version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
119
instance_config.go
Normal file
119
instance_config.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VsServerConfigOptions struct {
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
|
||||||
|
if _, err := os.Stat(instanceConfigPath); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
destination, err := os.Create(instanceConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed creating target instance configuration: %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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawConfig map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &rawConfig); err != nil {
|
||||||
|
return fmt.Errorf("failed parsing configuration JSON payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawConfig["ServerName"] = config.ServerName
|
||||||
|
rawConfig["Port"] = config.Port
|
||||||
|
rawConfig["MaxClients"] = config.MaxClients
|
||||||
|
|
||||||
|
if config.Password != "" {
|
||||||
|
rawConfig["Password"] = config.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")
|
||||||
|
}
|
||||||
|
|
||||||
|
if modPaths, ok := rawConfig["ModPaths"].([]interface{}); ok {
|
||||||
|
for i, pathVal := range modPaths {
|
||||||
|
if strPath, ok := pathVal.(string); ok && (strings.Contains(strPath, "/vs-manager/instances/") || filepath.IsAbs(strPath)) && strPath != "Mods" {
|
||||||
|
modPaths[i] = filepath.Join(instanceDir, "Mods")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedData, err := json.MarshalIndent(rawConfig, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
65
main.go
Normal file
65
main.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"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")
|
||||||
|
}
|
||||||
|
|
||||||
|
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: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = DownloadAndExtractServer(targetVersion, cfg.Storage.InstallDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Downloader module encountered an error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.")
|
||||||
|
}
|
||||||
116
process.go
Normal file
116
process.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProcessManager struct {
|
||||||
|
sync.RWMutex
|
||||||
|
ActiveInstances map[string]*exec.Cmd
|
||||||
|
StdinPipes map[string]io.WriteCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProcessManager() *ProcessManager {
|
||||||
|
return &ProcessManager{
|
||||||
|
ActiveInstances: make(map[string]*exec.Cmd),
|
||||||
|
StdinPipes: make(map[string]io.WriteCloser),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) StartInstance(name string, version string, options VsServerConfigOptions, config *AppConfig) error {
|
||||||
|
pm.Lock()
|
||||||
|
defer pm.Unlock()
|
||||||
|
|
||||||
|
if _, running := pm.ActiveInstances[name]; running {
|
||||||
|
return fmt.Errorf("Instance '%s' is already running", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
binaryPath := filepath.Join(config.Storage.InstallDir, version, "VintagestoryServer")
|
||||||
|
instanceDataPath := filepath.Join(config.Storage.InstancesDir, name)
|
||||||
|
|
||||||
|
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
|
||||||
|
fmt.Printf("Server version '%s' not installed, attempting to install\n", version)
|
||||||
|
install_err := DownloadAndExtractServer(version, config.Storage.InstallDir)
|
||||||
|
if install_err != nil {
|
||||||
|
return fmt.Errorf("Could not locate or download a server binary for version '%s'", version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(binaryPath, "--dataPath", instanceDataPath)
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
stdinPipe, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to open stdin pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdoutPipe, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
stdinPipe.Close()
|
||||||
|
return fmt.Errorf("failed to open stdout conduit: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
stdinPipe.Close()
|
||||||
|
stdoutPipe.Close()
|
||||||
|
return fmt.Errorf("Failed to start process thread: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.ActiveInstances[name] = cmd
|
||||||
|
pm.StdinPipes[name] = stdinPipe
|
||||||
|
|
||||||
|
go pm.streamLogs(name, stdoutPipe)
|
||||||
|
go pm.watchProcessExit(name, cmd)
|
||||||
|
|
||||||
|
fmt.Printf("Server instance '%s' successfully spawned under PID %d\n", name, cmd.Process.Pid)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) streamLogs(name string, stdout io.ReadCloser) {
|
||||||
|
defer stdout.Close()
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) watchProcessExit(name string, cmd *exec.Cmd) {
|
||||||
|
_ = cmd.Wait()
|
||||||
|
|
||||||
|
pm.Lock()
|
||||||
|
defer pm.Unlock()
|
||||||
|
|
||||||
|
delete(pm.ActiveInstances, name)
|
||||||
|
if pipe, exists := pm.StdinPipes[name]; exists {
|
||||||
|
pipe.Close()
|
||||||
|
delete(pm.StdinPipes, name)
|
||||||
|
}
|
||||||
|
fmt.Printf("Server instance '%s' has shut down or terminated.\n", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *ProcessManager) SendCommand(name string, command string) error {
|
||||||
|
pm.RLock()
|
||||||
|
pipe, exists := pm.StdinPipes[name]
|
||||||
|
pm.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("Cannot send command, instance with name '%s' is not running", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := io.WriteString(pipe, command+"\n")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed writing to server process stdin conduit: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user