Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ UIMod/tls/cert.pem
UIMod/tls/key.pem
steamapps/**
steamcmd/**
Steam/**
rocketstation_BurstDebugInformation_DoNotShip/**
StationeersServerControlv*
UnityPlayer.so
Expand Down
10 changes: 10 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
]
}
3 changes: 2 additions & 1 deletion server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...")
Expand Down
6 changes: 6 additions & 0 deletions src/config/getters.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,3 +493,9 @@ func GetExtractedGameVersion() string {
defer ConfigMu.RUnlock()
return ExtractedGameVersion
}

func GetSkipSteamCMD() bool {
ConfigMu.RLock()
defer ConfigMu.RUnlock()
return SkipSteamCMD
}
16 changes: 16 additions & 0 deletions src/config/setters.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions src/config/vars.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/core/loader/cmdargs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand All @@ -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()
Expand All @@ -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)
Expand Down
57 changes: 29 additions & 28 deletions src/core/loader/helpers.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package loader

import (
"bufio"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/JacksonTheMaster/StationeersServerUI/v5/src/config"
Expand Down Expand Up @@ -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
}
13 changes: 13 additions & 0 deletions src/core/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
}
69 changes: 69 additions & 0 deletions src/core/loader/sanitycheck.go
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 3 additions & 1 deletion src/setup/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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!")
Expand Down
31 changes: 29 additions & 2 deletions src/steamcmd/getappinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"bytes"
"fmt"
"maps"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -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...")
Expand All @@ -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())
Expand Down
19 changes: 19 additions & 0 deletions src/steamcmd/steamcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, " ")
Expand Down