Skip to content

Lich Character Watchdog ‐ Linux

Ryan P. McKinnon edited this page May 20, 2026 · 6 revisions

Lich Character Watchdog (Linux)

A Bash-based watchdog that ensures a specific Lich character session stays running on Linux. Detects whether the character is currently active using two signals (session file + process check) and relaunches Lich if either is missing. Scheduled via a systemd user timer.

Note

Windows and macOS instructions live on separate wiki pages:

Table of Contents

How it works

The watchdog considers a character "running" only if both of these are true:

  1. Session file present — Lich writes <tmpdir>/simutronics/sessions/<CharacterName>.session while connected. On Linux this is typically /tmp/simutronics/sessions/.
  2. Matching process alive — A ruby process exists whose command line references lich and --login <CharacterName>. If either signal is missing, the watchdog launches Lich with the configured arguments.

Why both checks?

Scenario Session file Process Watchdog action
Normal running session Do nothing
Clean disconnect Relaunch
Lich crashed hard (stale session file) Relaunch
Lich starting up (process before session) Relaunch*

Note

The startup race window is small; with a 10-minute schedule it's very unlikely to trigger a duplicate launch. If you see duplicate launches in the log, that's the cause.

The script

Save this as ~/.local/bin/lich-watchdog.sh and make it executable (chmod +x).

#!/usr/bin/env bash
# lich-watchdog.sh
# Ensures a specific Lich character session is running. Launches if not.
# Considers the character "running" only if BOTH are true:
#   1. Lich's session file exists at <tmpdir>/simutronics/sessions/<Char>.session
#   2. A ruby process is alive whose command line references lich + --login <Char>
 
set -u
 
# ---- Config ----------------------------------------------------------------
CHARACTER_NAME="${CHARACTER_NAME:-Mycharname}"
RUBY_BIN="${RUBY_BIN:-/usr/bin/ruby}"
LICH_DIR="${LICH_DIR:-$HOME/lich5}"
LICH_SCRIPT="${LICH_SCRIPT:-lich.rbw}"
# Pass launch args as a single string; respects shell quoting.
LICH_ARGS="${LICH_ARGS:---login $CHARACTER_NAME --gemstone --stormfront}"
LOG_FILE="${LOG_FILE:-$HOME/.local/share/lich-watchdog/watchdog.log}"
 
# Session dir override. Default mirrors Ruby's Dir.tmpdir on Linux.
LICH_SESSION_DIR="${LICH_SESSION_DIR:-/tmp/simutronics/sessions}"
SESSION_FILE="$LICH_SESSION_DIR/${CHARACTER_NAME}.session"
# ---------------------------------------------------------------------------
 
log() {
  local stamp
  stamp="$(date '+%Y-%m-%d %H:%M:%S')"
  local dir
  dir="$(dirname -- "$LOG_FILE")"
  mkdir -p -- "$dir" 2>/dev/null || true
  printf '[%s] %s\n' "$stamp" "$*" >> "$LOG_FILE" 2>/dev/null || true
}
 
test_session_file() {
  [[ -f "$SESSION_FILE" ]]
}
 
test_lich_process() {
  # Match any ruby/rubyw process whose command line references lich AND
  # --login <Character>. Case-insensitive; tolerates "--login Name" or
  # "--login=Name". pgrep -f matches against the full command line.
  local pattern
  # Escape regex metacharacters in the character name.
  local escaped
  escaped="$(printf '%s' "$CHARACTER_NAME" | sed 's/[][\\.^$*+?(){}|/]/\\&/g')"
  pattern="lich.*--login[[:space:]=]+${escaped}([[:space:]]|$)"
  pgrep -fi -- "$pattern" >/dev/null 2>&1
}
 
