fix: entertainment 1h timer never fired (invalid OnCalendar); drop cron backend, systemd-only
This commit is contained in:
@@ -47,6 +47,11 @@ summary (newest last).
|
||||
|
||||
## Done (summary, newest last)
|
||||
|
||||
- 2026-08-06: Fix entertainment timer `1h` not firing — `interval_to_oncalendar`
|
||||
emitted invalid `OnCalendar=*-*-* */N:00:00` (systemd rejects `*/N` in the hour
|
||||
field); now `*-*-* 00/N:00:00`. Dropped the cron fallback entirely: scheduling
|
||||
is systemd user timers only (`sync_cron`/`interval_to_cron`/`cron_block`
|
||||
removed), `status` simplified, `Nd` intervals rejected with a clear error.
|
||||
- 2026-08-06: Nested `pos` subcommands — `# POS_SUBCMDS:` header annotation (telegram,
|
||||
docker-compose, docker-vbox) + `make gen` emits a `_pos_subcmds` completion map;
|
||||
nested tools (`telegram listener`) auto-list under their parent instead of as a
|
||||
|
||||
@@ -541,9 +541,9 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| `bin/pos-docker-vbox` | 158 | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) |
|
||||
| `bin/pos-entertainment-config` | 98 | Show or edit the entertainment config (ENABLED auto-trigger list, weather location) |
|
||||
| `bin/pos-entertainment-disable` | 32 | Disable a plugin's auto-trigger (remove it from ENABLED) |
|
||||
| `bin/pos-entertainment-enable` | 50 | Enable an auto-trigger for a plugin on a schedule |
|
||||
| `bin/pos-entertainment-enable` | 49 | Enable an auto-trigger for a plugin on a schedule |
|
||||
| `bin/pos-entertainment-send` | 93 | Run a public-API plugin and send its output via Telegram (default sender) |
|
||||
| `bin/pos-entertainment-status` | 55 | Show enabled plugins and scheduler state |
|
||||
| `bin/pos-entertainment-status` | 49 | Show enabled plugins and scheduler state |
|
||||
| `bin/pos-media-mp3` | 35 | Download audio as MP3 (yt-dlp) |
|
||||
| `bin/pos-media-mp4` | 38 | Download video as MP4 (interactive format select) |
|
||||
| `bin/pos-network-checkport` | 45 | Check TCP port connectivity |
|
||||
|
||||
+2
-2
@@ -183,7 +183,7 @@ make check # full self-consistency gate (syntax, exec bits, d
|
||||
|
||||
## Adding an Entertainment Plugin
|
||||
|
||||
The `entertainment` module routes public-API data to Telegram via the single runner `pos entertainment send <plugin>` (`bin/pos-entertainment-send`). Auto-triggering is config-driven: `ENABLED` in `entertainment.env` holds `plugin, interval` pairs; the tools `pos entertainment config|enable|disable|status` (`bin/pos-entertainment-*`) reconcile the schedule. All shared logic (ENABLED parsing, interval→schedule mapping, scheduler sync) lives in `lib/entertainment-lib.sh` — sourced by the `pos-entertainment-*` tools (never by plugins). The scheduler is auto-detected: systemd user timers when a user systemd manager exists, otherwise a managed user crontab block.
|
||||
The `entertainment` module routes public-API data to Telegram via the single runner `pos entertainment send <plugin>` (`bin/pos-entertainment-send`). Auto-triggering is config-driven: `ENABLED` in `entertainment.env` holds `plugin, interval` pairs; the tools `pos entertainment config|enable|disable|status` (`bin/pos-entertainment-*`) reconcile the schedule. All shared logic (ENABLED parsing, interval→schedule mapping, scheduler sync) lives in `lib/entertainment-lib.sh` — sourced by the `pos-entertainment-*` tools (never by plugins). The scheduler is **systemd user timers** — the only backend (requires a reachable user systemd manager).
|
||||
|
||||
### 1. Create the plugin
|
||||
|
||||
@@ -203,7 +203,7 @@ data="$(curl -fsS --max-time 20 https://api.example.com/foo)"
|
||||
printf 'Title: %s\n' "$data"
|
||||
```
|
||||
|
||||
**Contract:** plugins are **self-contained** — do **not** source `lib/common.sh`. Its `log`/`warn`/`ok` helpers print to **stdout**, and the runner captures stdout as the message to send (helper chatter would be sent to Telegram). All stdout is the message; errors go to stderr and exit nonzero. Plugins must be non-interactive (no prompts) — the module is designed for cron/systemd timers.
|
||||
**Contract:** plugins are **self-contained** — do **not** source `lib/common.sh`. Its `log`/`warn`/`ok` helpers print to **stdout**, and the runner captures stdout as the message to send (helper chatter would be sent to Telegram). All stdout is the message; errors go to stderr and exit nonzero. Plugins must be non-interactive (no prompts) — the module is designed for systemd user timers.
|
||||
|
||||
### 2. Config (if needed)
|
||||
|
||||
|
||||
+6
-7
@@ -242,7 +242,7 @@ The map file is re-read for every message — edits apply without a restart. The
|
||||
| `pos entertainment send <plugin> --markdown` | Send with `--parse-mode markdown` (via `pos communication telegram`) |
|
||||
| `pos entertainment config` | Show the config file (`~/.config/linux_post_install/entertainment.env`) |
|
||||
| `pos entertainment config set KEY=VALUE…` | Set keys (any UPPER_SNAKE key; warns if no installed plugin uses it) and re-sync the schedule |
|
||||
| `pos entertainment enable <plugin> [interval]` | Add plugin to `ENABLED` + schedule it (systemd user timer or cron, auto-detected) |
|
||||
| `pos entertainment enable <plugin> [interval]` | Add plugin to `ENABLED` + schedule it as a systemd user timer |
|
||||
| `pos entertainment disable <plugin>` | Remove plugin from `ENABLED` + remove its scheduled job |
|
||||
| `pos entertainment status` | Enabled plugins + scheduler + schedule state |
|
||||
|
||||
@@ -276,16 +276,15 @@ The plugin itself still reads the keys as plain env vars (`${MYFEED_URL:-}`). Th
|
||||
ENABLED="weather, 5m gold, 1h joke, daily"
|
||||
```
|
||||
|
||||
`pos entertainment enable <plugin> [interval]` appends/updates one entry and re-syncs; `pos entertainment disable <plugin>` removes it; `pos entertainment config set ENABLED="…"` replaces the whole list. The scheduler backend is auto-detected on each sync:
|
||||
`pos entertainment enable <plugin> [interval]` appends/updates one entry and re-syncs; `pos entertainment disable <plugin>` removes it; `pos entertainment config set ENABLED="…"` replaces the whole list. Scheduling uses **systemd user timers** (requires a reachable user systemd manager):
|
||||
|
||||
- **systemd** (when a user systemd manager is reachable): one **user timer** per enabled plugin (`~/.config/systemd/user/pos-entertainment-<plugin>.{service,timer}`), running `pos entertainment send <plugin>` as your user on that schedule (`OnCalendar` + `Persistent=true`). `pos entertainment enable` also tries `sudo loginctl enable-linger $USER` once so timers fire without login.
|
||||
- **cron** (fallback when no systemd user manager, e.g. this dev box): a managed block in your user crontab (`# POS-ENTERTAINMENT-BEGIN`…`END`), one line per plugin. Cron runs as your user and fires without login.
|
||||
- One **user timer** per enabled plugin (`~/.config/systemd/user/pos-entertainment-<plugin>.{service,timer}`), running `pos entertainment send <plugin>` as your user on that schedule (`OnCalendar` + `Persistent=true`). `pos entertainment enable` also tries `sudo loginctl enable-linger $USER` once so timers fire without login.
|
||||
|
||||
Either way the job runs as you, so it reads your `$HOME` configs (weather location, Telegram token) natively — no `Environment=HOME=` hacks.
|
||||
The job runs as you, so it reads your `$HOME` configs (weather location, Telegram token) natively — no `Environment=HOME=` hacks.
|
||||
|
||||
Intervals: `5m 10m 15m 30m 45m hourly 2h 6h 12h daily weekly`, or a raw `OnCalendar=…` spec (systemd mode only; cron mode uses the named intervals). Default when omitted: `daily`.
|
||||
Intervals: `5m 10m 15m 30m 45m hourly 2h 6h 12h daily weekly`, or a raw `OnCalendar=…` spec. Default when omitted: `daily`.
|
||||
|
||||
`pos entertainment status` shows the enabled plugins, the detected scheduler, and each plugin's interval + next/next-ish fire time (`systemctl --user list-timers` or the crontab block).
|
||||
`pos entertainment status` shows the enabled plugins, the scheduler, and each plugin's interval + next fire time (`systemctl --user list-timers`).
|
||||
|
||||
Notes:
|
||||
- The runner is headless/timer-friendly — no TTY prompts, exit 0 on success / 1 on failure.
|
||||
|
||||
@@ -42,8 +42,8 @@ pos entertainment status # plugins + active schedules
|
||||
|
||||
## How it works
|
||||
|
||||
- **Scheduling** uses systemd **user** timers (unit `pos-ent-<plugin>.timer`,
|
||||
fallback to cron when systemd user units are unavailable). Interval is
|
||||
- **Scheduling** uses systemd **user** timers only (unit
|
||||
`pos-entertainment-<plugin>.timer` in `~/.config/systemd/user/`). Interval is
|
||||
resolved through the same systemd-time parser used by `.timer` units —
|
||||
invalid values are rejected with a clear message.
|
||||
- **Delivery** goes through `notify_send`, so the platform follows
|
||||
|
||||
@@ -11,7 +11,7 @@ Usage: pos entertainment disable <plugin>
|
||||
|
||||
Remove a plugin from the ENABLED list in
|
||||
$CONFIG_FILE
|
||||
and remove its scheduled job (systemd user timer or cron line).
|
||||
and remove its scheduled systemd user timer.
|
||||
|
||||
Examples:
|
||||
pos entertainment disable weather
|
||||
|
||||
@@ -11,9 +11,8 @@ Usage: pos entertainment enable <plugin> [interval]
|
||||
|
||||
Enable an auto-trigger: writes '<plugin>, <interval>' into the ENABLED list in
|
||||
$CONFIG_FILE
|
||||
and schedules 'pos entertainment send <plugin>' to run (systemd user timer, or
|
||||
cron when systemd is unavailable). Re-running without an interval keeps the
|
||||
plugin's current one.
|
||||
and schedules 'pos entertainment send <plugin>' to run as a systemd user
|
||||
timer. Re-running without an interval keeps the plugin's current one.
|
||||
|
||||
Available plugins:
|
||||
$(list_plugins "$(plugin_dir)" | sed 's/^/ /')
|
||||
|
||||
@@ -39,17 +39,11 @@ for entry in "${ENABLED_ENTRIES[@]}"; do
|
||||
done
|
||||
|
||||
echo
|
||||
echo "Scheduler: $(if systemctl --user show-environment >/dev/null 2>&1; then echo 'systemd user timers'; elif command -v crontab >/dev/null 2>&1; then echo 'cron (user crontab)'; else echo 'none available'; fi)"
|
||||
if systemctl --user show-environment >/dev/null 2>&1; then
|
||||
echo "Scheduler: systemd user timers"
|
||||
echo "Timers (systemd --user):"
|
||||
systemctl --user list-timers --no-pager 'pos-entertainment-*.timer' 2>/dev/null || \
|
||||
echo " (no timers)"
|
||||
elif command -v crontab >/dev/null 2>&1; then
|
||||
echo "Cron jobs (user crontab):"
|
||||
block="$(cron_block)"
|
||||
if [ -n "$block" ]; then
|
||||
printf '%s\n' "$block" | sed 's/^/ /'
|
||||
else
|
||||
echo " (none)"
|
||||
fi
|
||||
else
|
||||
echo "Scheduler: none available (no systemd user manager)"
|
||||
fi
|
||||
|
||||
@@ -209,17 +209,12 @@ interval_to_oncalendar() {
|
||||
if [[ "$i" =~ ^([0-9]+)m$ ]]; then
|
||||
local n="${BASH_REMATCH[1]}"
|
||||
[ "$n" -ge 1 ] && [ "$n" -le 59 ] || return 1
|
||||
printf '*:0/%s:00' "$n"; return 0
|
||||
printf '*:00/%s:00' "$n"; return 0
|
||||
fi
|
||||
if [[ "$i" =~ ^([0-9]+)h$ ]]; then
|
||||
local n="${BASH_REMATCH[1]}"
|
||||
[ "$n" -ge 1 ] && [ "$n" -le 23 ] || return 1
|
||||
printf '*-*-* */%s:00:00' "$n"; return 0
|
||||
fi
|
||||
if [[ "$i" =~ ^([0-9]+)d$ ]]; then
|
||||
local n="${BASH_REMATCH[1]}"
|
||||
[ "$n" -ge 1 ] && [ "$n" -le 30 ] || return 1
|
||||
printf '*-*-*/%s 08:00:00' "$n"; return 0
|
||||
printf '*-*-* 00/%s:00:00' "$n"; return 0
|
||||
fi
|
||||
case "$i" in
|
||||
hourly) printf '*-*-* *:00:00' ;;
|
||||
@@ -293,15 +288,12 @@ ensure_linger() {
|
||||
}
|
||||
|
||||
# Reconcile the auto-trigger schedule with the ENABLED list in the config.
|
||||
# Backend is auto-detected: systemd user timers if a user systemd manager is
|
||||
# reachable, otherwise user crontab (cron fires without login, reads $HOME).
|
||||
# Backend: systemd user timers (requires a reachable user systemd manager).
|
||||
sync_timers() {
|
||||
if systemctl --user show-environment >/dev/null 2>&1; then
|
||||
sync_systemd
|
||||
elif command -v crontab >/dev/null 2>&1; then
|
||||
sync_cron
|
||||
else
|
||||
warn "no scheduler available (systemd user manager or cron) — run 'pos entertainment send <plugin>' manually"
|
||||
warn "no systemd user manager available — run 'pos entertainment send <plugin>' manually"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -357,72 +349,3 @@ sync_systemd() {
|
||||
done
|
||||
}
|
||||
|
||||
# ── Backend: user crontab (fallback when no systemd user manager) ──
|
||||
CRON_BEGIN="# POS-ENTERTAINMENT-BEGIN"
|
||||
CRON_END="# POS-ENTERTAINMENT-END"
|
||||
|
||||
interval_to_cron() {
|
||||
local i="$1"
|
||||
case "$i" in
|
||||
1h|hourly) printf '0 * * * *'; return 0 ;;
|
||||
1d|daily) printf '0 8 * * *'; return 0 ;;
|
||||
weekly) printf '0 8 * * 1'; return 0 ;;
|
||||
OnCalendar=*) return 1 ;;
|
||||
esac
|
||||
if [[ "$i" =~ ^([0-9]+)m$ ]]; then
|
||||
local n="${BASH_REMATCH[1]}"
|
||||
[ "$n" -ge 1 ] && [ "$n" -le 59 ] || return 1
|
||||
printf '*/%s * * * *' "$n"; return 0
|
||||
fi
|
||||
if [[ "$i" =~ ^([0-9]+)h$ ]]; then
|
||||
local n="${BASH_REMATCH[1]}"
|
||||
[ "$n" -ge 1 ] && [ "$n" -le 23 ] || return 1
|
||||
printf '0 */%s * * *' "$n"; return 0
|
||||
fi
|
||||
if [[ "$i" =~ ^([0-9]+)d$ ]]; then
|
||||
local n="${BASH_REMATCH[1]}"
|
||||
[ "$n" -ge 1 ] && [ "$n" -le 30 ] || return 1
|
||||
printf '0 8 */%s * *' "$n"; return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
sync_cron() {
|
||||
local raw entry plugin interval cron runner
|
||||
raw="$(config_value ENABLED)"
|
||||
parse_enabled "$raw"
|
||||
runner="$(command -v pos-entertainment-send 2>/dev/null || echo /usr/local/bin/pos-entertainment-send)"
|
||||
|
||||
local -a lines=()
|
||||
for entry in "${ENABLED_ENTRIES[@]}"; do
|
||||
plugin="${entry%%,*}"; interval="${entry##*,}"
|
||||
[ "$interval" = "$plugin" ] && interval="$DEFAULT_INTERVAL"
|
||||
if ! plugin_exists "$(plugin_dir)" "$plugin"; then
|
||||
warn "plugin '$plugin' not installed — skipping"
|
||||
continue
|
||||
fi
|
||||
if ! cron="$(interval_to_cron "$interval")"; then
|
||||
warn "interval '$interval' cannot be mapped to cron for '$plugin' — skipping (use a named interval)"
|
||||
continue
|
||||
fi
|
||||
lines+=("$cron $runner $plugin")
|
||||
done
|
||||
|
||||
local head block=""
|
||||
head="$(crontab -l 2>/dev/null | sed "/^${CRON_BEGIN}$/,/^${CRON_END}$/d" || true)"
|
||||
[ "${#lines[@]}" -gt 0 ] && block="$CRON_BEGIN"$'\n'"$(printf '%s\n' "${lines[@]}")"$'\n'"$CRON_END"
|
||||
if [ -n "$block" ]; then
|
||||
[ -n "$head" ] && head+=$'\n'
|
||||
head+="$block"
|
||||
fi
|
||||
printf '%s\n' "$head" | crontab - 2>/dev/null || \
|
||||
err "could not write crontab — check 'crontab -l' permissions"
|
||||
local jobs
|
||||
jobs="$(crontab -l 2>/dev/null | sed -n "/^${CRON_BEGIN}$/,/^${CRON_END}$/p" | grep -c '^\*/\|^0 ' || true)"
|
||||
[ -z "$jobs" ] && jobs=0
|
||||
log "cron schedule synced ($jobs job(s))"
|
||||
}
|
||||
|
||||
cron_block() {
|
||||
crontab -l 2>/dev/null | sed -n "/^${CRON_BEGIN}$/,/^${CRON_END}$/p" || true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user