-
Notifications
You must be signed in to change notification settings - Fork 37
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:
- How it works
- The script
- Configuration
- systemd setup
- Testing
- Troubleshooting
- Running multiple characters
The watchdog considers a character "running" only if both of these are true:
-
Session file present — Lich writes
<tmpdir>/simutronics/sessions/<CharacterName>.sessionwhile connected. On Linux this is typically/tmp/simutronics/sessions/. -
Matching process alive — A
rubyprocess exists whose command line referenceslichand--login <CharacterName>. If either signal is missing, the watchdog launches Lich with the configured arguments.
| 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.
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.shThe 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 |
Before scheduling, launch Lich the normal way for your character, then run:
ps -eo pid,args | grep -i lich | grep -v grepLook 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 intest_lich_processfrom--login[[:space:]=]+${escaped}to--login[[:space:]=]+"?${escaped}"?.
Connect normally once, then check where Lich actually writes:
ls -la /tmp/simutronics/sessions/ 2>/dev/nullIf 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 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.
~/.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.shImportant
%h expands to your home directory inside unit files. Use it instead of $HOME — environment-variable expansion doesn't happen in Environment= lines.
~/.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.targetTip
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.
systemctl --user daemon-reload
systemctl --user enable --now lich-watchdog.timerBy default, user systemd units only run while you're logged in. To make the watchdog survive logout/login cycles:
loginctl enable-linger $USERWithout this, the timer pauses when you log out.
# 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 50Before trusting the schedule, verify all four scenarios behave correctly. Run the script manually each time:
~/.local/bin/lich-watchdog.shThen 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.
Watchdog runs but Lich doesn't appear on screen
The launched ruby process can't reach your display. Two common causes:-
X11:
DISPLAYandXAUTHORITYaren't set correctly in the service unit. Runecho $DISPLAY $XAUTHORITYin a terminal on your live session and match those values in the unit'sEnvironment=lines. -
Wayland: add
Environment=WAYLAND_DISPLAY=wayland-0(confirm the actual socket name withls $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 grepand verify it matches the regex. - Confirm the session file:
ls -la /tmp/simutronics/sessions/(or whereverDir.tmpdirresolves for your distro). If different, setLICH_SESSION_DIRin 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 $USERVerify 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).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.shThen enable per-character:
systemctl --user enable --now lich-watchdog@CharA.timer
systemctl --user enable --now lich-watchdog@CharB.timerYou'll need a matching templated timer (lich-watchdog@.timer) that calls lich-watchdog@%i.service.