167 lines
3.9 KiB
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
|
|
}
|