#!/bin/bash
#
# Remove Passenger CRIU checkpoint images.
#
# Callers: %post on upgrade and %preun on erase (--all), and cron.daily
# (--older-than 30). See docs/CRIU.md, "Closing the image lifecycle".
#
# Runs unattended as root on a path read from a config file, so it only ever
# removes entries named exactly <uid>-<16 lowercase hex>: aimed at the wrong
# directory it can still only delete something shaped like our own data.
set -u

DEFAULT_ROOT=/var/cache/passenger-criu
APACHE_CONF_DIRS=(/etc/apache2 /etc/httpd)

usage() {
    cat >&2 <<EOF
Usage: ${0##*/} --all | --older-than DAYS [--root DIR] [--dry-run]

  --all              remove every image set
  --older-than DAYS  remove image sets not modified in DAYS days
  --root DIR         operate on DIR only, instead of discovering roots
  --dry-run          report what would be removed and remove nothing
EOF
    exit 2
}

# The compiled-in default plus every PassengerCriuDir. That directive is
# server-scope only, so a tenant cannot aim this from .htaccess.
discover_roots() {
    printf '%s\n' "$DEFAULT_ROOT"
    local d
    for d in "${APACHE_CONF_DIRS[@]}"; do
        [ -d "$d" ] || continue
        grep -rhsE '^[[:space:]]*PassengerCriuDir[[:space:]]+' "$d" 2>/dev/null \
            | sed -E 's/^[[:space:]]*PassengerCriuDir[[:space:]]+//; s/^"//; s/"[[:space:]]*$//; s/[[:space:]]+$//'
    done
}

MODE=""
DAYS=""
ROOT=""
DRY_RUN=0

while [ $# -gt 0 ]; do
    case "$1" in
        --all)         MODE=all ;;
        --older-than)  MODE=older; DAYS="${2:-}"; shift ;;
        --root)        ROOT="${2:-}"; shift ;;
        --dry-run)     DRY_RUN=1 ;;
        -h|--help)     usage ;;
        *)             echo "unknown argument: $1" >&2; usage ;;
    esac
    shift
done

[ -n "$MODE" ] || usage
if [ "$MODE" = older ]; then
    case "$DAYS" in
        ''|*[!0-9]*) echo "--older-than needs a number of days" >&2; usage ;;
    esac
fi

if [ -n "$ROOT" ]; then
    roots=$(printf '%s\n' "$ROOT")
else
    roots=$(discover_roots | awk 'NF' | sort -u)
fi

removed=0
for root in $roots; do
    # Must be a real, root-owned directory: not a symlink, not someone else's.
    [ -d "$root" ] || continue
    [ -L "$root" ] && continue
    owner=$(stat -c %u "$root" 2>/dev/null) || continue
    [ "$owner" = 0 ] || continue

    while IFS= read -r set; do
        [ -n "$set" ] || continue
        if [ "$DRY_RUN" = 1 ]; then
            echo "would remove $set"
        else
            rm -rf -- "$set" && removed=$((removed + 1))
        fi
    done < <(
        # Image sets are directories named <uid>-<16 hex>. The sidecars beside
        # them are files: <uid>-<16 hex>.unhealthy, which records that a restore
        # from that set produced a process that could not serve, and
        # <16 hex>.restarted, which records when the application last restarted.
        # Both have to go with the set. Left behind, they make rmdir of the root
        # fail on erase, and a verdict written before an upgrade stays in force
        # for the rest of its TTL and suppresses checkpointing on the new build.
        if [ "$MODE" = all ]; then
            find "$root" -mindepth 1 -maxdepth 1 \
                 \( -type d -regextype posix-extended \
                    -regex '.*/[0-9]+-[0-9a-f]{16}$' \
                 -o -type f -regextype posix-extended \
                    -regex '.*/([0-9]+-)?[0-9a-f]{16}\.(unhealthy|restarted)$' \
                 \) 2>/dev/null
        else
            find "$root" -mindepth 1 -maxdepth 1 \
                 \( -type d -regextype posix-extended \
                    -regex '.*/[0-9]+-[0-9a-f]{16}$' \
                 -o -type f -regextype posix-extended \
                    -regex '.*/([0-9]+-)?[0-9a-f]{16}\.(unhealthy|restarted)$' \
                 \) -mtime "+$DAYS" 2>/dev/null
        fi
    )
done

if [ "$DRY_RUN" = 0 ] && [ "$removed" -gt 0 ]; then
    logger -t passenger-criu-cleanup \
        "removed $removed checkpoint image set(s) ($MODE${DAYS:+ $DAYS days})" \
        2>/dev/null || true
fi
exit 0
