feat: check subcommand for firing off expired alarms

This commit is contained in:
Lewis Wynne 2026-04-02 00:29:35 +01:00
parent 9de60f23cc
commit c6b2148eea
2 changed files with 164 additions and 12 deletions

85
nag
View file

@ -651,20 +651,20 @@ _next_for_rule() {
_time_of_day="$(date -d "@${_timestamp}" +%H:%M:%S)" _time_of_day="$(date -d "@${_timestamp}" +%H:%M:%S)"
case "${_rule}" in case "${_rule}" in
hourly) printf "%s" "$((_timestamp + 3600))" ;; hour) printf "%s" "$((_timestamp + 3600))" ;;
daily) date -d "$(date -d "@${_timestamp}" +%Y-%m-%d) + 1 day ${_time_of_day}" +%s ;; day) date -d "$(date -d "@${_timestamp}" +%Y-%m-%d) + 1 day ${_time_of_day}" +%s ;;
weekly) _next_matching_day "${_timestamp}" "${_time_of_day}" "$(date -d "@${_timestamp}" +%u)" ;; week) _next_matching_day "${_timestamp}" "${_time_of_day}" "$(date -d "@${_timestamp}" +%u)" ;;
weekday) _next_matching_day "${_timestamp}" "${_time_of_day}" "1 2 3 4 5" ;; weekday) _next_matching_day "${_timestamp}" "${_time_of_day}" "1 2 3 4 5" ;;
weekend) _next_matching_day "${_timestamp}" "${_time_of_day}" "6 7" ;; weekend) _next_matching_day "${_timestamp}" "${_time_of_day}" "6 7" ;;
mon) _next_matching_day "${_timestamp}" "${_time_of_day}" "1" ;; mon) _next_matching_day "${_timestamp}" "${_time_of_day}" "1" ;;
tue) _next_matching_day "${_timestamp}" "${_time_of_day}" "2" ;; tue) _next_matching_day "${_timestamp}" "${_time_of_day}" "2" ;;
wed) _next_matching_day "${_timestamp}" "${_time_of_day}" "3" ;; wed) _next_matching_day "${_timestamp}" "${_time_of_day}" "3" ;;
thu) _next_matching_day "${_timestamp}" "${_time_of_day}" "4" ;; thu) _next_matching_day "${_timestamp}" "${_time_of_day}" "4" ;;
fri) _next_matching_day "${_timestamp}" "${_time_of_day}" "5" ;; fri) _next_matching_day "${_timestamp}" "${_time_of_day}" "5" ;;
sat) _next_matching_day "${_timestamp}" "${_time_of_day}" "6" ;; sat) _next_matching_day "${_timestamp}" "${_time_of_day}" "6" ;;
sun) _next_matching_day "${_timestamp}" "${_time_of_day}" "7" ;; sun) _next_matching_day "${_timestamp}" "${_time_of_day}" "7" ;;
monthly) _next_month "${_timestamp}" "${_time_of_day}" ;; month) _next_month "${_timestamp}" "${_time_of_day}" ;;
yearly) _next_year "${_timestamp}" "${_time_of_day}" ;; year) _next_year "${_timestamp}" "${_time_of_day}" ;;
esac esac
} }
@ -1072,6 +1072,67 @@ stop() {
printf "Stopped alarm %s.\\n" "${_target_id}" printf "Stopped alarm %s.\\n" "${_target_id}"
} }
# check #######################################################################
describe "check" <<HEREDOC
Usage:
${_ME} check
Description:
Fire expired alarms. Intended to be run by cron every minute.
Repeating alarms are rescheduled. One-shot alarms are removed.
HEREDOC
check() {
[[ -f "${_ALARMS_FILE}" ]] || return 0
_acquire_lock
_read_alarms
local -a _new_alarms=()
local _now
_now="$(date +%s)"
local _line
for _line in "${_ALARMS[@]:-}"
do
[[ -n "${_line}" ]] || continue
local _id _timestamp _rule _message
_id="$(_get_alarm_field "${_line}" 1)"
_timestamp="$(_get_alarm_field "${_line}" 2)"
_rule="$(_get_alarm_field "${_line}" 3)"
_message="$(_get_alarm_field "${_line}" 4)"
if (( _timestamp <= _now ))
then
# Fire it.
${NAG_CMD} "nag" "${_message}" || _warn printf "Failed to notify: %s\\n" "${_message}"
if [[ -n "${_rule}" ]]
then
# Repeating: reschedule.
local _next_ts
_next_ts="$(_next_occurrence "${_rule}" "${_timestamp}")"
_new_alarms+=("$(printf "%s\t%s\t%s\t%s" "${_id}" "${_next_ts}" "${_rule}" "${_message}")")
fi
# One-shot: drop it (don't add to _new_alarms).
else
# Not expired — keep it.
_new_alarms+=("${_line}")
fi
done
if [[ "${#_new_alarms[@]}" -eq 0 ]]
then
_ALARMS=()
: > "${_ALARMS_FILE}"
else
_ALARMS=("${_new_alarms[@]}")
_write_alarms
fi
_release_lock
}
# at ########################################################################## # at ##########################################################################
describe "at" <<HEREDOC describe "at" <<HEREDOC

