#!/usr/bin/env bash # pi_wake_agent.sh — Reusable multi-agent wake-up timer with self-cron/daemon set -euo pipefail # ─── Constants ──────────────────────────────────────────────────────────── SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")" LOG_FILE="/tmp/pi_wake_agent.log" LOG_MAX_SIZE=$((10 * 1024 * 1024)) LOG_MAX_FILES=5 CRON_COMMENT_PREFIX="pi_wake_agent" AGENT_NICK="pi_nvnemo" H5I_AGENT="pi_nvnemo" H5I_BUS_ROOT="/mnt/dolphinng5_predict" DEFAULT_INTERVAL="1h" PANE="" CRON_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" PYTHON_BIN="/usr/bin/python3" # ─── Logging ────────────────────────────────────────────────────────────── log_rotate() { [[ -f "$LOG_FILE" ]] || return 0 local size size=$(stat -c%s "$LOG_FILE" 2>/dev/null || echo 0) [[ $size -lt $LOG_MAX_SIZE ]] && return 0 for i in $(seq $((LOG_MAX_FILES - 1)) -1 1); do [[ -f "${LOG_FILE}.${i}" ]] && mv "${LOG_FILE}.${i}" "${LOG_FILE}.$((i + 1))" done mv "$LOG_FILE" "${LOG_FILE}.1" } log_write() { local level="$1"; shift local msg="$*" local ts ts=$(date '+%Y-%m-%d %H:%M:%S') log_rotate echo "[$ts] [$level] $msg" >> "$LOG_FILE" if [[ "$level" == "ERROR" || "$level" == "WARN" ]]; then echo "[$ts] [$level] $msg" >&2 fi return 0 } log_debug() { [[ ${DEBUG:-0} == "1" ]] && log_write "DEBUG" "$@"; } log_info() { log_write "INFO" "$@"; } log_warn() { log_write "WARN" "$@"; } log_error() { log_write "ERROR" "$@"; } # ─── Interval Parsing ──────────────────────────────────────────────────── parse_interval() { local interval="$1" local seconds=0 if [[ "$interval" =~ ^([0-9]+)h$ ]]; then seconds=$((BASH_REMATCH[1] * 3600)) elif [[ "$interval" =~ ^([0-9]+)m$ ]]; then seconds=$((BASH_REMATCH[1] * 60)) elif [[ "$interval" =~ ^([0-9]+)s$ ]]; then seconds=$((BASH_REMATCH[1])) else log_error "Invalid interval format '$interval'. Use like 1h, 30m, 90m, 2h" return 1 fi echo "$seconds" return 0 } interval_to_cron() { local interval="$1" if [[ "$interval" =~ ^([0-9]+)h$ ]]; then local hours="${BASH_REMATCH[1]}" echo "0 */${hours} * * *" elif [[ "$interval" =~ ^([0-9]+)m$ ]]; then local mins="${BASH_REMATCH[1]}" if (( mins >= 60 )); then log_error "For minutes >= 60, use hours (e.g., 1h not 60m)" return 1 fi echo "*/${mins} * * * *" else log_error "Cron only supports minutes/hours intervals" return 1 fi } interval_to_human() { local interval="$1" if [[ "$interval" =~ ^([0-9]+)h$ ]]; then echo "${BASH_REMATCH[1]} hour(s)" elif [[ "$interval" =~ ^([0-9]+)m$ ]]; then echo "${BASH_REMATCH[1]} minute(s)" elif [[ "$interval" =~ ^([0-9]+)s$ ]]; then echo "${BASH_REMATCH[1]} second(s)" else echo "$interval" fi } # ─── Session Parsing ───────────────────────────────────────────────────── parse_sessions() { local raw="${1:-}" [[ -z "$raw" ]] && return 0 IFS=',' read -ra SESSIONS <<< "$raw" for i in "${!SESSIONS[@]}"; do SESSIONS[i]=$(echo "${SESSIONS[i]}" | xargs) done } # ─── Cron Management ──────────────────────────────────────────────────── cron_comment() { local sessions_str sessions_str=$(IFS=,; echo "${SESSIONS[*]}") echo "${CRON_COMMENT_PREFIX}:${sessions_str}:${INTERVAL}" } install_cron() { local cron_sched cron_sched=$(interval_to_cron "$INTERVAL") || return 1 local comment comment=$(cron_comment) local pane_arg="" [[ -n "$PANE" ]] && pane_arg="--pane ${PANE}" local cmd="cd ${H5I_BUS_ROOT} && export PATH=${CRON_PATH} && export H5I_AGENT=${H5I_AGENT} && ${PYTHON_BIN} ${SCRIPT_PATH} --run --sessions \"${SESSIONS[*]}\" --msg \"${MESSAGE}\" ${pane_arg}" (crontab -l 2>/dev/null | grep -v "${comment}") | crontab - (crontab -l 2>/dev/null; echo "${cron_sched} ${cmd} # ${comment}") | crontab - log_info "Installed cron: ${cron_sched} -> ${SESSIONS[*]} every ${INTERVAL}" } remove_cron() { local comment comment=$(cron_comment) (crontab -l 2>/dev/null | grep -v "${comment}") | crontab - log_info "Removed cron for ${SESSIONS[*]} (${INTERVAL})" } list_cron() { echo "=== pi_wake_agent cron entries ===" crontab -l 2>/dev/null | grep "${CRON_COMMENT_PREFIX}" || echo "None found" } # ─── Zellij Operations ────────────────────────────────────────────────── zellij_session_exists() { local session="$1" zellij list-sessions 2>/dev/null | grep -q "^${session} " } zellij_write_chars() { local session="$1" local text="$2" zellij --session "$session" action write-chars ${PANE:+--pane-id "$PANE"} "$text" 2>/dev/null || true } zellij_write_enter() { local session="$1" zellij --session "$session" action write ${PANE:+--pane-id "$PANE"} 13 2>/dev/null || true } # ─── Wake Action ──────────────────────────────────────────────────────── run_wake() { local sessions_str sessions_str=$(IFS=,; echo "${SESSIONS[*]}") log_info "Waking ${sessions_str} with message: ${MESSAGE}" for session in "${SESSIONS[@]}"; do zellij_write_chars "$session" "[${AGENT_NICK} via zellij] ${MESSAGE} Run: h5i-bus msg inbox" for i in 1 2 3 4 5; do zellij_write_enter "$session" sleep 1 done done cd "${H5I_BUS_ROOT}" && export H5I_AGENT="${H5I_AGENT}" && h5i msg send Fable "${MESSAGE} (timer wakeup)" 2>/dev/null || true log_info "Wake sent to ${sessions_str}" } # ─── Modes ────────────────────────────────────────────────────────────── run_once() { local interval_seconds interval_seconds=$(parse_interval "$INTERVAL") || return 1 log_info "One-shot timer set for ${INTERVAL} ($(interval_to_human "$INTERVAL"))" ( sleep "$interval_seconds" run_wake ) & local pid=$! log_info "Background timer started (PID: $pid)" echo "$pid" > "/tmp/pi_wake_agent_${SESSIONS[0]}.pid" return 0 } run_daemon() { local interval_seconds interval_seconds=$(parse_interval "$INTERVAL") || return 1 log_info "=== DAEMON START ===" log_info "Interval: ${INTERVAL} ($(interval_to_human "$INTERVAL"))" log_info "Sessions: ${SESSIONS[*]}" log_info "Message: ${MESSAGE}" run_wake while true; do log_debug "Sleeping for ${interval_seconds}s..." sleep "$interval_seconds" run_wake done } status() { echo "=== pi_wake_agent Status ===" echo "Script: ${SCRIPT_PATH}" echo "Log: ${LOG_FILE}" echo "Log size: $(du -h "$LOG_FILE" 2>/dev/null | cut -f1 || echo 'N/A')" echo "Agent: ${AGENT_NICK}" echo "" list_cron echo "" echo "=== Active one-shot timers ===" local found=0 for pidfile in /tmp/pi_wake_agent_*.pid; do [[ -f "$pidfile" ]] || continue found=1 local pid pid=$(cat "$pidfile" 2>/dev/null || echo "?") if kill -0 "$pid" 2>/dev/null; then echo " PID $pid (active)" else echo " PID $pid (dead, cleaning up)" rm -f "$pidfile" fi done [[ $found -eq 0 ]] && echo " (none)" } validate_sessions() { log_info "Validating sessions: ${SESSIONS[*]}" for session in "${SESSIONS[@]}"; do if zellij_session_exists "$session"; then log_info " $session: EXISTS" else log_warn " $session: NOT FOUND (may be dead/EXITED)" fi done } # ─── Usage ────────────────────────────────────────────────────────────── usage() { cat <: USAGE } # ─── Main ─────────────────────────────────────────────────────────────── MODE="install" INTERVAL="${DEFAULT_INTERVAL}" SESSIONS=() MESSAGE="Operator says CONTINUE. ${AGENT_NICK} here, saying hi!" SESSIONS_RAW="" DEBUG="${DEBUG:-0}" # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in --install) MODE="install"; shift ;; --once) MODE="once"; shift ;; --daemon) MODE="daemon"; shift ;; --run) MODE="run"; shift ;; --remove) MODE="remove"; shift ;; --list) MODE="list"; shift ;; --status) MODE="status"; shift ;; --validate) MODE="validate"; shift ;; --interval) INTERVAL="$2"; shift 2 ;; --session) SESSIONS_RAW="${SESSIONS_RAW:+${SESSIONS_RAW},}$2"; shift 2 ;; --sessions) SESSIONS_RAW="$2"; shift 2 ;; --msg) MESSAGE="$2"; shift 2 ;; --pane) PANE="$2"; shift 2 ;; --debug) DEBUG=1; shift ;; --help) usage; exit 0 ;; *) log_error "Unknown option: $1"; usage; exit 1 ;; esac done # Parse sessions parse_sessions "${SESSIONS_RAW}" # Validate if [[ ${#SESSIONS[@]} -eq 0 && "$MODE" != "list" && "$MODE" != "status" ]]; then log_error "--session or --sessions required" usage exit 1 fi # Log startup log_info "=== pi_wake_agent ${MODE} ===" log_info "Interval: ${INTERVAL} ($(interval_to_human "$INTERVAL"))" log_info "Sessions: ${SESSIONS[*]}" [[ -n "$PANE" ]] && log_info "Pane target: ${PANE}" [[ -n "$MESSAGE" ]] && log_info "Message: ${MESSAGE}" # Execute mode case "$MODE" in install) install_cron ;; once) run_once ;; daemon) run_daemon ;; run) run_wake ;; remove) remove_cron ;; list) list_cron ;; status) status ;; validate) validate_sessions ;; *) log_error "Unknown mode: $MODE"; usage; exit 1 ;; esac