main() {
  local session_ok=0 process_ok=0
  test_session_file && session_ok=1
  test_lich_process && process_ok=1
 
  if (( session_ok == 1 && process_ok == 1 )); then
    log "OK: $CHARACTER_NAME running (session file + process both present)."
    exit 0
  fi
 
  local reasons=()
  (( session_ok == 0 )) && reasons+=("no session file")
  (( process_ok == 0 )) && reasons+=("no matching ruby process")
  local IFS=', '
  log "Not running for $CHARACTER_NAME (${reasons[*]}). Launching..."
 
  if [[ ! -x "$RUBY_BIN" ]]; then
    log "ERROR: ruby not executable at $RUBY_BIN"; exit 2
  fi
  if [[ ! -f "$LICH_DIR/$LICH_SCRIPT" ]]; then
    log "ERROR: $LICH_SCRIPT not found in $LICH_DIR"; exit 2
  fi
 
  # Launch detached so the watchdog exit doesn't take Lich with it.
  # nohup + setsid keeps it alive across the systemd unit's lifetime.
  ( cd "$LICH_DIR" && \
    nohup setsid "$RUBY_BIN" "$LICH_SCRIPT" $LICH_ARGS \
      >/dev/null 2>&1 < /dev/null & )
 
  log "Launched: $RUBY_BIN $LICH_SCRIPT $LICH_ARGS (cwd=$LICH_DIR)"
  exit 0
}
 
main "$@"

Make it executable:

chmod +x ~/.local/bin/lich-watchdog.sh

Configuration

The script reads configuration from environment variables, with defaults. You can either edit the defaults at the top of the script, or set the vars in the systemd unit (recommended — keeps the script generic).

Variable Description Default
CHARACTER_NAME Character name as it appears in --login Mycharname
RUBY_BIN Full path to ruby executable /usr/bin/ruby
LICH_DIR Lich install directory $HOME/lich5
LICH_SCRIPT Lich entry script lich.rbw
LICH_ARGS Launch arguments (must match how you normally launch) --login $CHARACTER_NAME --gemstone --stormfront
LOG_FILE Where to write watchdog activity $HOME/.local/share/lich-watchdog/watchdog.log
LICH_SESSION_DIR Override the session-file directory /tmp/simutronics/sessions

Verifying the regex matches your real launch

Before scheduling, launch Lich the normal way for your character, then run:

ps -eo pid,args | grep -i lich | grep -v grep

