| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- #!/usr/bin/env bash
- # mac-ops :: panic-triage.sh
- # Decode the most recent kernel panic (or one specified by path/time).
- # Emits panic string, suspect kext, and the pre-panic timeline window.
- #
- # Usage:
- # scripts/panic-triage.sh # most recent panic
- # scripts/panic-triage.sh -f <path> # specific report file
- # scripts/panic-triage.sh -t '2026-05-14 03:14:22' # by timestamp (UTC)
- # scripts/panic-triage.sh -m 15 # widen pre-panic window to 15 min
- set -u
- PANIC_FILE=""
- PANIC_TIME=""
- WINDOW_MIN=10
- while [[ $# -gt 0 ]]; do
- case "$1" in
- -f|--file) PANIC_FILE="$2"; shift 2 ;;
- -t|--time) PANIC_TIME="$2"; shift 2 ;;
- -m|--minutes) WINDOW_MIN="$2"; shift 2 ;;
- --help|-h)
- cat <<EOF
- Usage: $0 [options]
- -f, --file PATH Specific .panic or Kernel*.ips file to decode
- -t, --time 'YYYY-MM-DD HH:MM:SS' Timestamp anchor for pre-panic window
- -m, --minutes N Pre-panic window in minutes (default: 10)
- --json, --redact, --quiet, --verbose Standard flags
- Exit codes:
- 0 success
- 3 no panic reports found
- EOF
- exit 0 ;;
- *) shift ;;
- esac
- done
- source "$(dirname "$0")/_lib/common.sh"
- parse_common_flags "$@"
- maybe_filter_self "$@"
- panic_dir="/Library/Logs/DiagnosticReports"
- # ----------------------------------------------------------------------------
- section "1. PANIC REPORT SELECTION"
- # ----------------------------------------------------------------------------
- if [[ -z "$PANIC_FILE" ]]; then
- # Find newest panic report
- PANIC_FILE=$(find "$panic_dir" -maxdepth 1 \( -name "*.panic" -o -name "Kernel*.ips" \) 2>/dev/null \
- | xargs ls -t 2>/dev/null | head -1)
- fi
- if [[ -z "$PANIC_FILE" ]] || [[ ! -f "$PANIC_FILE" ]]; then
- log_info "Panic reports" "none found in $panic_dir"
- emit_summary
- exit "$EXIT_NOT_FOUND"
- fi
- log_pass "Panic report selected" "$PANIC_FILE"
- panic_mtime=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$PANIC_FILE" 2>/dev/null)
- note " Last modified: $panic_mtime"
- # ----------------------------------------------------------------------------
- section "2. PANIC STRING + KEXT EXTRACTION"
- # ----------------------------------------------------------------------------
- if [[ "$PANIC_FILE" == *.ips ]]; then
- # .ips files are JSON-with-extras. The first line is a JSON header,
- # the rest of the file is structured but not strict JSON.
- panic_string=$(head -200 "$PANIC_FILE" | grep -m1 "panic(" | head -1)
- # Extract the bundleID of the panicking kext (best-effort)
- suspect_kext=$(grep -m1 -oE '"bundleID":"[^"]+"' "$PANIC_FILE" | head -1 | sed 's/.*"://; s/"//g')
- else
- panic_string=$(grep -m1 "^panic(" "$PANIC_FILE")
- # In old .panic format the "Kernel Extensions in backtrace" line lists suspects
- suspect_kext=$(awk '/Kernel Extensions in backtrace:/{getline; print; exit}' "$PANIC_FILE" | awk -F'[()]' '{print $2}')
- fi
- if [[ -n "$panic_string" ]]; then
- log_pass "Panic string extracted"
- note " $panic_string"
- fi
- if [[ -n "$suspect_kext" ]]; then
- case "$suspect_kext" in
- com.apple.*) log_warn "Suspect kext" "$suspect_kext (Apple — harder to fix; check macOS update)" ;;
- *) log_fail "Suspect kext" "$suspect_kext (third-party — primary suspect)" ;;
- esac
- else
- log_info "Suspect kext" "could not extract from report — check report manually"
- fi
- # Match panic string against the common-causes catalog
- note " Pattern match (quick lookup; see references/panic-codes.md for full catalog):"
- case "$panic_string" in
- *"Sleep wake failure"*)
- note " → Driver power-state bug. Often USB / Bluetooth / GPU. Check kext list around panic." ;;
- *"Unresponsive bootstrap subsystem"*)
- note " → launchd deadlock. Usually a third-party LaunchDaemon. Audit /Library/LaunchDaemons/." ;;
- *"WindowServer"*)
- note " → GPU driver / display kext fault. Try disabling external display, alternative GPU mode." ;;
- *"double_fault"*|*"page_fault"*)
- note " → Kernel-mode memory corruption. Bad RAM or buggy kext. Run memtest from recoveryOS." ;;
- *"panic_kthread"*)
- note " → Kernel watchdog timeout. A driver hung in infinite loop. Examine pre-panic kext activity." ;;
- *"Unable to find driver"*)
- note " → Boot-time kext failed to load. Often after macOS update. Try safe-boot." ;;
- *)
- note " → No quick-pattern match. See references/panic-codes.md." ;;
- esac
- # ----------------------------------------------------------------------------
- section "3. PRE-PANIC TIMELINE"
- # ----------------------------------------------------------------------------
- if [[ -z "$PANIC_TIME" ]]; then
- PANIC_TIME=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$PANIC_FILE" 2>/dev/null)
- fi
- note " Anchor: $PANIC_TIME (window: ${WINDOW_MIN} min before)"
- # Convert anchor to epoch, compute start
- if anchor_epoch=$(date -j -f "%Y-%m-%d %H:%M:%S" "$PANIC_TIME" "+%s" 2>/dev/null); then
- start_epoch=$((anchor_epoch - WINDOW_MIN * 60))
- start_str=$(date -r "$start_epoch" "+%Y-%m-%d %H:%M:%S")
- note " Searching unified log from $start_str to $PANIC_TIME ..."
- # Filter the noisy stuff out; surface kernel + kext + IO + power events
- log show --start "$start_str" --end "$PANIC_TIME" --style compact \
- --predicate '(subsystem == "com.apple.kernel" OR subsystem == "com.apple.iokit" OR processImagePath CONTAINS "kernel" OR senderImagePath CONTAINS ".kext") AND (messageType == "Default" OR messageType == "Error" OR messageType == "Fault")' \
- 2>/dev/null | tail -50 | sed 's/^/ /'
- log_info "Pre-panic events captured" "${WINDOW_MIN} min window"
- else
- log_warn "Pre-panic timeline" "could not parse panic timestamp; pass -t explicitly"
- fi
- # ----------------------------------------------------------------------------
- section "4. CONTEXT: RECENT PANICS"
- # ----------------------------------------------------------------------------
- recent_panics=$(find "$panic_dir" -maxdepth 1 \( -name "*.panic" -o -name "Kernel*.ips" \) \
- -mtime -30 2>/dev/null | wc -l | tr -d ' ')
- log_info "Panics in last 30 days" "$recent_panics"
- if [[ "$recent_panics" -gt 1 ]]; then
- note " Recent panic files:"
- find "$panic_dir" -maxdepth 1 \( -name "*.panic" -o -name "Kernel*.ips" \) -mtime -30 2>/dev/null \
- | xargs ls -lt 2>/dev/null | head -5 | awk '{print " "$NF" — "$6" "$7" "$8}'
- fi
- # ----------------------------------------------------------------------------
- emit_summary
|