- Add --timeout SECONDS option to pgrep search (default: 2) - Fix script to exclude its own PID from detection results - Add file existence check before reading /proc to avoid race condition errors - Initialize cmdline variable to prevent unbound variable errors Acceptance criteria PASS: - Script exists at scripts/check-orphaned-processes.sh ✓ - Is executable (chmod +x) ✓ - Searches for patterns: 'pdftract mcp', 'TH-0', 'TH_0' ✓ - Returns exit code 0 when no orphans, 1 when orphans detected ✓ - Outputs process details (PID, command, args) when found ✓ - Script is documented with usage comments at top ✓ - Accepts optional timeout parameter ✓
249 lines
7.4 KiB
Bash
Executable file
249 lines
7.4 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# check-orphaned-processes.sh - Verify no orphaned pdftract/TH-0 processes
|
|
#
|
|
# This script checks for orphaned processes that may have been left behind
|
|
# by test runs. Per CLAUDE.md test hygiene rules, no processes should remain
|
|
# after tests complete.
|
|
#
|
|
# Usage:
|
|
# ./scripts/check-orphaned-processes.sh [options]
|
|
#
|
|
# Options:
|
|
# --kill Kill any orphaned processes found
|
|
# --verbose Show detailed output
|
|
# --json Output in JSON format
|
|
# --pattern PATTERN Custom process pattern (default: 'pdftract mcp|TH_0|TH-0')
|
|
#
|
|
# Exit codes:
|
|
# 0 - No orphaned processes found
|
|
# 1 - Orphaned processes found (and not killed)
|
|
# 2 - Error occurred
|
|
#
|
|
# Examples:
|
|
# ./scripts/check-orphaned-processes.sh
|
|
# ./scripts/check-orphaned-processes.sh --kill
|
|
# ./scripts/check-orphaned-processes.sh --json | jq .
|
|
|
|
set -euo pipefail
|
|
|
|
# Default options
|
|
KILL=false
|
|
VERBOSE=false
|
|
JSON=false
|
|
PATTERN='pdftract mcp|TH_0|TH-0'
|
|
TIMEOUT=2
|
|
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--kill)
|
|
KILL=true
|
|
shift
|
|
;;
|
|
--verbose)
|
|
VERBOSE=true
|
|
shift
|
|
;;
|
|
--json)
|
|
JSON=true
|
|
shift
|
|
;;
|
|
--pattern)
|
|
PATTERN="$2"
|
|
shift 2
|
|
;;
|
|
--timeout)
|
|
TIMEOUT="$2"
|
|
shift 2
|
|
;;
|
|
--help)
|
|
echo "Usage: $0 [options]"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --kill Kill any orphaned processes found"
|
|
echo " --verbose Show detailed output"
|
|
echo " --json Output in JSON format"
|
|
echo " --pattern PATTERN Custom process pattern (default: 'pdftract mcp|TH_0|TH-0')"
|
|
echo " --timeout SECONDS Timeout for process search (default: 2)"
|
|
echo ""
|
|
echo "Exit codes:"
|
|
echo " 0 - No orphaned processes found"
|
|
echo " 1 - Orphaned processes found (and not killed)"
|
|
echo " 2 - Error occurred"
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Error: Unknown option: $1" >&2
|
|
echo "Run '$0 --help' for usage." >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Validate timeout is a positive integer
|
|
if ! [[ "$TIMEOUT" =~ ^[0-9]+$ ]]; then
|
|
echo "Error: --timeout must be a positive integer" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Function to find orphaned processes
|
|
find_orphaned_processes() {
|
|
local pattern="$1"
|
|
local current_pid=$$
|
|
# Get PIDs matching pattern, excluding current script
|
|
local pids
|
|
pids=$(timeout "$TIMEOUT" pgrep -f "$pattern" 2>/dev/null | grep -v "^$current_pid$" || true)
|
|
|
|
# If no PIDs found, return early
|
|
if [[ -z "$pids" ]]; then
|
|
return
|
|
fi
|
|
|
|
# Get full command line for each PID
|
|
while IFS= read -r pid; do
|
|
if [[ -n "$pid" ]]; then
|
|
# Get full command line (suppress errors for processes that exit)
|
|
local cmdline=""
|
|
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
|
if [[ -r "/proc/$pid/cmdline" ]]; then
|
|
cmdline=$(tr '\0' ' ' < "/proc/$pid/cmdline")
|
|
fi
|
|
else
|
|
cmdline=$(ps -p "$pid" -o command= 2>/dev/null) || true
|
|
fi
|
|
if [[ -n "$cmdline" ]]; then
|
|
echo "$pid $cmdline"
|
|
fi
|
|
fi
|
|
done <<< "$pids"
|
|
}
|
|
|
|
# Function to extract process details
|
|
extract_process_details() {
|
|
local output="$1"
|
|
local pids=()
|
|
local commands=()
|
|
|
|
while IFS= read -r line; do
|
|
if [[ -n "$line" ]]; then
|
|
local pid=$(echo "$line" | awk '{print $1}')
|
|
local cmdline=$(echo "$line" | cut -d' ' -f2-)
|
|
pids+=("$pid")
|
|
commands+=("$cmdline")
|
|
fi
|
|
done <<< "$output"
|
|
|
|
# Export as variables for JSON output
|
|
FOUND_PIDS=("${pids[@]}")
|
|
FOUND_COMMANDS=("${commands[@]}")
|
|
FOUND_COUNT=${#pids[@]}
|
|
}
|
|
|
|
# Main logic
|
|
main() {
|
|
local orphaned_output
|
|
orphaned_output=$(find_orphaned_processes "$PATTERN")
|
|
|
|
if [[ -z "$orphaned_output" ]]; then
|
|
if [[ "$JSON" == "true" ]]; then
|
|
echo '{"status": "clean", "orphaned_processes": [], "count": 0}'
|
|
else
|
|
if [[ "$VERBOSE" == "true" ]]; then
|
|
echo "✓ No orphaned processes found matching pattern: $PATTERN"
|
|
else
|
|
echo "clean"
|
|
fi
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# Found orphaned processes
|
|
extract_process_details "$orphaned_output"
|
|
|
|
if [[ "$JSON" == "true" ]]; then
|
|
# Build JSON output
|
|
local json_output='{"status": "orphaned", "orphaned_processes": ['
|
|
local first=true
|
|
for i in $(seq 0 $((FOUND_COUNT - 1))); do
|
|
if [[ "$first" == "true" ]]; then
|
|
first=false
|
|
else
|
|
json_output+=','
|
|
fi
|
|
json_output+="{\"pid\": \"${FOUND_PIDS[$i]}\", \"command\": \"$(echo "${FOUND_COMMANDS[$i]}" | sed 's/"/\\"/g')\"}"
|
|
done
|
|
json_output+='], "count": '"$FOUND_COUNT"
|
|
|
|
if [[ "$KILL" == "true" ]]; then
|
|
json_output+=', "action": "killed"'
|
|
else
|
|
json_output+=', "action": "detected"'
|
|
fi
|
|
json_output+='}'
|
|
|
|
echo "$json_output"
|
|
else
|
|
if [[ "$VERBOSE" == "true" ]]; then
|
|
echo "⚠ Found $FOUND_COUNT orphaned process(es) matching pattern: $PATTERN"
|
|
echo ""
|
|
for i in $(seq 0 $((FOUND_COUNT - 1))); do
|
|
echo " PID: ${FOUND_PIDS[$i]}"
|
|
echo " Command: ${FOUND_COMMANDS[$i]}"
|
|
echo ""
|
|
done
|
|
else
|
|
echo "orphaned ($FOUND_COUNT process(es))"
|
|
fi
|
|
fi
|
|
|
|
# Kill processes if requested
|
|
if [[ "$KILL" == "true" ]]; then
|
|
if [[ "$VERBOSE" == "true" && "$JSON" == "false" ]]; then
|
|
echo "Killing orphaned processes..."
|
|
fi
|
|
|
|
for pid in "${FOUND_PIDS[@]}"; do
|
|
if kill "$pid" 2>/dev/null; then
|
|
if [[ "$VERBOSE" == "true" && "$JSON" == "false" ]]; then
|
|
echo " ✓ Killed PID $pid"
|
|
fi
|
|
else
|
|
if [[ "$VERBOSE" == "true" && "$JSON" == "false" ]]; then
|
|
echo " ✗ Failed to kill PID $pid"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Wait briefly and verify cleanup
|
|
sleep 1
|
|
orphaned_output=$(find_orphaned_processes "$PATTERN")
|
|
if [[ -z "$orphaned_output" ]]; then
|
|
if [[ "$VERBOSE" == "true" && "$JSON" == "false" ]]; then
|
|
echo "✓ All orphaned processes cleaned up successfully"
|
|
fi
|
|
if [[ "$JSON" == "true" ]]; then
|
|
echo '{"status": "cleaned", "orphaned_processes": [], "count": 0}'
|
|
exit 0
|
|
else
|
|
echo "cleaned"
|
|
exit 0
|
|
fi
|
|
else
|
|
if [[ "$VERBOSE" == "true" && "$JSON" == "false" ]]; then
|
|
echo "⚠ Warning: Some processes could not be killed"
|
|
fi
|
|
if [[ "$JSON" == "true" ]]; then
|
|
echo '{"status": "partial_cleanup", "message": "Some processes could not be killed"}'
|
|
exit 1
|
|
else
|
|
echo "partial_cleanup"
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Exit with error if we found orphans and didn't kill them
|
|
exit 1
|
|
}
|
|
|
|
main
|