Skip to main content

tmux autostart (boot + Display)

Diese Anleitung richtet pro Benutzer einen systemd-Userdienst ein, der:

  • beim Boot sofort tm0 startet (ohne DISPLAY),
  • anschließend in einer Schleife auf die Anzeige wartet (Xauth-Cookie), dann DISPLAY/XAUTH/XDG_RUNTIME_DIR in den tmux-Server injiziert,
  • und danach die in ~/.config/tmux/autostart.conf definierten display|…-Sessions/Fenster vorbereitet/ausführt.

Der Orchestrator startet Kommandos in der bestehenden zsh, sodass Ctrl+C nur das Programm beendet. Ein cd … im Command wirkt persistent.


0) Voraussetzungen

sudo apt update
sudo apt install -y tmux zsh xauth x11-xserver-utils
# (optional) setxkbmap ist in x11-xserver-utils enthalten

Falls der Benutzer nicht jj heißt, ersetze ihn in den folgenden Befehlen entsprechend. Die Inhalte der Dateien bleiben gleich.


1) Benutzer jj einrichten

1.1 Linger aktivieren (User-Manager auch ohne Login)

sudo loginctl enable-linger jj

1.2 Verzeichnisse anlegen

sudo -u jj mkdir -p /home/jj/.config/systemd/user /home/jj/.config/tmux

1.3 Service-Datei (User-Unit)

/home/jj/.config/systemd/user/tmux-autostart.service

[Unit]
Description=Start/refresh tmux for jj (boot + wait for display, fixed socket)

[Service]
Type=simple
# Delay, bis /run/user/<uid> steht
ExecStartPre=/bin/sleep 10
# Sockets mit sicheren Rechten vorbereiten
ExecStartPre=/usr/bin/install -d -m 0700 -o %u -g %u %t
ExecStartPre=/usr/bin/install -d -m 0700 -o %u -g %u %t/tmux-%u
# alten Default-Socket ggf. entfernen (sauberer Start)
ExecStartPre=/usr/bin/env bash -c 'test -S "%t/tmux-%u/default" && rm -f "%t/tmux-%u/default" || true'

Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Environment=TMUX_TMPDIR=%t
Environment=TMUX=
UMask=0077
KillMode=process

ExecStart=%h/.config/tmux/orchestrator.sh

[Install]
WantedBy=default.target

1.4 Orchestrator-Skript (fixer Socket, persistentes cd, :0 nutzen)

/home/jj/.config/tmux/orchestrator.sh

#!/usr/bin/env bash
# ~/.config/tmux/orchestrator.sh (jj) – fixed socket /run/user/<uid>/tmux-<uid>/default
set -euo pipefail
unset TMUX

CONF="$HOME/.config/tmux/autostart.conf"
TMUX_BIN="$(command -v tmux)"
ZSH_BIN="$(command -v zsh || echo /usr/bin/zsh)"
UIDNUM="$(id -u)"
RUNDIR="${XDG_RUNTIME_DIR:-/run/user/${UIDNUM}}"
SOCKDIR="$RUNDIR/tmux-${UIDNUM}"
SOCK="$SOCKDIR/default"

umask 077
install -d -m 0700 -o "$UIDNUM" -g "$UIDNUM" "$RUNDIR"  >/dev/null 2>&1 || true
install -d -m 0700 -o "$UIDNUM" -g "$UIDNUM" "$SOCKDIR" >/dev/null 2>&1 || true

export TMUX_TMPDIR="$RUNDIR"
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
TMUXS=(-S "$SOCK")

log() { printf '[tmux-auto] %s\n' "$*" >&2; }
command -v "$TMUX_BIN" >/dev/null 2>&1 || { log "missing tmux"; exit 127; }

ensure_session() {
  local s="$1"
  if ! "$TMUX_BIN" "${TMUXS[@]}" has-session -t "$s" 2>/dev/null; then
    "$TMUX_BIN" "${TMUXS[@]}" new-session -d -s "$s" "$ZSH_BIN -l"
    sleep 0.1
    "$TMUX_BIN" "${TMUXS[@]}" send-keys -t "$s" "cd $HOME" C-m
    log "created session $s"
  fi
}

