feat(runner): More runner improvements (#294)

## Description
Whew..

- Steam can now run without namespaces using live-patcher (because
Docker..)
- Improved NVIDIA GPU selection and handling
- Pipeline tests for GPU picking logic
- Optimizations and cleanup all around
- SSH (by default disabled) for easier instance debugging.
- CachyOS' Proton because that works without namespaces (couldn't figure
out how to enable automatically in Steam yet..)
- Package updates and partial removal of futures (libp2p is going to
switch to Tokio in next release hopefully)



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- SSH server can now be enabled within the container for remote access
when configured.
- Added persistent live patching for Steam runtime entrypoints to
improve compatibility with namespace-less applications.
- Enhanced GPU selection with multi-GPU support and PCI bus ID matching
for improved hardware compatibility.
- Improved encoder selection by runtime testing of video encoders for
better reliability.
  - Added WebSocket transport support in peer-to-peer networking.
- Added flexible compositor and application launching with configurable
commands and improved socket handling.

- **Bug Fixes**
- Addressed NVIDIA-specific GStreamer issues by setting new environment
variables.
  - Improved error handling and logging for GPU and encoder selection.
- Fixed process monitoring to handle patcher restarts and added cleanup
logic.
- Added GStreamer cache clearing workaround for Wayland socket failures.

- **Improvements**
- Real-time logging of container processes to standard output and error
for easier monitoring.
- Enhanced process management and reduced CPU usage in protocol handling
loops.
- Updated dependency versions for greater stability and feature support.
  - Improved audio capture defaults and expanded audio pipeline support.
- Enhanced video pipeline setup with conditional handling for different
encoder APIs and DMA-BUF support.
- Refined concurrency and lifecycle management in protocol messaging for
increased robustness.
- Consistent namespace usage and updated crate references across the
codebase.
- Enhanced SSH configuration with key management, port customization,
and startup verification.
  - Improved GPU and video encoder integration in pipeline construction.
- Simplified error handling and consolidated write operations in
protocol streams.
- Removed Ludusavi installation from container image and updated package
installations.

- **Other**
- Minor formatting and style changes for better code readability and
maintainability.
- Docker build context now ignores `.idea` directory to streamline
builds.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
This commit is contained in:
Kristian Ollikainen
2025-07-07 09:06:48 +03:00
committed by GitHub
parent 191c59d230
commit 41dca22d9d
21 changed files with 2049 additions and 641 deletions

View File

@@ -0,0 +1,10 @@
#!/bin/bash
while true; do
case "$1" in
--*) shift ;;
*) break ;;
esac
done
exec "$@"

View File

