1 example

Zombie process

Completed process still listed as active.

[ FAQ1 ]

What is a zombie process?

A zombie process refers to a child process that has completed its execution but still has an entry in the system’s process table. This happens because the parent process has not performed the necessary system call (wait()) to retrieve the child's exit status. Zombies don't use system resources beyond their process table entry, but accumulating a large number of them can exhaust the system’s available process IDs. Essentially, zombie processes indicate that a program isn’t correctly managing its child processes, typically due to poor programming practices or unexpected termination of parent processes.
[ FAQ2 ]

How to kill a zombie process

Zombie processes cannot be killed using traditional signals (kill, kill -9) because they're already terminated. To remove them, the parent process must execute a wait() system call to read their exit status, thereby fully cleaning up the child’s entry in the process table. If the parent process itself has stopped functioning, killing or restarting the parent will allow the Linux init or systemd process (PID 1) to adopt the zombies and automatically clear them. Alternatively, rebooting the Linux system completely clears all zombie processes, though this method is less practical on production systems. Properly designing applications to handle child processes correctly using wait() is the most effective long-term solution.
diff block
+#!/bin/sh
+
+# manage-crontab.sh - Script to manage crontab entries for Raycast Alarms
+
+# Add common paths for Homebrew binaries
+export PATH="$PATH:/usr/local/bin:/opt/homebrew/bin"
+
+# Script directory (for finding resources)
+SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"
+BASE_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
+
+# Configuration directory
+CONFIG_DIR="$HOME/.raycast-alarms"
+ALARMS_FILE="$CONFIG_DIR/alarms.data"
+CRONTAB_MARKER="#--- RAYCAST ALARMS ---#"
+
+# Ensure config directory exists
+mkdir -p "$CONFIG_DIR/active"
+mkdir -p "$CONFIG_DIR/logs"
+
+# Log function
+log() {
+ LOG_FILE="$CONFIG_DIR/logs/crontab-$(date +%Y%m%d).log"
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
+}
+
+# Initialize alarms file if it doesn't exist
+if [ ! -f "$ALARMS_FILE" ]; then
+ touch "$ALARMS_FILE"
+fi
+
+# Function to add an alarm to crontab
+add_alarm() {
+ alarm_id="$1"
+ title="$2"
+ hours="$3"
+ minutes="$4"
+ seconds="$5"
+ sound_path="$6"
+
+ log "Adding alarm: ID=$alarm_id, Title=$title, Time=$hours:$minutes:$seconds, Sound=$sound_path"
+
+ # Validate parameters
+ if [ -z "$alarm_id" ] || [ -z "$title" ] || [ -z "$hours" ] || [ -z "$minutes" ] || [ -z "$seconds" ] || [ -z "$sound_path" ]; then
+ log "Error: Missing required parameters"
+ echo "Usage: $0 add alarm_id title hours minutes seconds sound_path"
+ exit 1
+ fi
+
+ # Validate time format
+ if ! [[ "$hours" =~ ^[0-9]+$ ]] || [ "$hours" -lt 0 ] || [ "$hours" -gt 23 ]; then
+ log "Error: Hours must be between 0-23"
+ echo "Error: Hours must be between 0-23"
+ exit 1
+ fi
+
+ if ! [[ "$minutes" =~ ^[0-9]+$ ]] || [ "$minutes" -lt 0 ] || [ "$minutes" -gt 59 ]; then
+ log "Error: Minutes must be between 0-59"
+ echo "Error: Minutes must be between 0-59"
+ exit 1
+ fi
+
+ if ! [[ "$seconds" =~ ^[0-9]+$ ]] || [ "$seconds" -lt 0 ] || [ "$seconds" -gt 59 ]; then
+ log "Error: Seconds must be between 0-59"
+ echo "Error: Seconds must be between 0-59"
+ exit 1
+ fi
+
+ # Check that trigger script exists
+ TRIGGER_SCRIPT="$HOME/.raycast-alarms/scripts/trigger-alarm.sh"
+ if [ ! -f "$TRIGGER_SCRIPT" ]; then
+ log "ERROR: Trigger script not found at: $TRIGGER_SCRIPT"
+ echo "ERROR: Trigger script not found at: $TRIGGER_SCRIPT"
+ # Try to find it elsewhere
+ log "Searching for trigger script in current directory..."
+ CURRENT_DIR="$(pwd)"
+ log "Current directory: $CURRENT_DIR"
+ FOUND_SCRIPT=$(find "$CURRENT_DIR" -name "trigger-alarm.sh" 2>/dev/null | head -n 1)
+ log "Found script (if any): $FOUND_SCRIPT"
+ else
+ log "Trigger script found at: $TRIGGER_SCRIPT"
+ fi
+
+ # Create the crontab entry
+ # Format: minute hour day month weekday command
+ # Escape spaces in paths with backslashes
+ escaped_sound_path=$(echo "$sound_path" | sed 's/ /\\ /g')
+ escaped_script_path=$(echo "$HOME/.raycast-alarms/scripts/trigger-alarm.sh" | sed 's/ /\\ /g')
+
+ # Create a crontab entry with proper parameter order, pass seconds as a parameter
+ cron_entry="$minutes $hours * * * $escaped_script_path $alarm_id \"$title\" $escaped_sound_path $seconds"
+
+ log "Generated crontab entry: $cron_entry"
+
+ # Get current crontab
+ crontab -l > "$CONFIG_DIR/temp_crontab" 2>/dev/null || echo "" > "$CONFIG_DIR/temp_crontab"
+ log "Retrieved current crontab content"
+
+ # Check if marker exists
+ if ! grep -q "$CRONTAB_MARKER" "$CONFIG_DIR/temp_crontab"; then
+ # Add marker section if it doesn't exist
+ echo "" >> "$CONFIG_DIR/temp_crontab"
+ echo "$CRONTAB_MARKER" >> "$CONFIG_DIR/temp_crontab"
+ echo "#--- DO NOT EDIT THIS SECTION MANUALLY ---#" >> "$CONFIG_DIR/temp_crontab"
+ echo "" >> "$CONFIG_DIR/temp_crontab"
+ log "Added marker section to crontab"
+ else
+ log "Marker already exists in crontab"
+ fi
+
+ # For debugging, show the marker section in the crontab
+ log "Current crontab marker section:"
+ grep -A 5 -B 5 "$CRONTAB_MARKER" "$CONFIG_DIR/temp_crontab" | while read line; do log "CRONTAB: $line"; done
+
+ # Add new entry
+ sed -i '' "/$CRONTAB_MARKER/a\\
+$cron_entry
+" "$CONFIG_DIR/temp_crontab"
+ log "Added entry to temp crontab file"
+
+ # For debugging, check if the entry was added correctly
+ log "Checking if entry was added correctly:"
+ grep "$alarm_id" "$CONFIG_DIR/temp_crontab" | while read line; do log "ENTRY: $line"; done
+
+ # Install updated crontab
+ crontab "$CONFIG_DIR/temp_crontab"
+ CRONTAB_RESULT=$?
+ log "Installed updated crontab, exit code: $CRONTAB_RESULT"
+
+ # Debug: Check crontab content after installation
+ log "Checking crontab content after installation:"
+ crontab -l | grep -A 5 -B 5 "$CRONTAB_MARKER" | while read line; do log "INSTALLED: $line"; done
+
+ rm -f "$CONFIG_DIR/temp_crontab"
+
+ log "Added alarm: $alarm_id at $hours:$minutes:$seconds - '$title'"
+
+ # Add to our tracking file using a simple pipe-delimited format
+ # Format: id|title|time|sound_path
+
+ # Format time with leading zeros
+ formatted_hours=$(printf "%02d" $hours)
+ formatted_minutes=$(printf "%02d" $minutes)
+ formatted_time="$formatted_hours:$formatted_minutes"
+
+ # Escape pipes in title and sound path
+ safe_title=$(echo "$title" | sed 's/|/_/g')
+ safe_sound_path=$(echo "$sound_path" | sed 's/|/_/g')
+
+ # Add to the data file
+ echo "$alarm_id|$safe_title|$formatted_time|$safe_sound_path" >> "$ALARMS_FILE"
+ DATA_RESULT=$?
+ log "Added alarm to data file, exit code: $DATA_RESULT"
+
+ if [ $DATA_RESULT -ne 0 ]; then
+ log "ERROR: Failed to add alarm to data file"
+ echo "ERROR: Failed to add alarm to data file"
+ fi
+
+ # Debug: Show the data file content
+ log "Data file content after update:"
+ cat "$ALARMS_FILE" | while read line; do log "DATA: $line"; done
+
+ echo "Alarm added successfully: $title at $hours:$minutes:$seconds"
+}
+
+# Function to remove an alarm from crontab
+remove_alarm() {
+ alarm_id="$1"
+
+ log "Removing alarm with ID: $alarm_id"
+
+ # Get current crontab
+ crontab -l > "$CONFIG_DIR/temp_crontab" 2>/dev/null || echo "" > "$CONFIG_DIR/temp_crontab"
+
+ # Debug: Check if the alarm exists in crontab before removal
+ if grep -q "$alarm_id" "$CONFIG_DIR/temp_crontab"; then
+ log "Found alarm $alarm_id in crontab, proceeding with removal"
+ else
+ log "Warning: Alarm $alarm_id not found in crontab"
+ fi
+
+ # Remove the specific alarm entry
+ # The alarm ID is always the first parameter to the trigger script
+ sed -i '' "/$alarm_id /d" "$CONFIG_DIR/temp_crontab"
+
+ # Debug: Verify the alarm is gone
+ if grep -q "$alarm_id" "$CONFIG_DIR/temp_crontab"; then
+ log "ERROR: Failed to remove alarm $alarm_id from crontab"
+ else
+ log "Successfully removed alarm $alarm_id from crontab"
+ fi
+
+ # Install updated crontab
+ crontab "$CONFIG_DIR/temp_crontab"
+ CRONTAB_RESULT=$?
+ log "Installed updated crontab, exit code: $CRONTAB_RESULT"
+
+ # Debug: Check crontab content after installation
+ log "Checking crontab content after removal:"
+ crontab -l | grep -A 5 -B 5 "$CRONTAB_MARKER" | while read line; do log "CRONTAB: $line"; done
+
+ rm -f "$CONFIG_DIR/temp_crontab"
+
+ # Remove from tracking file - simply grep for lines not containing the alarm ID
+ if [ -f "$ALARMS_FILE" ]; then
+ # Create a temp file with all lines except the one with alarm_id
+ grep -v "^$alarm_id|" "$ALARMS_FILE" > "$CONFIG_DIR/temp_alarms.data"
+ mv "$CONFIG_DIR/temp_alarms.data" "$ALARMS_FILE"
+
+ DATA_RESULT=$?
+ log "Removed alarm from data file, exit code: $DATA_RESULT"
+
+ if [ $DATA_RESULT -ne 0 ]; then
+ log "ERROR: Failed to remove alarm from data file"
+ echo "ERROR: Failed to remove alarm from data file"
+ fi
+
+ # Debug: Check data file content after removal
+ log "Data file content after removal:"
+ cat "$ALARMS_FILE" | while read line; do log "DATA: $line"; done
+ else
+ log "Warning: alarms data file not found, nothing to remove"
+ fi
+
+ echo "Alarm removed successfully: $alarm_id"
+}
+
+# Function to list all alarms
+list_alarms() {
+ if [ ! -f "$ALARMS_FILE" ] || [ ! -s "$ALARMS_FILE" ]; then
+ # Return empty JSON array since that's what the TypeScript code expects
+ echo "[]"
+ return 0
+ fi
+
+ # Convert our pipe-delimited format to JSON format
+ echo "["
+ first=true
+ while IFS="|" read -r id title time sound; do
+ if [ "$first" = true ]; then
+ first=false
+ else
+ echo ","
+ fi
+ echo " {\"id\": \"$id\", \"title\": \"$title\", \"time\": \"$time\", \"sound\": \"$sound\"}"
+ done < "$ALARMS_FILE"
+ echo "]"
+}
+
+# Function to get all active alarm IDs
+get_active_alarm_ids() {
+ ls -1 "$CONFIG_DIR/active/" 2>/dev/null | grep -v "_loop$" || echo ""
+}
+
+# Function to check if an alarm is active
+is_alarm_active() {
+ local alarm_id="$1"
+ [ -f "$CONFIG_DIR/active/$alarm_id" ]
+}
+
+# Function to kill an active alarm
+kill_alarm() {
+ local alarm_id="$1"
+ local active_file="$CONFIG_DIR/active/$alarm_id"
+ local control_file="$CONFIG_DIR/active/${alarm_id}_loop"
+
+ if [ -f "$active_file" ]; then
+ pid=$(cat "$active_file")
+ log "Killing alarm $alarm_id (PID: $pid)"
+
+ # Remove the loop control file to stop any sound loops
+ rm -f "$control_file"
+
+ # Kill the sound process
+ kill -TERM "$pid" 2>/dev/null || true
+
+ # Kill any afplay processes related to this alarm
+ pkill -f "afplay.*$alarm_id" 2>/dev/null || true
Greptile
greptile
style: Using TERM signal first, but no fallback to KILL if process persists. Could leave zombie processes.