ensure_window() {
  local s="$1" w="$2"
  if ! "$TMUX_BIN" "${TMUXS[@]}" list-windows -t "$s" -F '#W' 2>/dev/null | grep -Fxq -- "$w"; then
    "$TMUX_BIN" "${TMUXS[@]}" new-window -t "$s" -n "$w" "$ZSH_BIN -l"
    sleep 0.05
    "$TMUX_BIN" "${TMUXS[@]}" send-keys -t "$s:$w" "cd $HOME" C-m
    log "created window $s:$w"
  fi
}

run_in_window_shell() {
  local tgt="$1" cmd="$2"
  if printf '%s' "$cmd" | grep -Eq '^[[:space:]]*cd[[:space:]]+'; then
    "$TMUX_BIN" "${TMUXS[@]}" send-keys -t "$tgt" "$cmd" C-m
  else
    "$TMUX_BIN" "${TMUXS[@]}" send-keys -t "$tgt" "bash -lc '$cmd'" C-m
  fi
  log "started $tgt -> $cmd"
}

harden_tmux_server() {
  "$TMUX_BIN" "${TMUXS[@]}" set -g remain-on-exit on >/dev/null
  "$TMUX_BIN" "${TMUXS[@]}" set -g exit-empty off >/dev/null
  "$TMUX_BIN" "${TMUXS[@]}" set -g detach-on-destroy off >/dev/null
}

run_phase_boot() {
  ensure_session tm0
  harden_tmux_server
  [[ -f "$CONF" ]] || return 0
  awk -F '|' '/^[[:space:]]*#/ || /^[[:space:]]*$/ {next} $1=="boot"{print}' "$CONF" |
  while IFS='|' read -r _p s w cmd; do
    s="${s:-tm0}"; ensure_session "$s"
    if [ -z "${cmd:-}" ]; then [ -n "${w:-}" ] && ensure_window "$s" "$w"; continue; fi
    if [ "$s" = "tm0" ]; then
      [ -n "${w:-}" ] && "$TMUX_BIN" "${TMUXS[@]}" rename-window -t tm0:0 "$w" 2>/dev/null || true
      run_in_window_shell "tm0:0" "$cmd"
    else
      if [ -n "${w:-}" ]; then
        ensure_window "$s" "$w"; run_in_window_shell "$s:$w" "$cmd"
      else
        run_in_window_shell "$s:0" "$cmd"  # :0 wiederverwenden
      fi
    fi
  done
}

wait_for_display_and_inject() {
  local xauth disp
  log "waiting for display (xauth cookie in $RUNDIR)…"
  while true; do
    xauth="$(ls -t "$RUNDIR"/xauth_* 2>/dev/null | head -n1 || true)"
    if [ -n "${xauth:-}" ] && [ -s "$xauth" ]; then
      disp="$(xauth -f "$xauth" list 2>/dev/null | awk '{ if (match($0,/unix:([0-9]+)/)) {n=substr($0,RSTART+5,RLENGTH-5); print ":" n; exit} if (match($0,/:([0-9]+)/)) {n=substr($0,RSTART+1,RLENGTH-1); print ":" n; exit} }')"
      if [ -n "${disp:-}" ]; then
        "$TMUX_BIN" "${TMUXS[@]}" set-environment -g XAUTHORITY "$xauth"
        "$TMUX_BIN" "${TMUXS[@]}" set-environment -g DISPLAY "$disp"
        "$TMUX_BIN" "${TMUXS[@]}" set-environment -g XDG_RUNTIME_DIR "$RUNDIR"
        export XAUTHORITY="$xauth" DISPLAY="$disp" XDG_RUNTIME_DIR="$RUNDIR"
        log "injected DISPLAY=$disp, XAUTHORITY=$xauth"
        return 0
      fi
    fi
    sleep 2
  done
}

set_xkb_for_xwayland() {
  command -v setxkbmap >/dev/null 2>&1 || return 0
  local cur_layout cur_variant
  cur_layout="$(setxkbmap -query 2>/dev/null | awk '/^layout:/ {print $2}')"
  cur_variant="$(setxkbmap -query 2>/dev/null | awk '/^variant:/ {print $2}')"
  if [ "$cur_layout" != "de" ] || [ "${cur_variant:-}" != "nodeadkeys" ]; then
    setxkbmap -layout de -variant nodeadkeys 2>/dev/null || true
    log "setxkbmap de nodeadkeys for Xwayland clients"
  fi
}

