Skip to content

Instantly share code, notes, and snippets.

@mmasko
Last active September 6, 2025 18:42
Show Gist options
  • Select an option

  • Save mmasko/e2ff9b4f1b55e4bcf12c74479abbfc57 to your computer and use it in GitHub Desktop.

Select an option

Save mmasko/e2ff9b4f1b55e4bcf12c74479abbfc57 to your computer and use it in GitHub Desktop.
check stale activity
#!/bin/bash
# Stale activity checker
# Runs (e.g. via cron every 5 min). Shuts down instance after 30 min of NO file changes under $MONITOR_DIR.
# Improvements:
# - Uses a state file to persist the last observed activity timestamp.
# - Handles empty directories without immediately triggering shutdown.
# - Works off seconds (no integer minute truncation surprises).
# - Prevents race conditions with a lock.
set -euo pipefail
usage() {
cat <<EOF
Usage: $0 [options]
--dryrun Do not stop instance; log intention only
--quiet Suppress stdout (override QUIET env var)
--threshold-minutes N Override inactivity threshold (default 30)
--threshold N Inactivity threshold in SECONDS (overrides minutes)
--threashold N (Alias / common misspelling) same as --threshold
--monitor-dir PATH Directory to monitor (default: \$HOME/projects)
-h, --help Show this help
Environment overrides: MONITOR_DIR, THRESHOLD_MINUTES, QUIET, DRY_RUN
EOF
}
# Parse arguments
ARGS=()
THRESHOLD_SECONDS_OVERRIDE=""
while [ $# -gt 0 ]; do
case "$1" in
--dryrun) DRY_RUN=1; shift ;;
--quiet) QUIET=1; shift ;;
--threshold-minutes) THRESHOLD_MINUTES="${2:-}"; shift 2 ;;
--threshold-minutes=*) THRESHOLD_MINUTES="${1#*=}"; shift ;;
--threshold) THRESHOLD_SECONDS_OVERRIDE="${2:-}"; shift 2 ;;
--threshold=*) THRESHOLD_SECONDS_OVERRIDE="${1#*=}"; shift ;;
--threashold) THRESHOLD_SECONDS_OVERRIDE="${2:-}"; shift 2 ;;
--threashold=*) THRESHOLD_SECONDS_OVERRIDE="${1#*=}"; shift ;;
--monitor-dir) MONITOR_DIR="${2:-}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) ARGS+=("$1"); shift ;;
esac
done
set -- "${ARGS[@]}"
MONITOR_DIR="${MONITOR_DIR:-$HOME/projects}"
LOG_FILE="${LOG_FILE:-$HOME/stale_connection_check.log}"
STATE_FILE="${STATE_FILE:-$HOME/.last_activity_ts}"
LOCK_FILE="${LOCK_FILE:-/tmp/stale_activity_check.lock}"
THRESHOLD_MINUTES=${THRESHOLD_MINUTES:-30}
DRY_RUN=${DRY_RUN:-0} # Set to 1 or use --dryrun to avoid actual shutdown (logs intention only)
# Determine final threshold seconds (explicit seconds override wins)
if [ -n "$THRESHOLD_SECONDS_OVERRIDE" ]; then
if ! [[ "$THRESHOLD_SECONDS_OVERRIDE" =~ ^[0-9]+$ ]]; then
echo "ERROR: --threshold value must be an integer number of seconds" >&2
exit 1
fi
THRESHOLD_SECONDS=$THRESHOLD_SECONDS_OVERRIDE
else
THRESHOLD_SECONDS=$(( THRESHOLD_MINUTES * 60 ))
fi
exec 9>"$LOCK_FILE" || exit 1
flock -n 9 || { echo "Another instance of the checker is running" >> "$LOG_FILE"; exit 0; }
NOW=$(date +%s)
# Discover most recent modification time of any regular file (fallback to directory mtime, then NOW).
LATEST_FILE_TS=$(find "$MONITOR_DIR" -type f -printf '%T@\n' 2>/dev/null | sort -n | tail -1 || true)
if [ -z "$LATEST_FILE_TS" ]; then
# No files; use directory mtime
if [ -d "$MONITOR_DIR" ]; then
LATEST_FILE_TS=$(stat -c %Y "$MONITOR_DIR" 2>/dev/null || echo "$NOW")
else
LATEST_FILE_TS=$NOW
fi
else
LATEST_FILE_TS=${LATEST_FILE_TS%.*}
fi
# Initialize state file if missing
if [ ! -f "$STATE_FILE" ]; then
echo "$LATEST_FILE_TS" > "$STATE_FILE"
fi
LAST_ACTIVITY_TS=$(cat "$STATE_FILE" 2>/dev/null || echo "$LATEST_FILE_TS")
# If we saw newer activity than stored, update the state
if [ "$LATEST_FILE_TS" -gt "$LAST_ACTIVITY_TS" ]; then
echo "$LATEST_FILE_TS" > "$STATE_FILE"
LAST_ACTIVITY_TS=$LATEST_FILE_TS
fi
INACTIVE_SECONDS=$(( NOW - LAST_ACTIVITY_TS ))
INACTIVE_MINUTES=$(( INACTIVE_SECONDS / 60 ))
# If QUIET=1 suppress stdout messages (useful for cron). Default: show summary when run interactively.
QUIET=${QUIET:-0}
{
echo "Checked: $(date -u)"
echo "Latest observed file mtime: $LATEST_FILE_TS"
echo "State last activity ts: $LAST_ACTIVITY_TS"
echo "Inactive (s): $INACTIVE_SECONDS"
echo "Inactive (m): $INACTIVE_MINUTES (threshold ${THRESHOLD_SECONDS}s)"
} >> "$LOG_FILE"
if [ "$INACTIVE_SECONDS" -ge "$THRESHOLD_SECONDS" ]; then
if [ "$DRY_RUN" = "1" ]; then
MSG="Inactivity >= ${THRESHOLD_SECONDS}s. DRY RUN: would stop instance."
echo "$MSG" | tee -a "$LOG_FILE"
else
MSG="Inactivity >= ${THRESHOLD_SECONDS}s. Initiating stop."
echo "$MSG" | tee -a "$LOG_FILE"
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
REGION=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/document | grep region | awk -F\" '{print $4}')
aws ec2 stop-instances --instance-ids "$INSTANCE_ID" --region "$REGION" || echo "WARNING: stop-instances call failed" >> "$LOG_FILE"
fi
else
MSG="Still active (inactive ${INACTIVE_SECONDS}s < ${THRESHOLD_SECONDS}s threshold)."
echo "Still active within threshold (inactive ${INACTIVE_SECONDS}s < ${THRESHOLD_SECONDS}s)." >> "$LOG_FILE"
fi
if [ "$QUIET" != "1" ]; then
echo "$MSG"
fi
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment