#!/usr/bin/env bash # This file is part of I38. # I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, # either version 3 of the License, or (at your option) any later version. # I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along with I38. If not, see . # 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" logFile="/tmp/i38-i3-watchdog-${UID}.log" maxLogBytes=262144 displayFailureLimit=3 rotate_log() { local logSize [[ -f "$logFile" ]] || return 0 logSize=$(wc -c < "$logFile") if ((logSize >= maxLogBytes)); then mv -f -- "$logFile" "${logFile}.old" fi } log_message() { local message message="[i3-watchdog] $* - $(date '+%Y-%m-%d %H:%M:%S')" rotate_log printf '%s\n' "$message" printf '%s\n' "$message" >> "$logFile" } check_health() { ipcWorks=0 focusWorks=0 if timeout "$timeoutSeconds" i3-msg -t get_version > /dev/null 2>&1; then ipcWorks=1 fi if timeout 1 xdotool getwindowfocus > /dev/null 2>&1; then focusWorks=1 fi } check_x_display() { timeout 1 xdotool getmouselocation > /dev/null 2>&1 } cleanup_pid_file() { local recordedPid [[ -r "$pidFile" ]] || return 0 read -r recordedPid < "$pidFile" if [[ "$recordedPid" == "$$" ]]; then rm -f -- "$pidFile" fi } # 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" trap cleanup_pid_file EXIT log_message "I38 Watchdog started (PID: $$)" lastSuccessTime=$(date +%s) consecutiveFailures=0 consecutiveDisplayFailures=0 while true; do sleep "$checkInterval" # An i3 restart leaves X available and launches a replacement watchdog via # exec_always. Only exit when the X session itself is no longer available. if check_x_display; then consecutiveDisplayFailures=0 else ((consecutiveDisplayFailures++)) if [[ $consecutiveDisplayFailures -ge $displayFailureLimit ]]; then log_message "X display unavailable for $consecutiveDisplayFailures consecutive checks - exiting watchdog" exit 0 fi continue fi # Try to communicate with i3 IPC AND check X focus check_health # 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=$(timeout "$timeoutSeconds" 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=$(timeout 1 xdotool getwindowfocus 2>&1) log_message "Focus check result: $focusCheck" # Try to reset focus to i3 log_message "Attempting to reset X focus..." timeout 1 xdotool key --clearmodifiers Super_L 2>/dev/null sleep 0.5 # Try to focus on i3's root window i3RootWindow=$(timeout 1 xdotool search --class "i3" 2>/dev/null | head -1) if [[ -n "$i3RootWindow" ]]; then log_message "Focusing i3 root window: $i3RootWindow" timeout 1 xdotool windowfocus "$i3RootWindow" 2>/dev/null fi # Try i3-msg to focus something log_message "Using i3-msg to focus workspace..." timeout "$timeoutSeconds" i3-msg workspace number 1 >/dev/null 2>&1 sleep 0.5 timeout "$timeoutSeconds" i3-msg focus output primary >/dev/null 2>&1 sleep 1 # Recovery is successful only if both i3 IPC and X focus work. check_health if [[ $ipcWorks -eq 1 ]] && [[ $focusWorks -eq 1 ]]; then log_message "Recovery successful" lastSuccessTime=$(date +%s) consecutiveFailures=0 else log_message "Recovery incomplete - IPC:$ipcWorks Focus:$focusWorks; restarting i3" if timeout "$timeoutSeconds" i3-msg -t run_command restart >/dev/null 2>&1; then log_message "Restart command accepted" else log_message "Restart command failed or timed out" fi # Wait for restart to complete, then verify it actually recovered. sleep 5 check_health if [[ $ipcWorks -eq 1 ]] && [[ $focusWorks -eq 1 ]]; then log_message "i3 healthy after restart" lastSuccessTime=$(date +%s) consecutiveFailures=0 else log_message "i3 still unhealthy after restart - IPC:$ipcWorks Focus:$focusWorks" fi fi fi fi done