package deploy import ( "encoding/json" "fmt" "log" "os" "os/exec" "path/filepath" "runtime" "time" "github.com/nats-io/nats.go" ) // GameServerStarter abstracts the game server process manager so the deployer // can set the executable path and start the server without depending on the // concrete process.GameServer type. The existing GameServer will implement // UpdatePath in a separate task. type GameServerStarter interface { Start() error UpdatePath(path string) } // Deployer orchestrates one-click Rust server deployment. It downloads SteamCMD, // installs the Rust Dedicated Server, generates server.cfg, and starts the server // process — publishing progress updates to NATS at each stage so the frontend can // display real-time deployment status. type Deployer struct { nc *nats.Conn licenseID string installDir string gameServer GameServerStarter } // NewDeployer creates a new Deployer instance. func NewDeployer(nc *nats.Conn, licenseID, installDir string, gs GameServerStarter) *Deployer { return &Deployer{ nc: nc, licenseID: licenseID, installDir: installDir, gameServer: gs, } } // Deploy executes the full deployment pipeline: SteamCMD install, Rust server // download, config generation, and server startup. If any stage fails, a "failed" // status is published and the error is returned. Progress updates are published // to NATS at each stage transition. func (d *Deployer) Deploy(cfg DeployConfig) error { // Stage 1: SteamCMD log.Printf("Deploy: starting SteamCMD installation for license %s", d.licenseID) d.publishStatus("downloading_steamcmd", 0, "Checking for existing SteamCMD installation...") steamcmdPath, err := InstallSteamCMD(d.installDir) if err != nil { d.publishStatus("failed", 0, "SteamCMD installation failed", err.Error()) return fmt.Errorf("steamcmd install failed: %w", err) } log.Printf("Deploy: SteamCMD ready at %s", steamcmdPath) d.publishStatus("downloading_steamcmd", 100, "SteamCMD ready") // Stage 2: Download Rust Dedicated Server log.Printf("Deploy: downloading Rust Dedicated Server via SteamCMD") d.publishStatus("downloading_rust", 0, "Downloading Rust Dedicated Server via SteamCMD...") if err := DownloadRustServer(steamcmdPath, d.installDir); err != nil { d.publishStatus("failed", 0, "Rust server download failed", err.Error()) return fmt.Errorf("rust server download failed: %w", err) } log.Printf("Deploy: Rust Dedicated Server installed") d.publishStatus("downloading_rust", 100, "Rust Dedicated Server installed") // Stage 3: Generate server.cfg log.Printf("Deploy: generating server.cfg") d.publishStatus("configuring", 0, "Generating server.cfg...") if err := GenerateServerCfg(d.installDir, cfg); err != nil { d.publishStatus("failed", 0, "Server configuration failed", err.Error()) return fmt.Errorf("config generation failed: %w", err) } log.Printf("Deploy: server.cfg written") d.publishStatus("configuring", 100, "Server configured") // Stage 4: Start the server log.Printf("Deploy: starting Rust server") d.publishStatus("starting", 0, "Starting Rust server...") var exePath string switch runtime.GOOS { case "windows": exePath = filepath.Join(d.installDir, "server", "RustDedicated.exe") default: exePath = filepath.Join(d.installDir, "server", "RustDedicated") } d.gameServer.UpdatePath(exePath) if err := d.gameServer.Start(); err != nil { d.publishStatus("failed", 0, "Server failed to start", err.Error()) return fmt.Errorf("server start failed: %w", err) } log.Printf("Deploy: Rust server is now running") d.publishStatus("online", 100, "Rust server is now running") return nil } // DownloadRustServer runs SteamCMD to download/update the Rust Dedicated Server // (App ID 258550) into {installDir}/server. This function is platform-agnostic — // it simply executes the steamcmd binary which was installed by the platform-specific // InstallSteamCMD function. func DownloadRustServer(steamcmdPath, installDir string) error { serverDir := filepath.Join(installDir, "server") log.Printf("Downloading Rust Dedicated Server to %s", serverDir) cmd := exec.Command(steamcmdPath, "+login", "anonymous", "+force_install_dir", serverDir, "+app_update", "258550", "validate", "+quit", ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("steamcmd app_update 258550 failed: %w", err) } return nil } // CheckServerInstalled returns true if the Rust Dedicated Server executable // exists at the expected path within the install directory. func CheckServerInstalled(installDir string) bool { var exePath string switch runtime.GOOS { case "windows": exePath = filepath.Join(installDir, "server", "RustDedicated.exe") default: exePath = filepath.Join(installDir, "server", "RustDedicated") } _, err := os.Stat(exePath) return err == nil } // publishStatus publishes a DeployStatus message to the NATS subject // corrosion.{licenseID}.deploy.status. Publish errors are logged but do not // fail the deployment — losing a progress update is not fatal. func (d *Deployer) publishStatus(stage string, progress int, message string, errDetail ...string) { subject := fmt.Sprintf("corrosion.%s.deploy.status", d.licenseID) status := DeployStatus{ Stage: stage, Progress: progress, Message: message, Timestamp: time.Now().UTC().Format(time.RFC3339), } if len(errDetail) > 0 && errDetail[0] != "" { status.Error = errDetail[0] } data, err := json.Marshal(status) if err != nil { log.Printf("Failed to marshal deploy status: %v", err) return } if err := d.nc.Publish(subject, data); err != nil { log.Printf("Failed to publish deploy status to %s: %v", subject, err) } }