//go:build windows package deploy import ( "archive/zip" "fmt" "io" "log" "net/http" "os" "os/exec" "path/filepath" ) // InstallSteamCMD downloads and installs SteamCMD for Windows into the given // install directory. If SteamCMD is already present it returns the existing // path without re-downloading. The returned string is the absolute path to // steamcmd.exe. func InstallSteamCMD(installDir string) (string, error) { steamcmdDir := filepath.Join(installDir, "steamcmd") steamcmdPath := filepath.Join(steamcmdDir, "steamcmd.exe") // Already installed — nothing to do. if _, err := os.Stat(steamcmdPath); err == nil { log.Printf("SteamCMD already installed at %s", steamcmdPath) return steamcmdPath, nil } if err := os.MkdirAll(steamcmdDir, 0755); err != nil { return "", fmt.Errorf("failed to create steamcmd directory %s: %w", steamcmdDir, err) } // Download the Windows zip. zipPath := filepath.Join(steamcmdDir, "steamcmd.zip") if err := downloadFile("https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip", zipPath); err != nil { return "", fmt.Errorf("failed to download steamcmd: %w", err) } // Extract the zip into steamcmdDir. if err := extractZip(zipPath, steamcmdDir); err != nil { return "", fmt.Errorf("failed to extract steamcmd.zip: %w", err) } // Verify the exe landed where expected. if _, err := os.Stat(steamcmdPath); err != nil { return "", fmt.Errorf("steamcmd.exe not found after extraction: %w", err) } log.Printf("SteamCMD installed successfully at %s", steamcmdPath) return steamcmdPath, nil } // RegisterService creates a Windows service for the Rust Dedicated Server // using sc.exe. If the caller does not have administrator privileges the // command will fail silently with a warning log. func RegisterService(installDir string, cfg DeployConfig) error { serverPath := filepath.Join(installDir, "server", "RustDedicated.exe") binPath := fmt.Sprintf(`"%s" -batchmode +server.hostname "%s" +server.port %d +rcon.port %d +rcon.password "%s" +rcon.web 1 +server.identity "corrosion" +server.maxplayers %d +server.worldsize %d +server.seed %d`, serverPath, cfg.ServerName, cfg.ServerPort, cfg.RconPort, cfg.RconPassword, cfg.MaxPlayers, cfg.WorldSize, cfg.Seed) cmd := exec.Command("sc.exe", "create", "RustServer", "binPath=", binPath, "start=", "auto") if out, err := cmd.CombinedOutput(); err != nil { log.Printf("WARNING: sc.exe create failed (may require admin): %v — output: %s", err, string(out)) } else { log.Println("Windows service RustServer registered successfully") } return nil } // downloadFile fetches url and writes the response body to dest on disk. func downloadFile(url, dest string) error { resp, err := http.Get(url) if err != nil { return fmt.Errorf("GET %s failed: %w", url, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("GET %s returned status %d", url, resp.StatusCode) } out, err := os.Create(dest) if err != nil { return fmt.Errorf("failed to create file %s: %w", dest, err) } defer out.Close() if _, err := io.Copy(out, resp.Body); err != nil { return fmt.Errorf("failed to write to %s: %w", dest, err) } return nil } // extractZip extracts all files from a zip archive into destDir, preserving // the directory structure from the archive. func extractZip(zipPath, destDir string) error { r, err := zip.OpenReader(zipPath) if err != nil { return fmt.Errorf("failed to open zip %s: %w", zipPath, err) } defer r.Close() for _, f := range r.File { target := filepath.Join(destDir, f.Name) if f.FileInfo().IsDir() { if err := os.MkdirAll(target, 0755); err != nil { return fmt.Errorf("failed to create directory %s: %w", target, err) } continue } // Ensure the parent directory exists. if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { return fmt.Errorf("failed to create parent dir for %s: %w", target, err) } rc, err := f.Open() if err != nil { return fmt.Errorf("failed to open zip entry %s: %w", f.Name, err) } outFile, err := os.Create(target) if err != nil { rc.Close() return fmt.Errorf("failed to create file %s: %w", target, err) } if _, err := io.Copy(outFile, rc); err != nil { outFile.Close() rc.Close() return fmt.Errorf("failed to extract %s: %w", f.Name, err) } outFile.Close() rc.Close() } return nil }