#!/usr/bin/env bash # I38 i3 Watchdog - Monitors i3 responsiveness and auto-restarts on lockup # This script runs in the background and checks if i3 is responding to commands # Configuration checkInterval=3 # Check every 3 seconds timeoutSeconds=2 # Consider i3 locked if it doesn't respond within 2 seconds pidFile="${HOME}/.config/i3/i3_watchdog.pid" log_message() { echo "[i3-watchdog] $*" } # Kill any existing watchdog instances if [[ -f "$pidFile" ]]; then oldPid=$(cat "$pidFile") if kill -0 "$oldPid" 2>/dev/null; then log_message "Killing old watchdog instance (PID: $oldPid)" kill "$oldPid" 2>/dev/null fi fi # Write our PID echo "$$" > "$pidFile" log_message "I38 Watchdog started (PID: $$)" lastSuccessTime=$(date +%s) consecutiveFailures=0 while true; do sleep "$checkInterval" # Check if i3 is still running if ! pgrep -x i3 > /dev/null; then log_message "i3 process not found - exiting watchdog" rm -f "$pidFile" exit 0 fi # Try to communicate with i3 IPC AND check X focus ipcWorks=0 focusWorks=0 if timeout "$timeoutSeconds" i3-msg -t get_version > /dev/null 2>&1; then ipcWorks=1 fi # Also check if X focus is working (use current DISPLAY, not hardcoded) if timeout 1 xdotool getwindowfocus > /dev/null 2>&1; then focusWorks=1 fi # Both must work for system to be healthy if [[ $ipcWorks -eq 1 ]] && [[ $focusWorks -eq 1 ]]; then # Success - reset counters lastSuccessTime=$(date +%s) consecutiveFailures=0 else # Failed - increment failure counter ((consecutiveFailures++)) currentTime=$(date +%s) timeSinceSuccess=$((currentTime - lastSuccessTime)) log_message "Health check failed - IPC:$ipcWorks Focus:$focusWorks (failure #$consecutiveFailures, ${timeSinceSuccess}s since last success)" # If we've had 2+ consecutive failures, i3 is likely frozen if [[ $consecutiveFailures -ge 2 ]]; then log_message "CRITICAL: i3 IPC frozen for ${timeSinceSuccess}s - running diagnostics" # Check what processes exist i3Pid=$(pgrep -x i3) soundPid=$(pgrep -f "scripts/sound.py") log_message "Process check: i3=$i3Pid sound.py=$soundPid" # Check i3 process state if [[ -n "$i3Pid" ]]; then i3State=$(ps -o stat= -p "$i3Pid" 2>/dev/null) log_message "i3 process state: $i3State" fi # Check sound.py process state if [[ -n "$soundPid" ]]; then soundState=$(ps -o stat= -p "$soundPid" 2>/dev/null) log_message "sound.py process state: $soundState" # Check what sound.py is doing soundStack=$(cat "/proc/$soundPid/stack" 2>/dev/null | head -5) log_message "sound.py kernel stack: $soundStack" fi # Try to get i3 socket info i3Socket=$(i3 --get-socketpath 2>/dev/null) log_message "i3 socket path: $i3Socket" if [[ -n "$i3Socket" ]] && [[ -S "$i3Socket" ]]; then socketInfo=$(ls -l "$i3Socket" 2>/dev/null) log_message "Socket exists: $socketInfo" else log_message "Socket does not exist or is not a socket!" fi # Check if this is a focus issue (Wine keyboard grab bug) focusCheck=$(xdotool getwindowfocus 2>&1) log_message "Focus check result: $focusCheck" # Try to reset focus to i3 log_message "Attempting to reset X focus..." xdotool key --clearmodifiers Super_L 2>/dev/null sleep 0.5 # Try to focus on i3's root window i3RootWindow=$(xdotool search --class "i3" | head -1) if [[ -n "$i3RootWindow" ]]; then log_message "Focusing i3 root window: $i3RootWindow" xdotool windowfocus "$i3RootWindow" 2>/dev/null fi # Try i3-msg to focus something log_message "Using i3-msg to focus workspace..." i3-msg workspace number 1 >/dev/null 2>&1 sleep 0.5 i3-msg focus output primary >/dev/null 2>&1 sleep 1 # Check if focus is actually fixed now if timeout 1 xdotool getwindowfocus >/dev/null 2>&1; then log_message "Focus recovery successful!" else log_message "Focus still broken - restarting i3..." i3-msg -t run_command restart log_message "Restart command sent" fi # Wait for restart to complete sleep 5 # Reset counters lastSuccessTime=$(date +%s) consecutiveFailures=0 fi fi done