Implements complete companion agent for Rust servers not on managed panels.
Features:
- NATS integration with token auth and auto-reconnect
- Game server process management (start/stop/restart/monitor)
- File operations (read/write/delete/list) via NATS
- SteamCMD integration for automated updates
- Self-update capability with download and replace
- Heartbeat publishing every 60s with server status
- Graceful shutdown handling (SIGTERM/SIGINT)
- Zombie process prevention via cmd.Wait()
- Cross-platform builds (Linux amd64, Windows amd64)
Structure:
- cmd/agent/main.go: Entry point, config, signal handling
- internal/app/daemon.go: Main loop, NATS subscriptions
- internal/client/nats.go: NATS connection with reconnect
- internal/process/gameserver.go: Process management
- internal/process/steamcmd.go: Steam update execution
- internal/files/operations.go: File system operations
- internal/update/updater.go: Self-update logic
- Makefile: Cross-compilation targets
- README.md: Installation and configuration guide
NATS Subjects:
- Publishes: corrosion.{license_id}.companion.heartbeat
- Publishes: corrosion.{license_id}.files.response
- Subscribes: corrosion.{license_id}.cmd.server
- Subscribes: corrosion.{license_id}.files.{get|put|delete|list}
- Subscribes: corrosion.{license_id}.update.steam
- Subscribes: corrosion.{license_id}.update.companion
Built binaries: 7.0MB (Linux), 7.2MB (Windows)
Total code: 1,356 LOC across 8 files
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
139 lines
3.3 KiB
Go
139 lines
3.3 KiB
Go
package process
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
rustAppID = "258550" // Rust Dedicated Server App ID
|
|
)
|
|
|
|
// SteamCMD handles SteamCMD operations for game server updates
|
|
type SteamCMD struct {
|
|
path string
|
|
}
|
|
|
|
// NewSteamCMD creates a new SteamCMD instance
|
|
func NewSteamCMD(path string) *SteamCMD {
|
|
return &SteamCMD{
|
|
path: path,
|
|
}
|
|
}
|
|
|
|
// UpdateRustServer updates the Rust Dedicated Server via SteamCMD
|
|
func (sc *SteamCMD) UpdateRustServer(validate bool) error {
|
|
log.Printf("Starting SteamCMD update for Rust Server (validate=%v)", validate)
|
|
|
|
// Check if SteamCMD exists
|
|
if _, err := os.Stat(sc.path); os.IsNotExist(err) {
|
|
return fmt.Errorf("steamcmd not found at: %s", sc.path)
|
|
}
|
|
|
|
startTime := time.Now()
|
|
|
|
// Build SteamCMD command
|
|
// +login anonymous +force_install_dir /path/to/rust +app_update 258550 validate +quit
|
|
args := []string{
|
|
"+login", "anonymous",
|
|
"+force_install_dir", getServerInstallDir(),
|
|
"+app_update", rustAppID,
|
|
}
|
|
|
|
if validate {
|
|
args = append(args, "validate")
|
|
}
|
|
|
|
args = append(args, "+quit")
|
|
|
|
log.Printf("Executing: %s %v", sc.path, args)
|
|
|
|
// Create command
|
|
cmd := exec.Command(sc.path, args...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
// Run SteamCMD (this will block until update completes)
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("steamcmd update failed: %w", err)
|
|
}
|
|
|
|
duration := time.Since(startTime)
|
|
log.Printf("SteamCMD update completed in %v", duration)
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateRustServerWithPath updates the Rust server to a specific install directory
|
|
func (sc *SteamCMD) UpdateRustServerWithPath(installPath string, validate bool) error {
|
|
log.Printf("Starting SteamCMD update for Rust Server at %s (validate=%v)", installPath, validate)
|
|
|
|
// Check if SteamCMD exists
|
|
if _, err := os.Stat(sc.path); os.IsNotExist(err) {
|
|
return fmt.Errorf("steamcmd not found at: %s", sc.path)
|
|
}
|
|
|
|
startTime := time.Now()
|
|
|
|
// Build SteamCMD command
|
|
args := []string{
|
|
"+login", "anonymous",
|
|
"+force_install_dir", installPath,
|
|
"+app_update", rustAppID,
|
|
}
|
|
|
|
if validate {
|
|
args = append(args, "validate")
|
|
}
|
|
|
|
args = append(args, "+quit")
|
|
|
|
log.Printf("Executing: %s %v", sc.path, args)
|
|
|
|
// Create command
|
|
cmd := exec.Command(sc.path, args...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
// Run SteamCMD
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("steamcmd update failed: %w", err)
|
|
}
|
|
|
|
duration := time.Since(startTime)
|
|
log.Printf("SteamCMD update completed in %v", duration)
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckSteamCMDInstalled verifies SteamCMD is installed and executable
|
|
func (sc *SteamCMD) CheckSteamCMDInstalled() error {
|
|
if _, err := os.Stat(sc.path); os.IsNotExist(err) {
|
|
return fmt.Errorf("steamcmd not found at: %s", sc.path)
|
|
}
|
|
|
|
// Try to execute with --help or similar to verify it's executable
|
|
cmd := exec.Command(sc.path, "+quit")
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("steamcmd is not executable or working: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getServerInstallDir returns the default server installation directory
|
|
// This should ideally come from configuration, but we provide a fallback
|
|
func getServerInstallDir() string {
|
|
// Try to determine from GAME_SERVER_PATH environment variable
|
|
serverPath := os.Getenv("GAME_SERVER_PATH")
|
|
if serverPath != "" {
|
|
return getDirectory(serverPath)
|
|
}
|
|
|
|
// Default fallback paths by OS
|
|
return "/home/rustserver/server"
|
|
}
|