#!/usr/bin/env bash # # pane-health.sh - Check health status of a tmux pane # # PURPOSE: # Verify pane and session state before operations to prevent "pane not found" # errors and detect failures early. Essential for reliable tmux automation. # # HOW IT WORKS: # 1. Check if tmux server is running on the specified socket # 2. Verify session exists using tmux has-session # 3. Check if pane exists and get its state (dead/alive, PID) # 4. Validate process is running via ps command # 5. Determine overall health status and return structured output # # USE CASES: # - Before sending commands: verify pane is ready # - After errors: determine if pane crashed # - Periodic health checks during long operations # - Cleanup decision: which panes to kill vs keep # - Integration with other tools (safe-send.sh, etc.) # # EXAMPLES: # # Check pane health in JSON format # ./pane-health.sh -S /tmp/my.sock -t session:0.0 # # # Check pane health in text format # ./pane-health.sh -t myapp:0.0 --format text # # # Use in conditional logic # if ./pane-health.sh -t session:0.0 --format text; then # echo "Pane is healthy" # else # echo "Pane has issues (exit code: $?)" # fi # # EXIT CODES: # 0 - Healthy (pane alive, process running) # 1 - Dead (pane marked as dead) # 2 - Missing (pane/session doesn't exist) # 3 - Zombie (process exited but pane still exists) # 4 - Server not running # # DEPENDENCIES: # - bash (with [[, printf, functions) # - tmux (for has-session, list-panes) # - ps (for process state validation) # # Bash strict mode: # -e: Exit immediately if any command fails # -u: Treat unset variables as errors # -o pipefail: Pipe fails if any command in pipeline fails set -euo pipefail # Get script directory to source registry library SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=lib/registry.sh source "$SCRIPT_DIR/lib/registry.sh" usage() { cat <<'USAGE' Usage: pane-health.sh -t target [options] OR: pane-health.sh -s session [options] OR: pane-health.sh [options] # auto-detect single session Check health status of a tmux pane and report structured results. Target Selection (priority order): -s, --session session name (looks up socket/target in registry) -t, --target tmux target (session:window.pane), explicit (no flags) auto-detect if only one session in registry Options: -S, --socket tmux socket path (for custom sockets via -S) --format output format: json|text (default: json) -h, --help show this help Exit Codes: 0 - Healthy (pane alive, process running) 1 - Dead (pane marked as dead) 2 - Missing (pane/session doesn't exist) 3 - Zombie (process exited but pane still exists) 4 - Server not running Output Formats: json - Structured JSON with all health information text - Human-readable status message Examples: # Using session name ./pane-health.sh -s my-python # Auto-detect single session ./pane-health.sh --format text # Explicit socket/target (backward compatible) ./pane-health.sh -t session:0.0 -S /tmp/claude.sock # Use in script with session registry if ./pane-health.sh -s my-session; then echo "Pane is ready for commands" fi USAGE } # ============================================================================ # Default Configuration # ============================================================================ # Required parameters (must be provided by user) target="" # tmux target pane (format: session:window.pane) # Optional parameters session_name="" # session name for registry lookup socket="" # tmux socket path (empty = use default tmux socket) output_format="json" # output format: json or text # ============================================================================ # Parse Command-Line Arguments # ============================================================================ while [[ $# -gt 0 ]]; do case "$1" in -s|--session) session_name="${2-}"; shift 2 ;; # Set session name for registry lookup -t|--target) target="${2-}"; shift 2 ;; # Set target pane -S|--socket) socket="${2-}"; shift 2 ;; # Set custom socket path --format) output_format="${2-}"; shift 2 ;; # Set output format -h|--help) usage; exit 0 ;; # Show help and exit *) echo "Unknown option: $1" >&2; usage; exit 1 ;; # Error on unknown option esac done # ============================================================================ # Session Resolution # ============================================================================ # Resolve session name to socket/target if provided # Priority: 1) Explicit -S/-t, 2) Session name -s, 3) Auto-detect single session if [[ -n "$socket" && -n "$target" ]]; then # Priority 1: Explicit socket and target provided (backward compatible) : # Use as-is, no resolution needed elif [[ -n "$session_name" ]]; then # Priority 2: Session name provided, look up in registry if ! session_data=$(registry_get_session "$session_name" 2>/dev/null); then echo "Error: Session '$session_name' not found in registry" >&2 echo "Use 'list-sessions.sh' to see available sessions" >&2 exit 1 fi # Extract socket and target from session data socket=$(echo "$session_data" | jq -r '.socket') target=$(echo "$session_data" | jq -r '.target') # Update activity timestamp registry_update_activity "$session_name" 2>/dev/null || true elif [[ -z "$socket" && -z "$target" ]]; then # Priority 3: No explicit params, try auto-detect single session session_count=$(registry_list_sessions 2>/dev/null | jq '.sessions | length' 2>/dev/null || echo "0") if [[ "$session_count" == "1" ]]; then # Single session exists, auto-use it auto_session_name=$(registry_list_sessions | jq -r '.sessions | keys[0]') session_data=$(registry_get_session "$auto_session_name") socket=$(echo "$session_data" | jq -r '.socket') target=$(echo "$session_data" | jq -r '.target') # Update activity timestamp registry_update_activity "$auto_session_name" 2>/dev/null || true elif [[ "$session_count" == "0" ]]; then echo "Error: No sessions found in registry" >&2 echo "Create a session with 'create-session.sh' or specify -t and -S explicitly" >&2 exit 1 else echo "Error: Multiple sessions found ($session_count total)" >&2 echo "Please specify session name with -s or use -t/-S explicitly" >&2 echo "Use 'list-sessions.sh' to see available sessions" >&2 exit 1 fi fi # ============================================================================ # Validate Required Parameters and Dependencies # ============================================================================ # Check that required parameters were provided (after resolution) if [[ -z "$target" ]]; then echo "target is required" >&2 usage exit 1 fi # Validate output format if [[ "$output_format" != "json" && "$output_format" != "text" ]]; then echo "format must be 'json' or 'text'" >&2 exit 1 fi # Check that tmux is installed and available in PATH if ! command -v tmux >/dev/null 2>&1; then echo "tmux not found in PATH" >&2 exit 1 fi # Check that ps is installed and available in PATH if ! command -v ps >/dev/null 2>&1; then echo "ps not found in PATH" >&2 exit 1 fi # ============================================================================ # Build tmux Command Array # ============================================================================ # Build base tmux command with optional socket parameter # ${socket:+-S "$socket"} expands to "-S $socket" if socket is set, empty otherwise tmux_cmd=(tmux ${socket:+-S "$socket"}) # ============================================================================ # Initialize Health State Variables # ============================================================================ # Health check results (will be populated during checks) server_running=false # Is tmux server running on the socket? session_exists=false # Does the target session exist? pane_exists=false # Does the target pane exist? pane_dead=false # Is the pane marked as dead by tmux? pid="" # Process ID running in the pane process_running=false # Is the process actually running? status="unknown" # Overall health status exit_code=0 # Script exit code # ============================================================================ # Function: output_result # ============================================================================ # Output health check results in requested format (JSON or text) # # Arguments: None (uses global variables) # Returns: None (outputs to stdout) # output_result() { if [[ "$output_format" == "json" ]]; then # -------------------------------------------------------------------------- # JSON Output Format # -------------------------------------------------------------------------- # Structured output with all health information # Boolean values: "true" or "false" (JSON strings) # PID: numeric value or null if not available # cat </dev/null 2>&1; then server_running=true else # Server is not running - this is the most fundamental failure status="server_not_running" exit_code=4 output_result exit "$exit_code" fi # ============================================================================ # Health Check: Step 2 - Extract session name from target # ============================================================================ # Target format: session:window.pane or session:window or just session # Extract session name (everything before first colon, or entire string if no colon) # ${target%%:*} means: remove longest match of ":*" from the end session_name="${target%%:*}" # ============================================================================ # Health Check: Step 3 - Check if session exists # ============================================================================ # Use tmux has-session to check if session exists # -t: target session name # Exit code 0 = session exists, non-zero = session doesn't exist if "${tmux_cmd[@]}" has-session -t "$session_name" 2>/dev/null; then session_exists=true else # Session doesn't exist - can't check pane without session status="missing" exit_code=2 output_result exit "$exit_code" fi # ============================================================================ # Health Check: Step 4 - Check if pane exists and get pane state # ============================================================================ # Query tmux for pane information # list-panes -F: Format output with specific variables # #{pane_dead}: 1 if pane is dead, 0 if alive # #{pane_pid}: Process ID running in the pane # -t: target (can be session, session:window, or session:window.pane) # 2>/dev/null: Suppress errors if pane doesn't exist # # Note: If target is session:0.0 but pane doesn't exist, list-panes returns empty # If target is just session, it lists all panes in session # if ! pane_info="$("${tmux_cmd[@]}" list-panes -F '#{pane_dead} #{pane_pid}' -t "$target" 2>/dev/null)"; then # list-panes failed - pane doesn't exist status="missing" exit_code=2 output_result exit "$exit_code" fi # Check if we got any output (pane exists) if [[ -z "$pane_info" ]]; then # No output means pane doesn't exist status="missing" exit_code=2 output_result exit "$exit_code" fi # Pane exists - mark it and parse the info pane_exists=true # -------------------------------------------------------------------------- # Parse pane information # -------------------------------------------------------------------------- # pane_info format: "0 12345" or "1 12345" # First field: pane_dead flag (0 = alive, 1 = dead) # Second field: pane_pid # # Read into variables using read command # IFS=' ': Use space as field separator # read -r: Don't interpret backslashes read -r pane_dead_flag pid_value <<< "$pane_info" # Set pane_dead boolean based on flag if [[ "$pane_dead_flag" == "1" ]]; then pane_dead=true status="dead" exit_code=1 output_result exit "$exit_code" fi # Store PID value pid="$pid_value" # ============================================================================ # Health Check: Step 5 - Validate process is running # ============================================================================ # Use ps to check if process with this PID is actually running # -p: Specify process ID to check # -o pid=: Output only PID (with no header) # 2>/dev/null: Suppress errors if PID doesn't exist # grep -q: Quiet mode, just check if pattern matches (exit code 0 = match) # if ps -p "$pid" -o pid= >/dev/null 2>&1; then # Process is running - pane is healthy! process_running=true status="healthy" exit_code=0 else # Process is not running but pane still exists - this is a zombie # This can happen when a process exits but tmux keeps the pane open # (depending on remain-on-exit setting) process_running=false status="zombie" exit_code=3 fi # ============================================================================ # Output Results and Exit # ============================================================================ output_result exit "$exit_code"