run_phase_display() {
  [[ -f "$CONF" ]] || return 0
  awk -F '|' '/^[[:space:]]*#/ || /^[[:space:]]*$/ {next} $1=="display"{print}' "$CONF" |
  while IFS='|' read -r _p s w cmd; do
    s="${s:-tm0}"; ensure_session "$s"
    if [ -z "${w:-}" ]; then w=""; fi
    if [ -z "${cmd:-}" ]; then
      if [ -n "$w" ]; then ensure_window "$s" "$w"; fi
      continue
    fi
    if [ -n "$w" ]; then
      ensure_window "$s" "$w"; run_in_window_shell "$s:$w" "$cmd"
    else
      run_in_window_shell "$s:0" "$cmd"
    fi
  done
}

log "start (boot phase)"; run_phase_boot
log "wait/display phase"; wait_for_display_and_inject
set_xkb_for_xwayland
run_phase_display
log "done"; exit 0

1.5 Beispiel-Config

/home/jj/.config/tmux/autostart.conf

# phase|session|window|command

# BOOT: sofort in tm0:0 ausführen
boot|tm0|AI|cd /home/jj/Test_Lab/Camera/; source /home/jj/Test_Lab/Camera/.venv/bin/activate && python3 /home/jj/Test_Lab/Camera/Version_0.8__capture.py

# DISPLAY: Sessions vorbereiten/Ordner setzen
display|tm12||cd /home/jj/OpenVPN/Schneider.land
display|tm5||

1.6 Rechte & Start

sudo -u jj chmod 0755 /home/jj/.config/tmux/orchestrator.sh
sudo -u jj systemctl --user daemon-reload
sudo -u jj systemctl --user enable --now tmux-autostart.service

1.7 Test

# direkt nach Aktivierung
sudo -u jj tmux -S /run/user/1000/tmux-1000/default ls
# nach Login (GUI): display-Phase sollte gelaufen sein
sudo -u jj tmux ls

2) Optional: root identisch als User-Unit

2.1 Linger & Verzeichnisse

sudo loginctl enable-linger root
sudo mkdir -p /root/.config/systemd/user /root/.config/tmux

2.2 Service-Datei

/root/.config/systemd/user/tmux-autostart.service

[Unit]
Description=Start/refresh tmux for root (boot + wait for display, fixed socket)

[Service]
Type=simple
ExecStartPre=/bin/sleep 10
ExecStartPre=/usr/bin/install -d -m 0700 -o root -g root /run/user/0
ExecStartPre=/usr/bin/install -d -m 0700 -o root -g root /run/user/0/tmux-0
ExecStartPre=/usr/bin/env bash -c 'test -S "/run/user/0/tmux-0/default" && rm -f "/run/user/0/tmux-0/default" || true'

Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Environment=TMUX_TMPDIR=%t
Environment=TMUX=
UMask=0077
KillMode=process

ExecStart=%h/.config/tmux/orchestrator.sh

[Install]
WantedBy=default.target

2.3 Orchestrator-Skript (root, fester Socket + Boot-:0-Fix)

/root/.config/tmux/orchestrator.sh

#!/usr/bin/env bash
# /root/.config/tmux/orchestrator.sh – fixed socket /run/user/0/tmux-0/default
set -euo pipefail
unset TMUX

CONF="/root/.config/tmux/autostart.conf"
TMUX_BIN="$(command -v tmux)"
ZSH_BIN="$(command -v zsh || echo /usr/bin/zsh)"
RUNDIR="/run/user/0"
SOCKDIR="$RUNDIR/tmux-0"
SOCK="$SOCKDIR/default"

umask 077
install -d -m 0700 -o root -g root "$RUNDIR"  >/dev/null 2>&1 || true
install -d -m 0700 -o root -g root "$SOCKDIR" >/dev/null 2>&1 || true

export TMUX_TMPDIR="$RUNDIR"
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
TMUXS=(-S "$SOCK")

log() { printf '[tmux-root] %s\n' "$*" >&2; }
command -v "$TMUX_BIN" >/dev/null 2>&1 || { log "missing tmux"; exit 127; }