View file

@ -213,3 +213,94 @@ load test_helper
[[ "${_first_line}" =~ "standup" ]] [[ "${_first_line}" =~ "standup" ]]
[[ "${_second_line}" =~ "take a break" ]] [[ "${_second_line}" =~ "take a break" ]]
} }
# check #######################################################################
# Helper: write a raw alarm line directly to the alarms file.
write_alarm() {
mkdir -p "${NAG_DIR}"
printf "%s\\n" "$1" >> "${NAG_DIR}/alarms"
}
@test "check fires expired one-shot and removes it" {
local _past_ts=$(( $(date +%s) - 60 ))
write_alarm "$(printf "1\t%s\t\tpast alarm" "${_past_ts}")"
# Record what was fired.
local _fired="${NAG_DIR}/fired"
export NAG_CMD="${NAG_DIR}/recorder"
cat > "${NAG_CMD}" <<'SCRIPT'
#!/usr/bin/env bash
printf "%s\n" "$2" >> "${NAG_DIR}/fired"
SCRIPT
chmod +x "${NAG_CMD}"
run "${_NAG}" check
[ "${status}" -eq 0 ]
# Alarm should have been removed.
[[ ! -s "${NAG_DIR}/alarms" ]] || [ "$(wc -l < "${NAG_DIR}/alarms")" -eq 0 ]
# Notification should have fired.
grep -q "past alarm" "${_fired}"
}
@test "check reschedules expired repeating alarm" {
local _past_ts=$(( $(date +%s) - 60 ))
write_alarm "$(printf "1\t%s\tday\tdaily alarm" "${_past_ts}")"
run "${_NAG}" check
[ "${status}" -eq 0 ]
# Alarm should still exist with a future timestamp.
[ -s "${NAG_DIR}/alarms" ]
local _new_ts
_new_ts="$(cut -f2 "${NAG_DIR}/alarms" | head -1)"
local _now
_now="$(date +%s)"
(( _new_ts > _now ))
}
@test "check does not fire future alarms" {
local _future_ts=$(( $(date +%s) + 3600 ))
write_alarm "$(printf "1\t%s\t\tfuture alarm" "${_future_ts}")"
run "${_NAG}" check
[ "${status}" -eq 0 ]
# Alarm should still be there, unchanged.
[ -s "${NAG_DIR}/alarms" ]
grep -q "future alarm" "${NAG_DIR}/alarms"
}
@test "check fires all missed alarms" {
local _past1=$(( $(date +%s) - 120 ))
local _past2=$(( $(date +%s) - 60 ))
write_alarm "$(printf "1\t%s\t\tmissed one" "${_past1}")"
write_alarm "$(printf "2\t%s\t\tmissed two" "${_past2}")"
local _fired="${NAG_DIR}/fired"
export NAG_CMD="${NAG_DIR}/recorder"
cat > "${NAG_CMD}" <<'SCRIPT'
#!/usr/bin/env bash
printf "%s\n" "$2" >> "${NAG_DIR}/fired"
SCRIPT
chmod +x "${NAG_CMD}"
run "${_NAG}" check
[ "${status}" -eq 0 ]
# Both should have fired.
grep -q "missed one" "${_fired}"
grep -q "missed two" "${_fired}"
# Both should be removed (one-shots).
[[ ! -s "${NAG_DIR}/alarms" ]] || [ "$(wc -l < "${NAG_DIR}/alarms")" -eq 0 ]
}
@test "help check shows check usage" {
run "${_NAG}" help check
[ "${status}" -eq 0 ]
[[ "${output}" =~ "Usage:" ]]
[[ "${output}" =~ "check" ]]
}