Files
I38/scripts/i3_watchdog.sh

210 lines
7.2 KiB
Bash
Executable File

#!/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 <https://www.gnu.org/licenses/>.
# 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