ensure_session() {
  local s="$1"
  if ! "$TMUX_BIN" "${TMUXS[@]}" has-session -t "$s" 2>/dev/null; then
    "$TMUX_BIN" "${TMUXS[@]}" new-session -d -s "$s" "$ZSH_BIN -l"
    sleep 0.1
    "$TMUX_BIN" "${TMUXS[@]}" send-keys -t "$s" "cd /root" C-m
    log "created session $s"
  fi
}

ensure_window() {
  local s="$1" w="$2"
  if ! "$TMUX_BIN" "${TMUXS[@]}" list-windows -t "$s" -F '#W' 2>/dev/null | grep -Fxq -- "$w"; then
    "$TMUX_BIN" "${TMUXS[@]}" new-window -t "$s" -n "$w" "$ZSH_BIN -l"
    sleep 0.05
    "$TMUX_BIN" "${TMUXS[@]}" send-keys -t "$s:$w" "cd /root" C-m
    log "created window $s:$w"
  fi
}

run_in_window_shell() {
  local tgt="$1" cmd="$2"
  if printf '%s' "$cmd" | grep -Eq '^[[:space:]]*cd[[:space:]]+'; then
    "$TMUX_BIN" "${TMUXS[@]}" send-keys -t "$tgt" "$cmd" C-m
  else
    "$TMUX_BIN" "${TMUXS[@]}" send-keys -t "$tgt" "bash -lc '$cmd'" C-m
  fi
  log "started $tgt -> $cmd"
}

harden_tmux() {
  "$TMUX_BIN" "${TMUXS[@]}" set -g remain-on-exit on >/dev/null
  "$TMUX_BIN" "${TMUXS[@]}" set -g exit-empty off >/dev/null
  "$TMUX_BIN" "${TMUXS[@]}" set -g detach-on-destroy off >/dev/null
}

run_phase_boot() {
  ensure_session tm0
  harden_tmux
  [[ -f "$CONF" ]] || return 0
  awk -F '|' '/^[[:space:]]*#/ || /^[[:space:]]*$/ {next} $1=="boot"{print}' "$CONF" |
  while IFS='|' read -r _p s w cmd; do
    s="${s:-tm0}"; ensure_session "$s"
    if [ -z "${cmd:-}" ]; then [ -n "${w:-}" ] && ensure_window "$s" "$w"; continue; fi
    if [ "$s" = "tm0" ]; then
      [ -n "${w:-}" ] && "$TMUX_BIN" "${TMUXS[@]}" rename-window -t tm0:0 "$w" 2>/dev/null || true
      run_in_window_shell "tm0:0" "$cmd"
    else
      if [ -n "${w:-}" ]; then
        ensure_window "$s" "$w"; run_in_window_shell "$s:$w" "$cmd"
      else
        run_in_window_shell "$s:0" "$cmd"   # :0 wiederverwenden (kein "auto")
      fi
    fi
  done
}

find_latest_xauth() { ls -t /run/user/*/xauth_* 2>/dev/null | head -n1 || true; }

wait_for_display_and_inject() {
  local xauth disp cookie_dir
  log "waiting for display (any /run/user/*/xauth_*)…"
  while true; do
    xauth="$(find_latest_xauth)"
    if [ -n "${xauth:-}" ] && [ -s "$xauth" ]; then
      disp="$(xauth -f "$xauth" list 2>/dev/null | awk '{ if (match($0,/unix:([0-9]+)/)) {n=substr($0,RSTART+5,RLENGTH-5); print ":" n; exit} if (match($0,/:([0-9]+)/)) {n=substr($0,RSTART+1,RLENGTH-1); print ":" n; exit} }')"
      if [ -n "${disp:-}" ]; then
        cookie_dir="$(dirname "$xauth")"
        "$TMUX_BIN" "${TMUXS[@]}" set-environment -g XAUTHORITY "$xauth"
        "$TMUX_BIN" "${TMUXS[@]}" set-environment -g DISPLAY "$disp"
        "$TMUX_BIN" "${TMUXS[@]}" set-environment -g XDG_RUNTIME_DIR "$cookie_dir"
        export XAUTHORITY="$xauth" DISPLAY="$disp" XDG_RUNTIME_DIR="$cookie_dir"
        log "injected DISPLAY=$disp, XAUTHORITY=$xauth, XDG_RUNTIME_DIR=$cookie_dir"
        return 0
      fi
    fi
    sleep 2
  done
}

