Files
vssm/process.go

167 lines
3.9 KiB
Go

package main
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
)
type ProcessManager struct {
sync.RWMutex
ActiveInstances map[string]*exec.Cmd
StdinPipes map[string]io.WriteCloser
LogBuffers map[string]*InstanceLogBuffer
}
type InstanceLogBuffer struct {
sync.RWMutex
Lines []string
MaxLines int
}
func NewProcessManager() *ProcessManager {
return &ProcessManager{
ActiveInstances: make(map[string]*exec.Cmd),
StdinPipes: make(map[string]io.WriteCloser),
LogBuffers: map[string]*InstanceLogBuffer{},
}
}
func (pm *ProcessManager) StartInstance(name string, version string, meta InstanceMetadata, 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
pm.LogBuffers[name] = NewInstanceLogBuffer(200)
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() {
line := scanner.Text()
fmt.Printf("[%s]: %s\n", name, line)
pm.RLock()
buf, exists := pm.LogBuffers[name]
pm.RUnlock()
if exists {
buf.Append(line)
}
}
}
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)
}
if command != "" && !strings.HasPrefix(command, "/") {
command = "/" + command
}
_, err := io.WriteString(pipe, command+"\n")
if err != nil {
return fmt.Errorf("failed writing to server process stdin conduit: %w", err)
}
return nil
}
func NewInstanceLogBuffer(maxLines int) *InstanceLogBuffer {
return &InstanceLogBuffer{
Lines: make([]string, 0, maxLines),
MaxLines: maxLines,
}
}
func (lb *InstanceLogBuffer) Append(line string) {
lb.Lock()
defer lb.Unlock()
if len(lb.Lines) >= lb.MaxLines {
// Shift array out by dropping index 0
lb.Lines = lb.Lines[1:]
}
lb.Lines = append(lb.Lines, line)
}
func (lb *InstanceLogBuffer) GetSnapshot() []string {
lb.RLock()
defer lb.RUnlock()
// Return a copy so the caller can safely iterate or serialize to JSON without lock conflicts
snapshot := make([]string, len(lb.Lines))
copy(snapshot, lb.Lines)
return snapshot
}