diff --git a/i38.sh b/i38.sh index 00566e3..7203a58 100755 --- a/i38.sh +++ b/i38.sh @@ -1149,6 +1149,10 @@ $(if [[ $sounds -eq 0 ]]; then echo "exec_always --no-startup-id ${i3Path}/scripts/sound.py" fi fi +# i3 watchdog - monitors i3 responsiveness and auto-recovers from freezes +if [[ $usingSway -ne 0 ]]; then + echo "exec_always --no-startup-id ${i3Path}/scripts/i3_watchdog.sh" +fi # xbrlapi is X11-only, skip on Sway/Wayland if [[ $brlapi -eq 0 ]] && [[ $usingSway -ne 0 ]]; then echo 'exec --no-startup-id xbrlapi --quiet' diff --git a/scripts/i3_watchdog.sh b/scripts/i3_watchdog.sh new file mode 100755 index 0000000..e5f94c4 --- /dev/null +++ b/scripts/i3_watchdog.sh @@ -0,0 +1,144 @@ +#!/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 + if DISPLAY=:0 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=$(DISPLAY=:0 xdotool getwindowfocus 2>&1) + log_message "Focus check result: $focusCheck" + + # Try to reset focus to i3 + log_message "Attempting to reset X focus..." + DISPLAY=:0 xdotool key --clearmodifiers Super_L 2>/dev/null + sleep 0.5 + + # Try to focus on i3's root window + i3RootWindow=$(DISPLAY=:0 xdotool search --class "i3" | head -1) + if [[ -n "$i3RootWindow" ]]; then + log_message "Focusing i3 root window: $i3RootWindow" + DISPLAY=:0 xdotool windowfocus "$i3RootWindow" 2>/dev/null + fi + + # Try i3-msg to focus something + log_message "Using i3-msg to focus workspace..." + DISPLAY=:0 i3-msg workspace number 1 >/dev/null 2>&1 + sleep 0.5 + DISPLAY=:0 i3-msg focus output primary >/dev/null 2>&1 + sleep 1 + + # Check if focus is actually fixed now + if DISPLAY=:0 timeout 1 xdotool getwindowfocus >/dev/null 2>&1; then + log_message "Focus recovery successful!" + else + log_message "Focus still broken - restarting i3..." + DISPLAY=:0 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 diff --git a/scripts/sound.py b/scripts/sound.py index 809803f..22e94e6 100755 --- a/scripts/sound.py +++ b/scripts/sound.py @@ -50,13 +50,20 @@ def on_new_window(self,i3): def on_close_window(self,i3): try: - windowName = getattr(i3.container, 'name', None) - windowClass = getattr(i3.container, 'window_class', None) + # Get container early - if this fails, bail immediately + container = getattr(i3, 'container', None) + if container is None: + return + + # Fast checks with immediate bailout on any issue + windowName = getattr(container, 'name', None) + windowClass = getattr(container, 'window_class', None) + # Skip sound only for notification daemon (it has its own sound) if windowName != 'xfce4-notifyd' and windowClass != 'xfce4-notifyd': play_sound_async('play -nqV0 synth .25 sin 880:440 sin 920:480 remix - norm -3 pitch -500') except Exception: - # Silently ignore errors to prevent blocking i3 + # Silently ignore any errors - better no sound than blocking i3 pass def on_mode(self,event):