diff --git a/.gitignore b/.gitignore index a2349175..ecfce688 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ UIMod/tls/cert.pem UIMod/tls/key.pem steamapps/** steamcmd/** +Steam/** rocketstation_BurstDebugInformation_DoNotShip/** StationeersServerControlv* UnityPlayer.so diff --git a/.vscode/launch.json b/.vscode/launch.json index 5e1d543e..9b4ebad8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,6 +19,16 @@ "program": "${workspaceFolder}/server.go", "console": "integratedTerminal", "showLog": false, // Hides some Go Debugger(Delve) log stuff that is not useful for debugging atm + }, + { + "name": "Debug Go Server noSteamCMD noSvelte", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}/server.go", + "console": "integratedTerminal", + "showLog": false, // Hides some Go Debugger(Delve) log stuff that is not useful for debugging atm + "args": ["--SkipSteamCMD"] } ] } \ No newline at end of file diff --git a/server.go b/server.go index 5045ee97..9f8226a4 100644 --- a/server.go +++ b/server.go @@ -37,7 +37,8 @@ var v1uiFS embed.FS func main() { var wg sync.WaitGroup logger.ConfigureConsole() - loader.SetupWorkingDir() + loader.SanityCheck(&wg) + wg.Wait() logger.Main.Debug("Initializing resources...") loader.InitVirtFS(v1uiFS) logger.Install.Info("Starting setup...") diff --git a/src/config/getters.go b/src/config/getters.go index 936a4bbd..bbc9e402 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -493,3 +493,9 @@ func GetExtractedGameVersion() string { defer ConfigMu.RUnlock() return ExtractedGameVersion } + +func GetSkipSteamCMD() bool { + ConfigMu.RLock() + defer ConfigMu.RUnlock() + return SkipSteamCMD +} diff --git a/src/config/setters.go b/src/config/setters.go index 401b53c5..44a3cf9f 100644 --- a/src/config/setters.go +++ b/src/config/setters.go @@ -46,6 +46,22 @@ func SetExtractedGameVersion(value string) error { return nil } +func SetSkipSteamCMD(value bool) error { + ConfigMu.Lock() + defer ConfigMu.Unlock() + + SkipSteamCMD = value + return nil +} + +func SetIsDockerContainer(value bool) error { + ConfigMu.Lock() + defer ConfigMu.Unlock() + + IsDockerContainer = value + return nil +} + // ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT // ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT // ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT diff --git a/src/config/vars.go b/src/config/vars.go index 0ba6fb1a..71a6728d 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -61,8 +61,15 @@ var ( LanguageSetting string AutoStartServerOnStartup bool SSUIIdentifier string - CurrentBranchBuildID string // ONLY RUNTIME - ExtractedGameVersion string // ONLY RUNTIME +) + +// Runtime only variables + +var ( + CurrentBranchBuildID string // ONLY RUNTIME + ExtractedGameVersion string // ONLY RUNTIME + SkipSteamCMD bool // ONLY RUNTIME + IsDockerContainer bool // ONLY RUNTIME ) // Discord integration diff --git a/src/core/loader/cmdargs.go b/src/core/loader/cmdargs.go index 787d63e9..e1a950b5 100644 --- a/src/core/loader/cmdargs.go +++ b/src/core/loader/cmdargs.go @@ -21,6 +21,7 @@ func LoadCmdArgs() { var createSSUILogFile bool var recoveryPassword string var devMode bool + var skipSteamCMD bool flag.StringVar(&backendEndpointPort, "BackendEndpointPort", "", "Override the backend endpoint port (e.g., 8080)") flag.StringVar(&backendEndpointPort, "p", "", "(Alias) Override the backend endpoint port (e.g., 8080)") @@ -35,6 +36,8 @@ func LoadCmdArgs() { flag.BoolVar(&isDebugMode, "debug", false, "(Alias) Enable debug mode") flag.BoolVar(&createSSUILogFile, "CreateSSUILogFile", false, "Create a log file for SSUI") flag.BoolVar(&createSSUILogFile, "lf", false, "(Alias) Create a log file for SSUI") + flag.BoolVar(&skipSteamCMD, "SkipSteamCMD", false, "Skips SteamCMD installation") + flag.BoolVar(&skipSteamCMD, "nosteam", false, "(Alias) Skips SteamCMD installation") // Parse command-line flags flag.Parse() @@ -47,6 +50,10 @@ func LoadCmdArgs() { logger.Main.Info("Dev mode enabled: Auth enabled, admin user set to admin:admin:superadmin, console enabled") } + if skipSteamCMD { + config.SetSkipSteamCMD(true) + } + if backendEndpointPort != "" && backendEndpointPort != "8443" { oldPort := config.GetSSUIWebPort() config.SetSSUIWebPort(backendEndpointPort) diff --git a/src/core/loader/helpers.go b/src/core/loader/helpers.go index 29e129fa..98f3ec86 100644 --- a/src/core/loader/helpers.go +++ b/src/core/loader/helpers.go @@ -1,10 +1,9 @@ package loader import ( + "bufio" "fmt" "os" - "path/filepath" - "runtime" "strings" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" @@ -165,33 +164,35 @@ func PrintConfigDetails(logLevel ...string) { logger.Config.Debug("=======================================") } -// SetupWorkingDir sets the working directory to the directory of the executable to prevent user errors -func SetupWorkingDir() error { - if runtime.GOOS == "windows" { - // For now Windows doesn't have symlinking issues so we'll just let is use the current working directory - return nil +func IsInsideContainer() bool { + // Check .dockerenv file (Docker-specific) + if _, err := os.Stat("/.dockerenv"); err == nil { + config.SetIsDockerContainer(true) + return true } - if runtime.GOOS == "linux" { - // Get the current executable path from /proc/self/exe - exePath, err := os.Readlink("/proc/self/exe") - if err != nil { - return err - } - // Get the directory path of the executable - dirPath := filepath.Dir(exePath) - // Change the working directory to the executable's directory - cwd, err := os.Getwd() - if err != nil { - return err - } - if cwd != dirPath { - logger.Core.Debug("Changing working directory to " + dirPath) - err = os.Chdir(dirPath) - if err != nil { - return err - } + // Check cgroup (works for Docker and other container runtimes) + return isContainerFromCGroup() +} + +func isContainerFromCGroup() bool { + file, err := os.Open("/proc/1/cgroup") + if err != nil { + return false + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + // Check for various container runtime indicators + if strings.Contains(line, "docker") || + strings.Contains(line, "containerd") || + strings.Contains(line, "kubepods") || + strings.Contains(line, "crio") || + strings.Contains(line, "libpod") { + config.SetIsDockerContainer(true) + return true } - return nil } - return nil + return false } diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index 72830a12..a92f8d3b 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -3,7 +3,9 @@ package loader import ( "embed" + "os" "sync" + "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discordbot" @@ -98,3 +100,14 @@ func ReloadAppInfoPoller() { func InitVirtFS(v1uiFS embed.FS) { config.SetV1UIFS(v1uiFS) } + +func SanityCheck(wg *sync.WaitGroup) { + wg.Add(1) + defer wg.Done() + err := runSanityCheck() + if err != nil { + logger.Main.Error("Sanity check failed, exiting in 10 secconds: " + err.Error()) + time.Sleep(10 * time.Second) + os.Exit(1) + } +} diff --git a/src/core/loader/sanitycheck.go b/src/core/loader/sanitycheck.go new file mode 100644 index 00000000..fc5d2378 --- /dev/null +++ b/src/core/loader/sanitycheck.go @@ -0,0 +1,69 @@ +package loader + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" +) + +func runSanityCheck() error { + + if runtime.GOOS == "windows" { + return nil + } + + // Check if running as root (UID 0) + if os.Geteuid() == 0 { + // Check if running inside a container + if !IsInsideContainer() { + return fmt.Errorf("root: SSUI should not be run as root") + } + } + + // Get the current executable path from /proc/self/exe + exePath, err := os.Readlink("/proc/self/exe") + if err != nil { + return err + } + // Get the directory path of the executable + dirPath := filepath.Dir(exePath) + // Change the working directory to the executable's directory + cwd, err := os.Getwd() + if err != nil { + return err + } + + if cwd != dirPath && !strings.Contains(dirPath, "/tmp") { + err = os.Chdir(dirPath) + if err != nil { + return err + } + } + + // Check if current working directory is writable + workDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get working directory: %w", err) + } + + // Try to create a temporary file to test write permissions + testFile := filepath.Join(workDir, ".write_test") + if err := os.WriteFile(testFile, []byte("test"), 0600); err != nil { + return fmt.Errorf("cannot write to working directory, please make sure your user has write permissions in %s: %w", workDir, err) + } + // Clean up test file + if err := os.Remove(testFile); err != nil { + return fmt.Errorf("failed to clean up sanity check writetest file: %w", err) + } + + // Check if steamcmd package is installed (requires further testing, disabled for now) + //cmd := exec.Command("dpkg-query", "-W", "-f='${Status}'", "steamcmd") + //output, err := cmd.CombinedOutput() + //if err == nil && strings.Contains(string(output), "install ok installed") { + // return fmt.Errorf("steamcmd package is installed") + //} + + return nil +} diff --git a/src/setup/install.go b/src/setup/install.go index 12a33e80..dad418ad 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -42,7 +42,9 @@ func Install(wg *sync.WaitGroup) { logger.Install.Info("✅Blacklist.txt verified or created.") // Step 3: Install and run SteamCMD logger.Install.Info("🔄Installing and running SteamCMD...") - if config.GetBranch() != "indev-no-steamcmd" { + if config.GetSkipSteamCMD() { + logger.Install.Info("✅Skipping SteamCMD installation, SkipSteamCMD is true") + } else { steamcmd.InstallAndRunSteamCMD() } logger.Install.Info("✅Setup complete!") diff --git a/src/steamcmd/getappinfo.go b/src/steamcmd/getappinfo.go index ba6c6f89..53e68541 100644 --- a/src/steamcmd/getappinfo.go +++ b/src/steamcmd/getappinfo.go @@ -4,10 +4,12 @@ import ( "bytes" "fmt" "maps" + "os" "os/exec" "path/filepath" "regexp" "runtime" + "strings" "sync" "time" @@ -64,6 +66,13 @@ func AppInfoPoller() { // getAppInfo fetches the branches and their build IDs for the specified app ID using SteamCMD // and stores them in the package-level branches map. func getAppInfo() error { + + currentDir, err := os.Getwd() + if err != nil { + logger.Install.Error("❌ Error getting current working directory: " + err.Error() + "\n") + return err + } + if steamMu.TryLock() { // Successfully acquired the lock; no other func holds it logger.Core.Debug("🔄 Locking SteamMu for SteamCMD AppInfo...") @@ -90,14 +99,32 @@ func getAppInfo() error { cmd.Stdout = &stdout cmd.Stderr = &stderr + if runtime.GOOS == "linux" { + env := os.Environ() + // Replace or set HOME + newEnv := make([]string, 0, len(env)+1) + foundHome := false + for _, e := range env { + if !strings.HasPrefix(e, "HOME=") { + newEnv = append(newEnv, e) + } else { + newEnv = append(newEnv, "HOME="+currentDir) + foundHome = true + } + } + if !foundHome { + newEnv = append(newEnv, "HOME="+currentDir) + } + cmd.Env = newEnv + } + // Log the command //if config.GetLogLevel() == 10 { // cmdString := strings.Join(cmd.Args, " ") // logger.Install.Debug("🕑 Running SteamCMD for app info: " + cmdString) //} - // Run the command - err := cmd.Run() + err = cmd.Run() if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { logger.Install.Errorf("❌ SteamCMD app info failed (code %d): %s\n", exitErr.ExitCode(), stderr.String()) diff --git a/src/steamcmd/steamcmd.go b/src/steamcmd/steamcmd.go index 292ed1ce..7312e714 100644 --- a/src/steamcmd/steamcmd.go +++ b/src/steamcmd/steamcmd.go @@ -101,6 +101,25 @@ func runSteamCMD(steamCMDDir string) (int, error) { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr + if runtime.GOOS == "linux" { + env := os.Environ() + // Replace or set HOME + newEnv := make([]string, 0, len(env)+1) + foundHome := false + for _, e := range env { + if !strings.HasPrefix(e, "HOME=") { + newEnv = append(newEnv, e) + } else { + newEnv = append(newEnv, "HOME="+currentDir) + foundHome = true + } + } + if !foundHome { + newEnv = append(newEnv, "HOME="+currentDir) + } + cmd.Env = newEnv + } + // Run the command if config.GetLogLevel() == 10 { cmdString := strings.Join(cmd.Args, " ")