Files
absolute-dotfiles/.local/bin/docker-nuke
T

209 lines
6.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# docker-nuke — capture diagnostics from a wedged Docker Desktop, then
# force-kill and restart it.
#
# Written for bash 3.2 compatibility (macOS /bin/bash).
set -euo pipefail
# --- configuration ----------------------------------------------------------
#
# The SC2034 directives below are per-line on purpose, not file-level: each
# constant is consumed by a later task, and its directive is deleted at that
# point. A file-level disable would silence SC2034 for the whole file forever,
# hiding a genuinely dead variable added later. By the final task none of these
# directives should remain.
HEALTH_TIMEOUT=6 # seconds to wait on `docker info` before calling it wedged
# shellcheck disable=SC2034
GRACEFUL_WAIT=8 # seconds to let Docker quit politely
# shellcheck disable=SC2034
TERM_WAIT=5 # seconds to wait after SIGTERM before SIGKILL
# shellcheck disable=SC2034
RESTART_TIMEOUT=90 # seconds to wait for the daemon to come back
# shellcheck disable=SC2034
POLL_INTERVAL=2 # seconds between daemon health polls
# shellcheck disable=SC2034
SAMPLE_DURATION=2 # seconds of stack sampling per process
# shellcheck disable=SC2034
SAMPLE_TIMEOUT=10 # hard cap on a single `sample` invocation
# shellcheck disable=SC2034
DUMP_RETENTION=10 # how many dump directories to keep
# shellcheck disable=SC2034
LOG_TAIL_LINES=2000 # lines of Docker log to copy (keeps dumps small)
DOCKER_APP="/Applications/Docker.app"
# shellcheck disable=SC2034
DOCKER_LOG_DIR="$HOME/Library/Containers/com.docker.docker/Data/log"
# shellcheck disable=SC2034
DOCKER_RAW="$HOME/Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw"
# shellcheck disable=SC2034
CRASH_DIR="$HOME/Library/Logs/DiagnosticReports"
# Overridable so tests never touch real state.
# shellcheck disable=SC2034
DUMP_ROOT="${DOCKER_NUKE_DUMP_ROOT:-$HOME/.local/state/docker-nuke}"
OPT_FORCE=0
OPT_NO_RESTART=0
OPT_DRY_RUN=0
OPT_DEEP=0
# --- helpers ----------------------------------------------------------------
bin_exists() { command -v "$1" >/dev/null 2>&1; }
# Run a command with a hard time limit. Returns 124 if it timed out.
# Call inside a conditional so `set -e` does not abort on a non-zero return.
run_timeout() {
local secs="$1"
shift
if bin_exists timeout; then
timeout "$secs" "$@"
return $?
fi
if bin_exists gtimeout; then
gtimeout "$secs" "$@"
return $?
fi
# Pure-bash fallback: run in background and poll.
"$@" &
local pid=$!
local waited=0
while kill -0 "$pid" 2>/dev/null; do
if [ "$waited" -ge "$secs" ]; then
kill -9 "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
return 124
fi
sleep 1
waited=$((waited + 1))
done
local rc=0
wait "$pid" || rc=$?
return "$rc"
}
# Exact process names to target, in addition to anything under Docker.app.
# NOTE: never use `pgrep -f docker` here — it matches this script's own
# command line and the tool would kill itself.
DOCKER_PROC_NAMES="com.docker.backend com.docker.build com.docker.krun \
com.docker.vpnkit com.docker.virtualization vpnkit docker"
docker_pids() {
local self="$$"
local parent="${PPID:-0}"
local name pid comm
{
# `pgrep -f "$DOCKER_APP"` matches any process whose *command line*
# mentions the bundle path — e.g. `tail -f /Applications/Docker.app/...`
# matches even though that process is plainly not Docker. Post-filter
# each candidate by its actual executable so only processes truly
# launched from the bundle survive. Wrapped in `|| true`: under
# `set -o pipefail`, an empty `pgrep -f` match (exit 1) would otherwise
# make this whole statement fail and abort the function under `set -e`.
pgrep -f "$DOCKER_APP" 2>/dev/null | while read -r pid; do
if [ -z "$pid" ]; then continue; fi
# A dead PID (process exited between pgrep and ps) or a `ps` failure
# must yield an empty comm, which matches no case branch below —
# never a crash, never a false positive.
# NOTE: `ps -o comm=` reports the executable path as invoked, not
# canonicalized — a Docker helper ever exec'd by relative path or bare
# name would report a non-bundle comm here and be silently dropped.
comm="$(ps -o comm= -p "$pid" 2>/dev/null)" || true
case "$comm" in
"$DOCKER_APP"/*) printf '%s\n' "$pid" ;;
esac
done || true
# Exact-name matches are already precise — do NOT apply the comm
# filter here. One job of this branch is catching a hung
# /usr/local/bin/docker CLI, which legitimately lives outside the
# bundle; filtering by $DOCKER_APP prefix would break that.
for name in $DOCKER_PROC_NAMES; do
pgrep -x "$name" 2>/dev/null || true
done
} | sort -un | while read -r pid; do
if [ -z "$pid" ]; then continue; fi
if [ "$pid" = "$self" ]; then continue; fi
if [ "$pid" = "$parent" ]; then continue; fi
printf '%s\n' "$pid"
done
}
# Returns 0 if the daemon answers within HEALTH_TIMEOUT, else non-zero
# (124 specifically means it hung rather than errored).
docker_healthy() {
local rc=0
run_timeout "$HEALTH_TIMEOUT" docker info >/dev/null 2>&1 || rc=$?
return "$rc"
}
log() { printf '%s\n' "$*"; }
warn() { printf '%s\n' "$*" >&2; }
usage() {
cat <<'EOF'
docker-nuke: force-restart a wedged Docker Desktop, capturing diagnostics first
Usage:
docker-nuke [options]
Options:
-f, --force Skip the confirmation shown when Docker looks healthy
-n, --no-restart Kill Docker but do not relaunch it
--dry-run Report what would be killed and dumped; change nothing
--deep Also stop root launchd helpers (com.docker.vmnetd); needs sudo
-h, --help Show this help
Diagnostics are written to ~/.local/state/docker-nuke/<timestamp>/ before
anything is killed. The most recent 10 dumps are kept.
Exit codes:
0 Docker killed and confirmed responsive again
1 Daemon did not come back within the restart timeout
2 Usage error, or aborted at the healthy-daemon prompt
EOF
}
parse_args() {
OPT_FORCE=0
OPT_NO_RESTART=0
OPT_DRY_RUN=0
OPT_DEEP=0
while [ "$#" -gt 0 ]; do
# shellcheck disable=SC2034 # OPT_* are read by main once it is wired up
case "$1" in
-f|--force) OPT_FORCE=1 ;;
-n|--no-restart) OPT_NO_RESTART=1 ;;
--dry-run) OPT_DRY_RUN=1 ;;
--deep) OPT_DEEP=1 ;;
-h|--help) usage; return 3 ;;
*)
warn "docker-nuke: unknown option '$1'"
usage >&2
return 2
;;
esac
shift
done
return 0
}
# --- entrypoint -------------------------------------------------------------
main() {
local rc=0
parse_args "$@" || rc=$?
if [ "$rc" -eq 3 ]; then return 0; fi
if [ "$rc" -ne 0 ]; then return "$rc"; fi
log "docker-nuke: not yet implemented"
return 0
}
# Only run when executed, not when sourced by the test harness.
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
main "$@"
fi