Files
sentiment-engine/pi_wake_agent.sh

369 lines
13 KiB
Bash
Raw Normal View History

#!/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
pi_wake_agent.sh — Reusable multi-agent wake-up timer with self-cron/daemon
USAGE:
pi_wake_agent.sh --install [OPTIONS] Install recurring cron timer
pi_wake_agent.sh --once [OPTIONS] One-shot wake (no cron)
pi_wake_agent.sh --daemon [OPTIONS] Run as long-lived daemon (no cron)
pi_wake_agent.sh --run [OPTIONS] Internal: run wake action (called by cron)
pi_wake_agent.sh --remove [OPTIONS] Remove cron timer
pi_wake_agent.sh --list List active cron timers
pi_wake_agent.sh --status Show status (cron + one-shot timers)
pi_wake_agent.sh --validate [OPTIONS] Validate sessions exist in zellij
OPTIONS:
--interval DURATION Interval (default: 1h). Formats: 30m, 1h, 90m, 2h, etc.
--session SESSION Zellij session name (can repeat or comma-separate)
--sessions "A,B,C" Comma-separated list of sessions
--msg "MESSAGE" Wake message (default: "Operator says CONTINUE. Pi here!")
--pane PANE Zjet pane ID to target (focused pane if unset), e.g. terminal_1
--debug Enable debug logging
--help Show this help
EXAMPLES:
# Install recurring 1-hour timer for one session
pi_wake_agent.sh --install --interval 1h --session cc_UV_dev0_Fb
# Install 30-minute timer for multiple sessions
pi_wake_agent.sh --install --interval 30m --sessions "cc_UV_dev0_Fb,cc_UV_dev1_48" --msg "Wake up!"
# One-shot wake in 2 hours (no cron)
pi_wake_agent.sh --once --interval 2h --session cc_UV_dev0_Fb --msg "Time's up!"
# Run as daemon (long-lived process, no cron)
pi_wake_agent.sh --daemon --interval 1h --session cc_UV_dev0_Fb
# Remove timer
pi_wake_agent.sh --remove --session cc_UV_dev0_Fb --interval 1h
# List all timers
pi_wake_agent.sh --list
# Show status
pi_wake_agent.sh --status
# Validate sessions
pi_wake_agent.sh --validate --session cc_UV_dev0_Fb
NOTES:
- Identifies as ${AGENT_NICK} on h5i bus
- Sends 5× ENTER keypresses (1s delay) for reliable submission
- Self-cron: installs/removes own cron entries
- Structured logging to ${LOG_FILE} (rotated at 10MB, 5 files)
- Daemon mode runs indefinitely until killed (SIGTERM/SIGINT handled)
- Cron comments format: ${CRON_COMMENT_PREFIX}:<sessions>:<interval>
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