Last active
September 6, 2025 18:42
-
-
Save mmasko/e2ff9b4f1b55e4bcf12c74479abbfc57 to your computer and use it in GitHub Desktop.
check stale activity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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