@@ -13,7 +13,7 @@ log() {
# Ensures user directory ownership
chown_user_directory() {
local user_group="${USER}:${GID}"
if ! chown -R -h --no-preserve-root "$user_group" "${HOME}" 2>/dev/null; then
if ! chown -h --no-preserve-root "$user_group" "${HOME}" 2>/dev/null; then
echo "Error: Failed to change ownership of ${HOME} to ${user_group}" >&2
return 1
fi
@@ -36,6 +36,12 @@ wait_for_socket() {
return 1
}
# Prepares environment for namespace-less applications (like Steam)
setup_namespaceless() {
rm -f /run/systemd/container || true
mkdir -p /run/pressure-vessel || true
}
# Ensures cache directory exists
setup_cache() {
log "Setting up NVIDIA driver cache directory at $CACHE_DIR..."
@@ -103,8 +109,10 @@ get_nvidia_installer() {
install_nvidia_driver() {
local filename="$1"
log "Installing NVIDIA driver components from $filename..."
sudo ./"$filename" \
bash ./"$filename" \
--silent \
--skip-depmod \
--skip-module-unload \
--no-kernel-module \
--install-compat32-libs \
--no-nouveau-check \
@@ -116,18 +124,102 @@ install_nvidia_driver() {
log "Error: NVIDIA driver installation failed."
return 1
}
# Install CUDA package
log "Checking if CUDA is already installed"
if ! pacman -Q cuda &>/dev/null; then
log "Installing CUDA package"
pacman -S --noconfirm cuda --assume-installed opencl-nvidia
else
log "CUDA package is already installed, skipping"
fi
log "NVIDIA driver installation completed."
return 0
}
function log_gpu_info {
log_gpu_info() {
if ! declare -p vendor_devices &>/dev/null; then
log "Warning: vendor_devices array is not defined"
return
fi
log "Detected GPUs:"
for vendor in "${!vendor_devices[@]}"; do
log "> $vendor: ${vendor_devices[$vendor]}"
done
}
configure_ssh() {
# Return early if SSH not enabled
if [ -z "${SSH_ENABLE_PORT+x}" ] || [ "${SSH_ENABLE_PORT:-0}" -eq 0 ]; then
return 0
fi
# Check if we have required key
if [ -z "${SSH_ALLOWED_KEY+x}" ] || [ -z "${SSH_ALLOWED_KEY}" ]; then
return 0
fi
log "Configuring SSH server on port ${SSH_ENABLE_PORT} with public key authentication"
# Ensure SSH host keys exist
ssh-keygen -A 2>/dev/null || {
log "Error: Failed to generate SSH host keys"
return 1
}
# Create .ssh directory and authorized_keys file for nestri user
mkdir -p /home/nestri/.ssh
echo "${SSH_ALLOWED_KEY}" > /home/nestri/.ssh/authorized_keys
chmod 700 /home/nestri/.ssh
chmod 600 /home/nestri/.ssh/authorized_keys
chown -R nestri:nestri /home/nestri/.ssh
# Update SSHD config
sed -i -E "s/^#?Port .*/Port ${SSH_ENABLE_PORT}/" /etc/ssh/sshd_config || {
log "Error: Failed to update SSH port configuration"
return 1
}
# Configure secure SSH settings
{
echo "PasswordAuthentication no"
echo "PermitRootLogin no"
echo "ChallengeResponseAuthentication no"
echo "UsePAM no"
echo "PubkeyAuthentication yes"
} | while read -r line; do
grep -qF "$line" /etc/ssh/sshd_config || echo "$line" >> /etc/ssh/sshd_config
done
# Start SSH server
log "Starting SSH server on port ${SSH_ENABLE_PORT}"
/usr/sbin/sshd -D -p "${SSH_ENABLE_PORT}" &
SSH_PID=$!
# Verify the process started
if ! ps -p $SSH_PID > /dev/null 2>&1; then
log "Error: SSH server failed to start"
return 1
fi
log "SSH server started with PID ${SSH_PID}"
return 0
}
main() {
# Configure SSH
if [ -n "${SSH_ENABLE_PORT+x}" ] && [ "${SSH_ENABLE_PORT:-0}" -ne 0 ] && \
[ -n "${SSH_ALLOWED_KEY+x}" ] && [ -n "${SSH_ALLOWED_KEY}" ]; then
if ! configure_ssh; then
log "Error: SSH configuration failed with given variables - exiting"
exit 1
fi
else
log "SSH not configured (missing SSH_ENABLE_PORT or SSH_ALLOWED_KEY)"
fi
# Wait for required sockets
wait_for_socket "/run/dbus/system_bus_socket" "DBus" || exit 1
wait_for_socket "/run/user/${UID}/pipewire-0" "PipeWire" || exit 1
@@ -215,6 +307,10 @@ main() {
log "Ensuring user directory permissions..."
chown_user_directory || exit 1
# Setup namespaceless env
log "Applying namespace-less configuration"
setup_namespaceless
# Switch to nestri user
log "Switching to nestri user for application startup..."
if [[ ! -x /etc/nestri/entrypoint_nestri.sh ]]; then

View File

@@ -1,5 +1,4 @@
#!/bin/bash
set -euo pipefail
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
@@ -50,6 +49,70 @@ kill_if_running() {
fi
}
# Starts up Steam namespace-less live-patcher
start_steam_namespaceless_patcher() {
kill_if_running "${PATCHER_PID:-}" "steam-patcher"
local entrypoints=(
"${HOME}/.local/share/Steam/steamrt64/steam-runtime-steamrt/_v2-entry-point"
"${HOME}/.local/share/Steam/steamapps/common/SteamLinuxRuntime_soldier/_v2-entry-point"
"${HOME}/.local/share/Steam/steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point"
# < Add more entrypoints here if needed >
)
local custom_entrypoint="/etc/nestri/_v2-entry-point"
local temp_entrypoint="/tmp/_v2-entry-point.padded"
if [[ ! -f "$custom_entrypoint" ]]; then
log "Error: Custom _v2-entry-point not found at $custom_entrypoint"
exit 1
fi
log "Starting Steam _v2-entry-point patcher..."
(
while true; do
for i in "${!entrypoints[@]}"; do
local steam_entrypoint="${entrypoints[$i]}"
if [[ -f "$steam_entrypoint" ]]; then
# Get original file size
local original_size
original_size=$(stat -c %s "$steam_entrypoint" 2>/dev/null)
if [[ -z "$original_size" ]] || [[ "$original_size" -eq 0 ]]; then
log "Warning: Could not determine size of $steam_entrypoint, retrying..."
continue
fi
# Copy custom entrypoint to temp location
cp "$custom_entrypoint" "$temp_entrypoint" 2>/dev/null || {
log "Warning: Failed to copy custom entrypoint to $temp_entrypoint"
continue
}
# Pad the temporary file to match original size
if (( $(stat -c %s "$temp_entrypoint") < original_size )); then
truncate -s "$original_size" "$temp_entrypoint" 2>/dev/null || {
log "Warning: Failed to pad $temp_entrypoint to $original_size bytes"
continue
}
fi
# Copy padded file to Steam's entrypoint, if contents differ
if ! cmp -s "$temp_entrypoint" "$steam_entrypoint"; then
cp "$temp_entrypoint" "$steam_entrypoint" 2>/dev/null || {
log "Warning: Failed to patch $steam_entrypoint"
}
fi
fi
done
# Sleep for 1s
sleep 1
done
) &
PATCHER_PID=$!
log "Steam _v2-entry-point patcher started (PID: $PATCHER_PID)"
}
# Starts nestri-server
start_nestri_server() {
kill_if_running "${NESTRI_PID:-}" "nestri-server"
@@ -71,33 +134,93 @@ start_nestri_server() {
done
log "Error: Wayland display 'wayland-1' not available."
# Workaround for gstreamer being bit slow at times
log "Clearing gstreamer cache.."
rm -rf "${HOME}/.cache/gstreamer-1.0" 2>/dev/null || true
increment_retry "nestri-server"
restart_chain
}
# Starts compositor (gamescope) with Steam
# Starts compositor with optional application
start_compositor() {
kill_if_running "${COMPOSITOR_PID:-}" "compositor"
kill_if_running "${APP_PID:-}" "application"
log "Starting compositor with Steam..."
rm -rf /tmp/.X11-unix && mkdir -p /tmp/.X11-unix && chown nestri:nestri /tmp/.X11-unix
WAYLAND_DISPLAY=wayland-1 gamescope --backend wayland -g -f -e --rt --mangoapp -W "${WIDTH}" -H "${HEIGHT}" -- steam-native -tenfoot -cef-force-gpu &
COMPOSITOR_PID=$!
# Set default values only if variables are unset (not empty)
if [[ -z "${NESTRI_LAUNCH_CMD+x}" ]]; then
NESTRI_LAUNCH_CMD="steam-native -tenfoot -cef-force-gpu"
fi
if [[ -z "${NESTRI_LAUNCH_COMPOSITOR+x}" ]]; then
NESTRI_LAUNCH_COMPOSITOR="gamescope --backend wayland --force-grab-cursor -g -f --rt --mangoapp -W ${WIDTH} -H ${HEIGHT} -r ${FRAMERATE:-60}"
fi
log "Waiting for compositor to initialize..."
COMPOSITOR_SOCKET="${XDG_RUNTIME_DIR}/gamescope-0"
for ((i=1; i<=15; i++)); do
if [[ -e "$COMPOSITOR_SOCKET" ]]; then
log "Compositor initialized, gamescope-0 ready."
sleep 2
return
# Start Steam patcher only if Steam command is present
if [[ -n "${NESTRI_LAUNCH_CMD}" ]] && [[ "$NESTRI_LAUNCH_CMD" == *"steam"* ]]; then
start_steam_namespaceless_patcher
fi
# Launch compositor if configured
if [[ -n "${NESTRI_LAUNCH_COMPOSITOR}" ]]; then
local compositor_cmd="$NESTRI_LAUNCH_COMPOSITOR"
local is_gamescope=false
# Check if this is a gamescope command
if [[ "$compositor_cmd" == *"gamescope"* ]]; then
is_gamescope=true
# Append application command for gamescope if needed
if [[ -n "$NESTRI_LAUNCH_CMD" ]] && [[ "$compositor_cmd" != *" -- "* ]]; then
# If steam in launch command, enable gamescope integration via -e
if [[ "$NESTRI_LAUNCH_CMD" == *"steam"* ]]; then
compositor_cmd+=" -e"
fi
compositor_cmd+=" -- $NESTRI_LAUNCH_CMD"
fi
fi
sleep 1
done
log "Error: Compositor did not initialize."
increment_retry "compositor"
start_compositor
log "Starting compositor: $compositor_cmd"
WAYLAND_DISPLAY=wayland-1 /bin/bash -c "$compositor_cmd" &
COMPOSITOR_PID=$!
# Wait for appropriate socket based on compositor type
if $is_gamescope; then
COMPOSITOR_SOCKET="${XDG_RUNTIME_DIR}/gamescope-0"
log "Waiting for gamescope socket..."
else
COMPOSITOR_SOCKET="${XDG_RUNTIME_DIR}/wayland-0"
log "Waiting for wayland-0 socket..."
fi
for ((i=1; i<=15; i++)); do
if [[ -e "$COMPOSITOR_SOCKET" ]]; then
log "Compositor socket ready ($COMPOSITOR_SOCKET)."
# Patch resolution with wlr-randr for non-gamescope compositors
if ! $is_gamescope; then
local OUTPUT_NAME
OUTPUT_NAME=$(WAYLAND_DISPLAY=wayland-0 wlr-randr --json | jq -r '.[] | select(.enabled == true) | .name' | head -n 1)
if [ -z "$OUTPUT_NAME" ]; then
log "Warning: No enabled outputs detected. Skipping wlr-randr resolution patch."
return
fi
WAYLAND_DISPLAY=wayland-0 wlr-randr --output "$OUTPUT_NAME" --custom-mode "$WIDTH"x"$HEIGHT"
log "Patched resolution with wlr-randr."
fi
return
fi
sleep 1
done
log "Warning: Compositor socket not found after 15 seconds ($COMPOSITOR_SOCKET)."
else
# Launch standalone application if no compositor
if [[ -n "${NESTRI_LAUNCH_CMD}" ]]; then
log "Starting application: $NESTRI_LAUNCH_CMD"
WAYLAND_DISPLAY=wayland-1 /bin/bash -c "$NESTRI_LAUNCH_CMD" &
APP_PID=$!
else
log "No compositor or application configured."
fi
fi
}
# Increments retry counter
@@ -113,21 +236,24 @@ increment_retry() {
# Restarts the chain
restart_chain() {
log "Restarting nestri-server and compositor..."
RETRY_COUNT=0
start_nestri_server
}
# Cleans up processes
cleanup() {
local exit_code=$?
log "Terminating processes..."
kill_if_running "${NESTRI_PID:-}" "nestri-server"
kill_if_running "${COMPOSITOR_PID:-}" "compositor"
exit 0
kill_if_running "${APP_PID:-}" "application"
kill_if_running "${PATCHER_PID:-}" "steam-patcher"
rm -f "/tmp/_v2-entry-point.padded" 2>/dev/null
exit $exit_code
}
# Monitor processes for unexpected exits
main_loop() {
trap cleanup SIGINT SIGTERM
trap cleanup SIGINT SIGTERM EXIT
while true; do
sleep 1
@@ -141,6 +267,16 @@ main_loop() {
log "compositor died."
increment_retry "compositor"
start_compositor
# Check application
elif [[ -n "${APP_PID:-}" ]] && ! kill -0 "${APP_PID}" 2>/dev/null; then
log "application died."
increment_retry "application"
start_compositor
# Check patcher
elif [[ -n "${PATCHER_PID:-}" ]] && ! kill -0 "${PATCHER_PID}" 2>/dev/null; then
log "steam-patcher died."
increment_retry "steam-patcher"
start_steam_namespaceless_patcher
fi
done
}

View File

@@ -1,5 +1,4 @@
#!/bin/bash
set -euo pipefail
export XDG_RUNTIME_DIR=/run/user/${UID}/
export XDG_SESSION_TYPE=x11
@@ -11,3 +10,7 @@ export PROTON_NO_FSYNC=1
# Sleeker Mangohud preset :)
export MANGOHUD_CONFIG=preset=2
# Make gstreamer GL elements work without display output (NVIDIA issue..)
export GST_GL_API=gles2
export GST_GL_WINDOW=surfaceless

View File

@@ -54,3 +54,6 @@ autorestart=false
autostart=true
startretries=0
priority=10
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true