Look at the actual command line. The regex lich.*--login[[:space:]=]+<name> expects something like:

  • ruby lich.rbw --login Mycharname --gemstone --stormfront
  • ruby lich.rbw --login=Mycharname ...
  • ruby lich.rbw --login "Mycharname" ... (quoted name won't match) If your real command quotes the name, change the pattern in test_lich_process from --login[[:space:]=]+${escaped} to --login[[:space:]=]+"?${escaped}"?.

Verifying the session file path

Connect normally once, then check where Lich actually writes:

ls -la /tmp/simutronics/sessions/ 2>/dev/null

If the file lives somewhere else (e.g., $XDG_RUNTIME_DIR, or a different TMPDIR), set LICH_SESSION_DIR in the systemd unit to match.

systemd setup

systemd user services don't require root and run under your normal login session — exactly what you want here, since the frontend needs an interactive display.

1. Create the service unit

~/.config/systemd/user/lich-watchdog.service:

[Unit]
Description=Lich character watchdog (Mycharname)
After=graphical-session.target
 
[Service]
Type=oneshot
# Edit these to match your setup
Environment=CHARACTER_NAME=Mycharname
Environment=RUBY_BIN=/usr/bin/ruby
Environment=LICH_DIR=%h/lich5
Environment=LICH_SCRIPT=lich.rbw
Environment="LICH_ARGS=--login Mycharname --gemstone --stormfront"
Environment=LICH_SESSION_DIR=/tmp/simutronics/sessions
# DISPLAY/XAUTHORITY needed for the frontend to draw on your X session.
# Wayland users: also set WAYLAND_DISPLAY=wayland-0 (or whatever wayland-info shows).
Environment=DISPLAY=:0
Environment=XAUTHORITY=%h/.Xauthority
ExecStart=%h/.local/bin/lich-watchdog.sh

Important

%h expands to your home directory inside unit files. Use it instead of $HOME — environment-variable expansion doesn't happen in Environment= lines.

2. Create the timer unit

~/.config/systemd/user/lich-watchdog.timer:

[Unit]
Description=Run Lich watchdog every 10 minutes
 
[Timer]
OnBootSec=2min
OnUnitActiveSec=10min
Unit=lich-watchdog.service
Persistent=true
 
[Install]
WantedBy=timers.target

Tip

OnBootSec=2min delays the first run for 2 minutes after boot to give your graphical session time to come up. Persistent=true ensures the timer catches up if the system was asleep when a scheduled run was missed.

3. Enable and start

systemctl --user daemon-reload
systemctl --user enable --now lich-watchdog.timer

4. Enable lingering (optional but recommended)

By default, user systemd units only run while you're logged in. To make the watchdog survive logout/login cycles:

loginctl enable-linger $USER

Without this, the timer pauses when you log out.

5. Verify

# Confirm the timer is active and see the next scheduled run
systemctl --user status lich-watchdog.timer
systemctl --user list-timers lich-watchdog.timer
 
# Trigger the service immediately to test
systemctl --user start lich-watchdog.service
 
# Inspect recent service logs
journalctl --user -u lich-watchdog.service -n 50

Testing

Before trusting the schedule, verify all four scenarios behave correctly. Run the script manually each time:

~/.local/bin/lich-watchdog.sh

Then check ~/.local/share/lich-watchdog/watchdog.log.

# Setup Expected log line Expected behavior
1 Lich already running for the character OK: ... both present No relaunch
2 Lich cleanly exited (no process, no session file) Not running (no session file, no matching ruby process) Relaunch
3 Kill the ruby process (kill <pid>, stale session file remains) Not running (no matching ruby process) Relaunch
4 Delete the session file manually but leave process running Not running (no session file) Relaunch*

Warning

Scenario 4 will cause a duplicate launch, which is the expected tradeoff for catching scenario 3.

Troubleshooting

Watchdog runs but Lich doesn't appear on screen The launched ruby process can't reach your display. Two common causes:
  • X11: DISPLAY and XAUTHORITY aren't set correctly in the service unit. Run echo $DISPLAY $XAUTHORITY in a terminal on your live session and match those values in the unit's Environment= lines.
  • Wayland: add Environment=WAYLAND_DISPLAY=wayland-0 (confirm the actual socket name with ls $XDG_RUNTIME_DIR/wayland-*). After editing the unit: systemctl --user daemon-reload && systemctl --user start lich-watchdog.service.
Watchdog always relaunches even though Lich is running Either the process regex isn't matching, or `LICH_SESSION_DIR` is pointing at the wrong path.
  • Confirm the process command line: ps -eo pid,args | grep -i lich | grep -v grep and verify it matches the regex.
  • Confirm the session file: ls -la /tmp/simutronics/sessions/ (or wherever Dir.tmpdir resolves for your distro). If different, set LICH_SESSION_DIR in the unit.
Timer fires but service immediately exits Check `journalctl --user -u lich-watchdog.service -n 100` for the actual error. Most common: `ExecStart` path is wrong, or the script isn't executable.
Watchdog stops running after I log out User systemd units only run while a session exists for the user, unless lingering is enabled:
loginctl enable-linger $USER

Verify with loginctl show-user $USER | grep Linger.

Log file isn't being created The script tries to create the log directory with `mkdir -p`. If it's still missing, the parent directory may not be writable. Set `LOG_FILE` to somewhere you know is writable (e.g., `/tmp/lich-watchdog.log` for testing).
Multiple Lich instances spawn Either (a) the regex isn't matching your real command line so it thinks nothing is running, or (b) you hit the startup race window where the process was alive but the session file wasn't written yet. The log tells you which: look for "no matching ruby process" (case a) vs "no session file" (case b).
## Running multiple characters

Two options:

Per-character unit files (simpler): Copy the service unit to lich-watchdog-charA.service, lich-watchdog-charB.service, etc. Edit CHARACTER_NAME and LICH_ARGS in each. Create a matching timer per service. Enable each timer separately.

Templated unit (cleaner if you have several): Rename the unit to lich-watchdog@.service and reference the instance name inside it:

[Service]
Type=oneshot
Environment=CHARACTER_NAME=%i
Environment="LICH_ARGS=--login %i --gemstone --stormfront"
# ... rest of unit
ExecStart=%h/.local/bin/lich-watchdog.sh

Then enable per-character:

systemctl --user enable --now lich-watchdog@CharA.timer
systemctl --user enable --now lich-watchdog@CharB.timer

You'll need a matching templated timer (lich-watchdog@.timer) that calls lich-watchdog@%i.service.

Clone this wiki locally