diff --git a/.gitignore b/.gitignore index a2349175..b1685147 100644 --- a/.gitignore +++ b/.gitignore @@ -36,10 +36,12 @@ modconfig.xml UIMod/config/config.json C:/custom/file.txt UIMod/config/customdetections.json +./autostart.sh +./autostart.ps1 winhttp.dll -autostart* __debug_bin* frontend/node_modules frontend/dist frontend/build UIMod/onboard_bundled/v2 +StationeersServerUI.lnk \ No newline at end of file diff --git a/UIMod/onboard_bundled/scripts/autostart.sh b/UIMod/onboard_bundled/scripts/autostart.sh index b3d220b4..a9ee7710 100644 --- a/UIMod/onboard_bundled/scripts/autostart.sh +++ b/UIMod/onboard_bundled/scripts/autostart.sh @@ -1,3 +1,78 @@ -#!/bin/bash +#!/usr/bin/env bash -# not yet implemented \ No newline at end of file +# This script serves two purposes: +# 1. Installation: Creates and configures a systemd service (ssui.service) to run the StationeersServerControl (StationeersServerUI) application. +# 2. Runtime: When executed and Service already installed, finds and runs the latest StationeersServerControl binary (matching StationeersServerControlv*). +# The systemd service uses ExecStart=$SCRIPT_PATH to run this script, which then dynamically selects the latest binary version of SSUI to run. +# Check if running as root to prevent installing a service as root + +if [[ $(id -u) = 0 ]]; then + echo "For security reasons, it is not recommended to run this service as a root user." + exit 10 +fi + +# Check if systemd is available +if [[ ! -d /run/systemd/system ]]; then + echo "Error: systemd is not the active init system." + exit 2 +fi + +# Determine the full path of this script +SCRIPT_PATH=$(readlink -f "$0") + +# Determine the base directory +BASEDIR=$(dirname "$SCRIPT_PATH") +if [[ -z "$BASEDIR" || ! -d "$BASEDIR" ]]; then + echo "Error: Could not determine base directory from SCRIPT_PATH: '$SCRIPT_PATH'." + exit 3 +fi + + +USERNAME=$(whoami) + +# Create the systemd service file pointing to this script +sudo tee /etc/systemd/system/stationeersserverui.service > /dev/null < /dev/null <:" + config.GetSSUIWebPort()) + logger.Core.Cleanf("\n 🌐 Web UI available at: https://localhost:8443 (default) or https://:%s", config.GetSSUIWebPort()) logger.Core.Cleanf("\n 🌐 Support available at: https://discord.gg/8n3vN92MyJ") // Quote @@ -49,5 +49,10 @@ func printFirstTimeSetupMessage() { logger.Core.Cleanf(" │ • Support is provided at https://discord.gg/8n3vN92MyJ │") logger.Core.Cleanf(" │ • For more details, check the GitHub Wiki: │") logger.Core.Cleanf(" │ • https://github.com/JacksonTheMaster/StationeersServerUI/v5/wiki │") + logger.Core.Cleanf(" │ │") + if runtime.GOOS == "linux" { + logger.Core.Cleanf(" │ • Autostart: │") + logger.Core.Cleanf(" │ • to run SSUI automatically on boot, run SSUI with the --setupautostart flag once. │") + } logger.Core.Cleanf(" └─────────────────────────────────────────────────────────────────────────────────────────────┘") } diff --git a/src/setup/autostart/autostart.go b/src/setup/autostart/autostart.go new file mode 100644 index 00000000..b8ba4555 --- /dev/null +++ b/src/setup/autostart/autostart.go @@ -0,0 +1,24 @@ +package autostart + +import ( + "fmt" +) + +func Initialize() error { + + err := SetupAutostartScripts() + if err != nil { + return fmt.Errorf("failed to setup autostart script, cannot proceed with autostart setup: %w", err) + } + + err = SetupBinarySymlink() + if err != nil { + return fmt.Errorf("failed to create symlink for autostart: %w", err) + } + + err = SetupService() + if err != nil { + return fmt.Errorf("failed to setup service: %w", err) + } + return nil +} diff --git a/src/setup/autostartscripts.go b/src/setup/autostart/autostartscripts.go similarity index 53% rename from src/setup/autostartscripts.go rename to src/setup/autostart/autostartscripts.go index 4fe653d4..691e7c4c 100644 --- a/src/setup/autostartscripts.go +++ b/src/setup/autostart/autostartscripts.go @@ -1,6 +1,7 @@ -package setup +package autostart import ( + "fmt" "io" "io/fs" "os" @@ -9,40 +10,58 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" ) -func SetupAutostartScripts() { +func SetupAutostartScripts() error { scriptFS, err := fs.Sub(config.V1UIFS, "UIMod/onboard_bundled/scripts") if err != nil { - return + return err } if runtime.GOOS == "windows" { script, err := scriptFS.Open("autostart.ps1") if err != nil { - return + return err } defer script.Close() data, err := io.ReadAll(script) if err != nil { - return + return err } + + _, err = os.Stat("autostart.ps1") + if err == nil { + err = os.Remove("autostart.ps1") + if err != nil { + return fmt.Errorf("failed to remove autostart.ps1: %w", err) + } + } + err = os.WriteFile("autostart.ps1", data, 0755) if err != nil { - return + return err } } if runtime.GOOS == "linux" { script, err := scriptFS.Open("autostart.sh") if err != nil { - return + return err } defer script.Close() data, err := io.ReadAll(script) if err != nil { - return + return err } - err = os.WriteFile("autostart.service", data, 0755) + _, err = os.Stat("autostart.sh") + if err == nil { + err = os.Remove("autostart.sh") + if err != nil { + return fmt.Errorf("failed to remove autostart.sh: %w", err) + } + } + + err = os.WriteFile("autostart.sh", data, 0755) if err != nil { - return + return err } } + return nil } diff --git a/src/setup/autostart/service-lin.go b/src/setup/autostart/service-lin.go new file mode 100644 index 00000000..74172fe2 --- /dev/null +++ b/src/setup/autostart/service-lin.go @@ -0,0 +1,72 @@ +//go:build linux + +package autostart + +import ( + "fmt" + "os" + "os/exec" + "time" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" +) + +func SetupService() error { + // run ./autostart.sh, which will ask for elevation when needed and setup the service + + //stat /etc/systemd/system/ssui.service and it it exists tell the user that the service is already installed + _, err := os.Stat("/etc/systemd/system/stationeersserverui.service") + if err == nil { + logger.Main.Error("Autostart service is already installed. Exiting in 5 seconds...") + time.Sleep(5 * time.Second) + os.Exit(1) + } + + exec.Command("chmod", "+x", "autostart.sh").Run() + exec.Command("./autostart.sh", "install").Run() + + err = exec.Command("chmod", "+x", "autostart.sh").Run() + if err != nil { + return fmt.Errorf("failed to chmod autostart.sh: %w", err) + } + cmd := exec.Command("./autostart.sh") + err = cmd.Run() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + // handle exit codes in a switch statement + switch exitErr.ExitCode() { + case 10: + return fmt.Errorf("autostart.sh failed: For security reasons, it is not recommended to run this service as a root user") + case 2: + return fmt.Errorf("autostart.sh Error: systemd is not available on this system") + case 3: + return fmt.Errorf("autostart.sh Error: Could not determine base directory") + case 4: + return fmt.Errorf("autostart.sh Failed to stop ssui.service") + case 5: + return fmt.Errorf("autostart.sh Error: Failed to create service file") + case 6: + return fmt.Errorf("autostart.sh Error: Failed to reload systemd daemon") + case 7: + return fmt.Errorf("autostart.sh Error: Failed to enable ssui.service") + case 8: + return fmt.Errorf("autostart.sh Error: Failed to start ssui.service") + default: + return fmt.Errorf("autostart.sh failed with exit code %d: %w", exitErr.ExitCode(), err) + } + } + + return fmt.Errorf("failed to run autostart.sh: %w", err) + } + // stat for the file to check if it exists, if it does, remove it + _, err = os.Stat("autostart.sh") + if err == nil { + err = os.Remove("autostart.sh") + if err != nil { + return fmt.Errorf("failed to remove autostart.sh: %w", err) + } + } + logger.Main.Info("Autostart setup complete. Restart SSUI without the --setupautostart flag to start normally. Exiting in 5 seconds...") + return nil + +} diff --git a/src/setup/autostart/service-win.go b/src/setup/autostart/service-win.go new file mode 100644 index 00000000..fef8917a --- /dev/null +++ b/src/setup/autostart/service-win.go @@ -0,0 +1,8 @@ +//go:build windows + +package autostart + +func SetupService() error { + //TODO: Windows implementation + return nil +} diff --git a/src/setup/autostart/symlink-linux.go b/src/setup/autostart/symlink-linux.go new file mode 100644 index 00000000..034f419d --- /dev/null +++ b/src/setup/autostart/symlink-linux.go @@ -0,0 +1,88 @@ +package autostart + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" +) + +func SetupBinarySymlink() error { + if runtime.GOOS != "linux" { + return fmt.Errorf("not on linux") + } + // Check if we are inside a docker container, if so, don't try to create a symlink + if IsInsideContainer() { + logger.Install.Info("Inside a container, skipping symlink creation.") + return fmt.Errorf("inside a container") + } + + if err := updateSymlink(); err != nil { + return err + } + return nil +} + +func updateSymlink() error { + // Get the absolute path of the current executable + executablePath, err := os.Executable() + if err != nil { + return err + } + + // Get the directory of the executable + executableDir := filepath.Dir(executablePath) + + // Construct the target filename (e.g., StationeersServerControl5.0.x86_64) + currentExecutableName := "StationeersServerControlv" + config.GetVersion() + ".x86_64" + + // Construct the full path to the target executable + targetPath := filepath.Join(executableDir, currentExecutableName) + + symlinkPath := filepath.Join(executableDir, "StationeersServerUI.lnk") + + // Remove existing symlink if it exists + if err := os.Remove(symlinkPath); err != nil && !os.IsNotExist(err) { + return err + } + + // Create the symlink + return os.Symlink(targetPath, symlinkPath) +} + +func IsInsideContainer() bool { + // Check .dockerenv file (Docker-specific) + if _, err := os.Stat("/.dockerenv"); err == nil { + return true + } + + // 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") { + return true + } + } + return false +}