set_xkb_for_xwayland() {
  command -v setxkbmap >/dev/null 2>&1 || return 0
  local cur_layout cur_variant
  cur_layout="$(setxkbmap -query 2>/dev/null | awk '/^layout:/ {print $2}')"
  cur_variant="$(setxkbmap -query 2>/dev/null | awk '/^variant:/ {print $2}')"
  if [ "$cur_layout" != "de" ] || [ "${cur_variant:-}" != "nodeadkeys" ]; then
    setxkbmap -layout de -variant nodeadkeys 2>/dev/null || true
    log "setxkbmap de nodeadkeys"
  fi
}

run_phase_display() {
  [[ -f "$CONF" ]] || return 0
  awk -F '|' '/^[[:space:]]*#/ || /^[[:space:]]*$/ {next} $1=="display"{print}' "$CONF" |
  while IFS='|' read -r _p s w cmd; do
    s="${s:-tm0}"; ensure_session "$s"
    if [ -z "${w:-}" ]; then w=""; fi
    if [ -z "${cmd:-}" ]; then
      if [ -n "$w" ]; then ensure_window "$s" "$w"; fi
      continue
    fi
    if [ -n "$w" ]; then
      ensure_window "$s" "$w"; run_in_window_shell "$s:$w" "$cmd"
    else
      run_in_window_shell "$s:0" "$cmd"
    fi
  done
}

log "start (boot phase)"; run_phase_boot
log "wait/display phase"; wait_for_display_and_inject
set_xkb_for_xwayland
run_phase_display
log "done"; exit 0

2.4 Beispiel-Config (root)

/root/.config/tmux/autostart.conf

# phase|session|window|command
boot|tm11||cd /etc/JJ_SystemMetric/; source /etc/JJ_SystemMetric/.venv/bin/activate && python3 /etc/JJ_SystemMetric/Version_0.19__Start_SystemMetric.py
boot|tm12||cd /home/jj/OpenVPN/Schneider.land
boot|tm13||cd /home/jj/Cronjob/ntfy/; python3 /home/jj/Cronjob/ntfy/Version_0.13__ntfy.py

2.5 Rechte & Start

sudo chmod 0755 /root/.config/tmux/orchestrator.sh
sudo systemctl --user daemon-reload
sudo systemctl --user enable --now tmux-autostart.service
# prüfen:
sudo tmux -S /run/user/0/tmux-0/default ls

3) Nutzung & Beispiele

  • Nur Verzeichnis setzen: display|tm12||cd /pfad (Fenster :0 bleibt, kein zweites Fenster)
  • Fenster benennen: display|tm8|vpn|cd /home/jj/OpenVPN/Schneider.land
  • Direkt Programm starten: display|tm11|metric|cd /etc/JJ_SystemMetric/; source .venv/bin/activate && python3 run.py

4) Troubleshooting

  • jj: kein Socket: sudo -u jj tmux -S /run/user/1000/tmux-1000/default ls – sofern leer: journalctl --user -u tmux-autostart.service -n100 -o cat
  • root: unsafe permissions: wird durch die Unit via install -d -m 0700 behoben. Manuell: chmod 0700 /run/user/0 /run/user/0/tmux-0
  • Reset aller Sessions (jj): sudo -u jj tmux -S /run/user/1000/tmux-1000/default kill-server || true
  • Reset aller Sessions (root): sudo tmux -S /run/user/0/tmux-0/default kill-server || true

5) Sicherheitshinweis (root → User-Display)

Root nutzt das Xauth-Cookie der eingeloggten Nutzersitzung, um X/Wayland-Apps an diese Session anzubinden. Das ist üblich, aber beachte die Sicherheitsimplikationen (root erhält Display-Zugriff). Kein xhost + nötig.


6) Deinstallation

# jj
sudo -u jj systemctl --user disable --now tmux-autostart.service
sudo rm -f /home/jj/.config/systemd/user/tmux-autostart.service /home/jj/.config/tmux/orchestrator.sh
# root
sudo systemctl --user disable --now tmux-autostart.service
sudo rm -f /root/.config/systemd/user/tmux-autostart.service /root/.config/tmux/orchestrator.sh