Opens a larger view. Escape closes it.

home-server

manager.sh

#!/bin/sh
apk add --no-cache openssh-client > /dev/null 2>&1

SSH="ssh -i /ssh/id_ed25519 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR user@127.0.0.1"

RUNNING=1

stop_all() {
  RUNNING=0
  echo 'Stopping Sunshine and SDDM/gamescope...'
  # Single SSH round-trip instead of two sequential ones -- Docker's stop
  # grace period only allows a fixed window (raised to 30s below, but this
  # halves the round-trips regardless) before SIGKILL, and a prior stop
  # left gamescope/SDDM running because the second command never got a
  # chance to run (2026-09-12 incident: Portainer stop killed this
  # container before "sudo systemctl stop sddm" ever executed).
  $SSH 'systemctl --user stop sunshine.service; sudo systemctl stop sddm' &
  wait $!
  exit 0
}
trap stop_all TERM INT

start_session() {
  echo 'Starting SDDM (autologin to gamescope-session)...'
  $SSH 'sudo systemctl start sddm'

  echo 'Waiting for gamescope to be ready on DP-1...'
  for i in $(seq 1 60); do
    $SSH 'pgrep -x gamescope-wl' > /dev/null 2>&1 && sleep 5 && break
    sleep 1
  done
  echo 'Gamescope ready'

  echo 'Starting Sunshine...'
  $SSH 'systemctl --user reset-failed sunshine.service 2>/dev/null; systemctl --user start sunshine.service'
  echo 'Sunshine started'

  # Give the session time to fully settle before health checks begin --
  # is-active reports "activating" for a bit right after start, and the
  # first poll must not mistake that for a failure.
  sleep 30
}

start_session

echo 'Streaming logs...'
tail -F /sunshine-config/sunshine.log &

# Health-check loop: gamescope or Sunshine can die independently of this
# container (a host reboot killing the session mid-stream is what motivated
# this fix -- see 2026-09-11 incident), and the old script just tailed a dead
# log file forever with no way to notice or recover. Poll every 30s and
# restart the whole session only on a definite failure state -- "activating"/
# "reloading"/"deactivating" are normal transients, not failures.
while [ "$RUNNING" = "1" ]; do
  sleep 30 &
  wait $!
  SUNSHINE_STATE=$($SSH 'systemctl --user is-active sunshine.service' 2>/dev/null)
  GAMESCOPE_ALIVE=$($SSH 'pgrep -x gamescope-wl' 2>/dev/null)

  BAD=0
  case "$SUNSHINE_STATE" in
    failed|inactive|"") BAD=1 ;;
  esac
  [ -z "$GAMESCOPE_ALIVE" ] && BAD=1

  if [ "$BAD" = "1" ] && [ "$RUNNING" = "1" ]; then
    echo "Health check failed (gamescope pid: '${GAMESCOPE_ALIVE:-none}', sunshine: '$SUNSHINE_STATE') -- restarting session..."
    $SSH 'sudo systemctl stop sddm' 2>/dev/null
    sleep 3
    start_session
  fi
done