#!/usr/bin/env bash # Cron entry point for ba_dailies.sh presets. Pure process-locking/logging # plumbing -- no game-automation logic, same "thin wrapper" spirit as # ba_dailies.sh itself (see CLAUDE.md's Bash policy). # # The shared flock lock exists because "daily" fires twice a day (3:30 and # 4:30) and "q4h" fires every 4 hours -- without a lock, two overlapping # invocations would both drive xdotool/scrot against the same game window # at once, which is unsafe. -n (non-blocking): if a previous run is still # in progress, this invocation is skipped entirely rather than queued, so # a scheduled fire time can never silently run late. # # Logs to ~/ba_logs/.log -- one file per preset, so "daily" and # "q4h" never interleave. Every line is one of four explicitly-tagged # outcomes (OK / FAILED / SKIPPED -- previous run still in progress / # SKIPPED -- paused) specifically so a real failure can be found with a # single command, e.g.: # grep -E 'FAILED|SKIPPED' ~/ba_logs/*.log # A bare exit code alone can't do this: flock and a genuinely crashed task # both exit 1, so lock acquisition is checked as its own explicit step # (via an fd-based flock, not the "flock -n LOCKFILE COMMAND" form used # originally) rather than folded into the wrapped command's own exit code. # Likewise, ba_daily.py's manual pause switch (config.PAUSE_FLAG_PATH, # toggled via `./ba_dailies.sh pause`/`resume`) exits with the distinct # PAUSE_EXIT_CODE (75) below rather than the generic failure code, so a # deliberate pause never gets logged as a crash. # # Optional Discord alerting (see plan.md Phase 26): if repo-root .env # (gitignored -- copy .env.example to create it) provides a working # ALERT_BRIDGE_URL/ALERT_BRIDGE_API_KEY pair, this posts a start alert and # an OK/FAILED finish alert to alert-bridge for every real run. SKIPPED # (lock contention or paused) never alerts -- both are benign/expected and # would just be noise. A missing, incomplete, or malformed .env silently # disables alerting only -- it can never affect the wrapped ba_dailies.sh # run or this script's own OK/FAILED/SKIPPED logging. # # Separately (see plan.md Phase 30): if the exit_game task ends the run # with AP still above config.EXIT_AP_ALERT_THRESHOLD, it prints a # distinctly-tagged "AP_ALERT:" line rather than posting to Discord itself # (Python has no .env/alert-bridge access by design -- see .env.example). # This script greps only the lines this run itself appended to LOG_FILE # (tracked via a before/after line count, not $(...) capture or `>()` # process substitution, so the log keeps streaming live and neither of # CLAUDE.md's Bash-safety triggers for those forms applies) and fires an # extra `error`-level alert (pings you) independent of the run's own # OK/FAILED outcome. set -uo pipefail # Resolves its own directory (same pattern ba_dailies.sh itself already # uses) rather than assuming a fixed ~/ba_dailies.sh copy -- the runtime # now runs directly out of this git checkout, so cron's own crontab entry # points straight at this file's real path (see CLAUDE.md's "Deployment # model") and this always calls the ba_dailies.sh sitting right next to it, # wherever the checkout lives. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PRESET="${1:?usage: ba_cron_run.sh }" LOG_DIR="$HOME/ba_logs" LOG_FILE="$LOG_DIR/$PRESET.log" LOCK_FILE="$LOG_DIR/ba_dailies.lock" mkdir -p "$LOG_DIR" # Load optional alert-bridge config. nounset is relaxed only around the # `source` call so a malformed/typo'd .env (e.g. referencing an unset var, # or a bad quote) can never abort this wrapper -- it just fails to enable # alerting, same as a missing file or an unedited "changeme" key. ALERT_ENABLED=0 ENV_FILE="$SCRIPT_DIR/.env" if [ -f "$ENV_FILE" ]; then set +u source "$ENV_FILE" 2>/dev/null env_source_status=$? set -u if [ "$env_source_status" -eq 0 ] \ && [ -n "${ALERT_BRIDGE_URL:-}" ] \ && [ -n "${ALERT_BRIDGE_API_KEY:-}" ] \ && [ "${ALERT_BRIDGE_API_KEY:-}" != "changeme" ]; then ALERT_ENABLED=1 fi fi alert() { local level="$1" message="$2" [ "$ALERT_ENABLED" -eq 1 ] || return 0 curl -sf --max-time 5 -X POST "$ALERT_BRIDGE_URL" \ -H "Authorization: Bearer $ALERT_BRIDGE_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"source\":\"ba-cron-$PRESET\",\"message\":\"$message\",\"level\":\"$level\"}" \ > /dev/null 2>&1 \ || echo "=== $(date -Iseconds) alert POST failed, continuing ===" } { exec 9>"$LOCK_FILE" if ! flock -n 9; then echo "=== $(date -Iseconds) SKIPPED $PRESET -- previous run still in progress ===" exit 0 fi echo "=== $(date -Iseconds) starting $PRESET ===" start_epoch=$(date +%s) alert info "Starting $PRESET" start_line=$(wc -l < "$LOG_FILE" 2>/dev/null) start_line="${start_line:-0}" "$SCRIPT_DIR/ba_dailies.sh" "$PRESET" status=$? elapsed=$(( $(date +%s) - start_epoch )) duration="$(( elapsed / 60 ))m $(( elapsed % 60 ))s" if [ "$status" -eq 0 ]; then echo "=== $(date -Iseconds) finished $PRESET OK (exit 0) ===" alert info "$PRESET finished OK in $duration" elif [ "$status" -eq 75 ]; then echo "=== $(date -Iseconds) SKIPPED $PRESET -- paused ===" else echo "=== $(date -Iseconds) finished $PRESET FAILED (exit $status) ===" alert error "$PRESET FAILED (exit $status) after $duration" fi ap_alert_line=$(tail -n +"$((start_line + 1))" "$LOG_FILE" 2>/dev/null | grep -m1 "AP_ALERT:") if [ -n "$ap_alert_line" ]; then alert error "$PRESET: $ap_alert_line" fi } >> "$LOG_FILE" 2>&1