feat: entertainment hardening — notify_send delivery, plugin lib, last-run state, config edit; shared user-timers lib
This commit is contained in:
+54
-97
@@ -4,7 +4,6 @@
|
||||
|
||||
CONFIG_DIR="$HOME/.config/linux_post_install"
|
||||
CONFIG_FILE="$CONFIG_DIR/entertainment.env"
|
||||
USER_SYSTEMD_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
TIMER_PREFIX="pos-entertainment"
|
||||
DEFAULT_INTERVAL="daily"
|
||||
|
||||
@@ -14,6 +13,17 @@ declare -F warn >/dev/null || warn() { echo "[!] $*"; }
|
||||
declare -F ok >/dev/null || ok() { echo " OK $*"; }
|
||||
declare -F log >/dev/null || log() { echo "[+] $*"; }
|
||||
|
||||
# Shared systemd **user** timer machinery (interval→OnCalendar mapping, unit
|
||||
# pair writer, linger bootstrap) — the same lib the system scheduler uses, so
|
||||
# the two unit templates never drift apart. Defines USER_SYSTEMD_DIR + ut_*.
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/../lib/user-timers-lib.sh" 2>/dev/null \
|
||||
|| source "$(dirname "${BASH_SOURCE[0]}")/user-timers-lib.sh" 2>/dev/null \
|
||||
|| source "$(dirname "$0")/../lib/user-timers-lib.sh" 2>/dev/null \
|
||||
|| source "$(dirname "$0")/user-timers-lib.sh"
|
||||
|
||||
# Per-plugin last-run state (rc + timestamp + first output line).
|
||||
LAST_RUN_DIR="${LAST_RUN_DIR:-$HOME/.local/share/linux_post_install/entertainment/last}"
|
||||
|
||||
# ── Config file helpers (file is the source of truth, never sourced) ──
|
||||
config_value() {
|
||||
local k="$1" v
|
||||
@@ -28,6 +38,14 @@ write_config_key() {
|
||||
val="${val//$'\r'/}"
|
||||
val="${val%%$'\n'*}"
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
if [ "$val" = "-" ]; then
|
||||
[ -f "$CONFIG_FILE" ] || return 0
|
||||
tmp="$(mktemp)"
|
||||
grep -v "^${key}=" "$CONFIG_FILE" >"$tmp" || true
|
||||
mv "$tmp" "$CONFIG_FILE"
|
||||
chmod 600 "$CONFIG_FILE"
|
||||
return 0
|
||||
fi
|
||||
tmp="$(mktemp)"
|
||||
grep -v "^${key}=" "$CONFIG_FILE" 2>/dev/null >"$tmp" || true
|
||||
printf '%s="%s"\n' "$key" "$val" >>"$tmp"
|
||||
@@ -200,96 +218,10 @@ enabled_remove() {
|
||||
write_config_key ENABLED "$(render_enabled "${out[@]}")"
|
||||
}
|
||||
|
||||
# ── Interval → systemd OnCalendar ─────────────────────────────────
|
||||
interval_to_oncalendar() {
|
||||
local i="$1"
|
||||
case "$i" in
|
||||
OnCalendar=*) printf '%s' "${i#OnCalendar=}"; return 0 ;;
|
||||
esac
|
||||
if [[ "$i" =~ ^([0-9]+)m$ ]]; then
|
||||
local n="${BASH_REMATCH[1]}"
|
||||
[ "$n" -ge 1 ] && [ "$n" -le 59 ] || return 1
|
||||
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 '*-*-* 00/%s:00:00' "$n"; return 0
|
||||
fi
|
||||
case "$i" in
|
||||
hourly) printf '*-*-* *:00:00' ;;
|
||||
daily) printf '*-*-* 08:00:00' ;;
|
||||
weekly) printf 'Mon *-*-* 08:00:00' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
interval_label() {
|
||||
case "$1" in
|
||||
daily) echo "daily (08:00)" ;;
|
||||
weekly) echo "weekly (Mon 08:00)" ;;
|
||||
hourly) echo "hourly" ;;
|
||||
OnCalendar=*) echo "${1#OnCalendar=}" ;;
|
||||
*m|*h|*d) echo "every $1" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── systemd user timers ───────────────────────────────────────────
|
||||
unit_name() { echo "${TIMER_PREFIX}-${1}"; }
|
||||
|
||||
write_units() {
|
||||
local plugin="$1" oncal="$2"
|
||||
local base="$USER_SYSTEMD_DIR/$(unit_name "$plugin")"
|
||||
local runner
|
||||
runner="$(command -v pos-entertainment-send 2>/dev/null || echo /usr/local/bin/pos-entertainment-send)"
|
||||
mkdir -p "$USER_SYSTEMD_DIR"
|
||||
cat >"$base.service" <<EOF
|
||||
[Unit]
|
||||
Description=pos entertainment send $plugin
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=$runner $plugin
|
||||
TimeoutStopSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
cat >"$base.timer" <<EOF
|
||||
[Unit]
|
||||
Description=Schedule: pos entertainment send $plugin
|
||||
|
||||
[Timer]
|
||||
OnCalendar=$oncal
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
chmod 644 "$base.service" "$base.timer"
|
||||
}
|
||||
|
||||
ensure_linger() {
|
||||
local user
|
||||
user="$(id -un)"
|
||||
[ "$user" = "root" ] && { warn "running as root — enable linger for your real user: sudo loginctl enable-linger <user>"; return 0; }
|
||||
command -v loginctl >/dev/null 2>&1 || return 0
|
||||
if loginctl show-user "$user" 2>/dev/null | grep -q '^Linger=yes'; then
|
||||
return 0
|
||||
fi
|
||||
if sudo -n loginctl enable-linger "$user" 2>/dev/null; then
|
||||
ok "enabled linger for $user (timers fire without login)"
|
||||
else
|
||||
warn "run once so timers fire without login: sudo loginctl enable-linger $user"
|
||||
fi
|
||||
}
|
||||
|
||||
# Reconcile the auto-trigger schedule with the ENABLED list in the config.
|
||||
# Backend: systemd user timers (requires a reachable user systemd manager).
|
||||
# Interval → OnCalendar mapping, unit naming, the unit pair writer and linger
|
||||
# bootstrap come from lib/user-timers-lib.sh (ut_*). Reconcile the auto-trigger
|
||||
# schedule with the ENABLED list in the config.
|
||||
sync_timers() {
|
||||
if systemctl --user show-environment >/dev/null 2>&1; then
|
||||
sync_systemd
|
||||
@@ -312,24 +244,26 @@ sync_systemd() {
|
||||
warn "plugin '$plugin' not installed — skipping"
|
||||
continue
|
||||
fi
|
||||
if ! oncal="$(interval_to_oncalendar "$interval")"; then
|
||||
if ! oncal="$(ut_interval_to_oncalendar "$interval")"; then
|
||||
warn "invalid interval '$interval' for '$plugin' — skipping"
|
||||
continue
|
||||
fi
|
||||
wanted["$plugin"]="$oncal"
|
||||
done
|
||||
|
||||
local wrote=0 t n=0
|
||||
local runner wrote=0 t n=0
|
||||
runner="$(command -v pos-entertainment-send 2>/dev/null || echo /usr/local/bin/pos-entertainment-send)"
|
||||
for plugin in "${!wanted[@]}"; do
|
||||
write_units "$plugin" "${wanted[$plugin]}"
|
||||
local base="$USER_SYSTEMD_DIR/$(ut_unit_name "$TIMER_PREFIX" "$plugin")"
|
||||
ut_write_unit_pair "$base.service" "$base.timer" "pos entertainment send $plugin" "$runner $plugin" "${wanted[$plugin]}"
|
||||
wrote=1; n=$((n + 1))
|
||||
done
|
||||
[ "$wrote" -eq 1 ] && systemctl --user daemon-reload >/dev/null 2>&1 || true
|
||||
|
||||
[ "$n" -gt 0 ] && ensure_linger
|
||||
[ "$n" -gt 0 ] && ut_ensure_linger
|
||||
|
||||
for plugin in "${!wanted[@]}"; do
|
||||
t="$(unit_name "$plugin").timer"
|
||||
t="$(ut_unit_name "$TIMER_PREFIX" "$plugin").timer"
|
||||
if systemctl --user is-enabled "$t" >/dev/null 2>&1; then
|
||||
systemctl --user restart "$t" >/dev/null 2>&1 || true
|
||||
else
|
||||
@@ -343,8 +277,8 @@ sync_systemd() {
|
||||
[ -f "$f" ] || continue
|
||||
p="${f##*/}"; p="${p#${TIMER_PREFIX}-}"; p="${p%.timer}"
|
||||
if [ -z "${wanted[$p]:-}" ]; then
|
||||
systemctl --user disable --now "$(unit_name "$p").timer" >/dev/null 2>&1 || true
|
||||
rm -f "$USER_SYSTEMD_DIR/$(unit_name "$p").timer" "$USER_SYSTEMD_DIR/$(unit_name "$p").service"
|
||||
systemctl --user disable --now "$(ut_unit_name "$TIMER_PREFIX" "$p").timer" >/dev/null 2>&1 || true
|
||||
rm -f "$USER_SYSTEMD_DIR/$(ut_unit_name "$TIMER_PREFIX" "$p").timer" "$USER_SYSTEMD_DIR/$(ut_unit_name "$TIMER_PREFIX" "$p").service"
|
||||
log "removed timer for '$p'"
|
||||
removed=1
|
||||
fi
|
||||
@@ -352,3 +286,26 @@ sync_systemd() {
|
||||
[ "$removed" -eq 1 ] && systemctl --user daemon-reload >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# ── Per-plugin last-run state ────────────────────────────────────
|
||||
# Written by 'pos entertainment send' on every non---print run so 'status' can
|
||||
# show whether a scheduled run succeeded. State dir is per-user, not tracked.
|
||||
save_last_run() { # $1 = plugin, $2 = rc, $3 = message (first line)
|
||||
local f="$LAST_RUN_DIR/$1"
|
||||
mkdir -p "$LAST_RUN_DIR"
|
||||
{
|
||||
printf 'rc=%s\n' "$2"
|
||||
printf 'ts=%s\n' "$(date +%s)"
|
||||
printf 'msg=%s\n' "$3"
|
||||
} >"$f"
|
||||
chmod 600 "$f"
|
||||
}
|
||||
|
||||
last_run_str() { # $1 = plugin → "rc=N (MM-DD HH:MM)" or "never"
|
||||
local f="$LAST_RUN_DIR/$1" rc ts
|
||||
[ -f "$f" ] || { echo "never"; return 0; }
|
||||
rc="$(sed -n 's/^rc=//p' "$f" | tail -1)"
|
||||
ts="$(sed -n 's/^ts=//p' "$f" | tail -1)"
|
||||
[ -n "$rc" ] || { echo "never"; return 0; }
|
||||
printf 'rc=%s (%s)' "$rc" "$(date -d "@$ts" '+%m-%d %H:%M' 2>/dev/null || echo '?')"
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# lib/entertainment-plugin-lib.sh — message-safe helpers for entertainment
|
||||
# plugins. Plugins MAY source this (unlike lib/common.sh, whose log/warn/ok
|
||||
# chatter would leak into the sent message). Contract: this lib NEVER writes to
|
||||
# stdout — helpers print errors to stderr and exit non-zero, so stdout stays the
|
||||
# message. Defines only plugin_* names, so it can't collide with common.sh or a
|
||||
# plugin's own helpers.
|
||||
#
|
||||
# Source pattern (works from the repo and from /usr/local/bin after install):
|
||||
# source "$(dirname "${BASH_SOURCE[0]}")/../lib/entertainment-plugin-lib.sh" 2>/dev/null \
|
||||
# || source "$(dirname "${BASH_SOURCE[0]}")/entertainment-plugin-lib.sh" 2>/dev/null \
|
||||
# || source "$(dirname "$0")/../lib/entertainment-plugin-lib.sh" 2>/dev/null \
|
||||
# || source "$(dirname "$0")/entertainment-plugin-lib.sh"
|
||||
|
||||
# Config file is the source of truth — read it like the pos tools do (never source it).
|
||||
plugin_config_file="${PLUGIN_CONFIG_FILE:-$HOME/.config/linux_post_install/entertainment.env}"
|
||||
|
||||
plugin_err() { echo "ERROR: $*" >&2; exit 1; }
|
||||
|
||||
# Load KEY=VALUE pairs from entertainment.env into the environment.
|
||||
# Env vars already set win (env precedence); quoted values are stripped;
|
||||
# comments and blank lines skipped.
|
||||
plugin_load_config() {
|
||||
[ -f "$plugin_config_file" ] || return 0
|
||||
local k v
|
||||
while IFS='=' read -r k v; do
|
||||
[ -n "$k" ] || continue
|
||||
case "$k" in
|
||||
\#*) continue ;;
|
||||
esac
|
||||
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||
if [ -z "${!k:-}" ]; then
|
||||
export "$k"="$v"
|
||||
fi
|
||||
done < <(grep -E '^[A-Z_]+=' "$plugin_config_file" || true)
|
||||
}
|
||||
|
||||
# Fail unless the command is available.
|
||||
plugin_have() {
|
||||
command -v "$1" >/dev/null 2>&1 || plugin_err "$1 not found — install it (see DOC/PREINSTALL.md or preinstall.sh PACKAGES)"
|
||||
}
|
||||
|
||||
# Fail unless the config key is set (after plugin_load_config).
|
||||
plugin_require() {
|
||||
[ -n "${!1:-}" ] || plugin_err "$1 not set — add it to $plugin_config_file"
|
||||
}
|
||||
|
||||
# Fetch a URL as JSON with curl, retrying twice on transient failures, then
|
||||
# extract the given jq key (optional). Everything is silent on stdout; errors
|
||||
# go to stderr and exit non-zero.
|
||||
plugin_http_json() {
|
||||
local key="" url="" json
|
||||
local -a headers=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--key) key="$2"; shift 2 ;;
|
||||
--header|-H) headers+=(-H "$2"); shift 2 ;;
|
||||
*) url="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$url" ] || plugin_err "plugin_http_json: no URL given"
|
||||
json="$(curl -fsS --max-time 20 --retry 2 --retry-delay 2 "${headers[@]}" "$url")" || \
|
||||
plugin_err "Failed to fetch $url"
|
||||
if [ -n "$key" ]; then
|
||||
json="$(jq -r "$key" <<<"$json")" || plugin_err "Failed to parse '$key' from $url"
|
||||
fi
|
||||
printf '%s' "$json"
|
||||
}
|
||||
+27
-97
@@ -41,6 +41,14 @@ declare -F warn >/dev/null || warn() { echo "[!] $*"; }
|
||||
declare -F ok >/dev/null || ok() { echo " OK $*"; }
|
||||
declare -F log >/dev/null || log() { echo "[+] $*"; }
|
||||
|
||||
# Shared systemd **user** timer machinery (interval→OnCalendar mapping, unit
|
||||
# pair writer, linger bootstrap) — the same lib the entertainment module uses,
|
||||
# so the two unit templates never drift apart. Defines USER_SYSTEMD_DIR + ut_*.
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/../lib/user-timers-lib.sh" 2>/dev/null \
|
||||
|| source "$(dirname "${BASH_SOURCE[0]}")/user-timers-lib.sh" 2>/dev/null \
|
||||
|| source "$(dirname "$0")/../lib/user-timers-lib.sh" 2>/dev/null \
|
||||
|| source "$(dirname "$0")/user-timers-lib.sh"
|
||||
|
||||
# ── Job discovery / naming ───────────────────────────────────────
|
||||
sched_valid_name() {
|
||||
[[ "$1" =~ ^[a-z0-9][a-z0-9_-]*$ ]]
|
||||
@@ -341,92 +349,9 @@ sched_run_job() { # $1 = job name
|
||||
[ "${DRY_RUN:-0}" -ne 1 ] && sched_save_run "$1" "$JOB_OUT" "$JOB_RC"
|
||||
return 0
|
||||
}
|
||||
# ── Interval → systemd OnCalendar (same list as entertainment) ──
|
||||
sched_interval_to_oncalendar() {
|
||||
local i="$1"
|
||||
case "$i" in
|
||||
OnCalendar=*) printf '%s' "${i#OnCalendar=}"; return 0 ;;
|
||||
esac
|
||||
if [[ "$i" =~ ^([0-9]+)m$ ]]; then
|
||||
local n="${BASH_REMATCH[1]}"
|
||||
[ "$n" -ge 1 ] && [ "$n" -le 59 ] || return 1
|
||||
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 '*-*-* 00/%s:00:00' "$n"; return 0
|
||||
fi
|
||||
case "$i" in
|
||||
hourly) printf '*-*-* *:00:00' ;;
|
||||
daily) printf '*-*-* 08:00:00' ;;
|
||||
weekly) printf 'Mon *-*-* 08:00:00' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
sched_interval_label() {
|
||||
case "$1" in
|
||||
daily) echo "daily (08:00)" ;;
|
||||
weekly) echo "weekly (Mon 08:00)" ;;
|
||||
hourly) echo "hourly" ;;
|
||||
OnCalendar=*) echo "${1#OnCalendar=}" ;;
|
||||
*m|*h) echo "every $1" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── systemd user timers (one pair per job) ──────────────────────
|
||||
sched_unit() { printf '%s-%s\n' "$SCHED_PREFIX" "$1"; }
|
||||
|
||||
sched_ensure_linger() {
|
||||
local user
|
||||
user="$(id -un)"
|
||||
[ "$user" = "root" ] && { warn "running as root — enable linger for your real user: sudo loginctl enable-linger <user>"; return 0; }
|
||||
command -v loginctl >/dev/null 2>&1 || return 0
|
||||
if loginctl show-user "$user" 2>/dev/null | grep -q '^Linger=yes'; then
|
||||
return 0
|
||||
fi
|
||||
if sudo -n loginctl enable-linger "$user" 2>/dev/null; then
|
||||
ok "enabled linger for $user (timers fire without login)"
|
||||
else
|
||||
warn "run once so timers fire without login: sudo loginctl enable-linger $user"
|
||||
fi
|
||||
}
|
||||
|
||||
sched_write_units() { # $1 = job name, $2 = OnCalendar
|
||||
local name="$1" oncal="$2"
|
||||
local base="$USER_SYSTEMD_DIR/$(sched_unit "$name")"
|
||||
mkdir -p "$USER_SYSTEMD_DIR"
|
||||
cat >"$base.service" <<EOF
|
||||
[Unit]
|
||||
Description=pos system schedule — run job $name
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=$SCHED_RUNNER run $name
|
||||
TimeoutStopSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
cat >"$base.timer" <<EOF
|
||||
[Unit]
|
||||
Description=Schedule: pos system schedule — $name
|
||||
|
||||
[Timer]
|
||||
OnCalendar=$oncal
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
chmod 644 "$base.service" "$base.timer"
|
||||
}
|
||||
|
||||
# Interval → OnCalendar mapping, unit naming, the unit pair writer and linger
|
||||
# bootstrap come from lib/user-timers-lib.sh (ut_*).
|
||||
sched_job_active() { # $1 = job name — valid AND not disabled
|
||||
sched_read_job "$1" >/dev/null 2>&1 || return 1
|
||||
[ "$JOB_ENABLED" = "false" ] && return 1
|
||||
@@ -444,27 +369,32 @@ sched_sync() {
|
||||
continue
|
||||
fi
|
||||
[ "$JOB_ENABLED" = "false" ] && continue # disabled: orphan cleanup drops its timer
|
||||
if ! oncal="$(sched_interval_to_oncalendar "$JOB_INTERVAL")"; then
|
||||
if ! oncal="$(ut_interval_to_oncalendar "$JOB_INTERVAL")"; then
|
||||
warn "invalid interval '$JOB_INTERVAL' for '$name' — skipping"
|
||||
continue
|
||||
fi
|
||||
if [ "${DRY_RUN:-0}" -eq 1 ]; then
|
||||
log "(dry-run) write $(sched_unit "$name").timer (OnCalendar=$oncal)"
|
||||
log "(dry-run) write $(ut_unit_name "$SCHED_PREFIX" "$name").timer (OnCalendar=$oncal)"
|
||||
else
|
||||
sched_write_units "$name" "$oncal"
|
||||
ut_write_unit_pair \
|
||||
"$USER_SYSTEMD_DIR/$(ut_unit_name "$SCHED_PREFIX" "$name").service" \
|
||||
"$USER_SYSTEMD_DIR/$(ut_unit_name "$SCHED_PREFIX" "$name").timer" \
|
||||
"pos system schedule — run job $name" \
|
||||
"$SCHED_RUNNER run $name" \
|
||||
"$oncal"
|
||||
fi
|
||||
wrote=1; n=$((n + 1))
|
||||
done
|
||||
[ "${DRY_RUN:-0}" -eq 1 ] && return 0
|
||||
[ "$wrote" -eq 1 ] && systemctl --user daemon-reload >/dev/null 2>&1 || true
|
||||
[ "$n" -gt 0 ] && sched_ensure_linger
|
||||
[ "$n" -gt 0 ] && ut_ensure_linger
|
||||
for name in "${jobs[@]}"; do
|
||||
if ! sched_read_job "$name" >/dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
[ "$JOB_ENABLED" = "false" ] && continue
|
||||
sched_interval_to_oncalendar "$JOB_INTERVAL" >/dev/null 2>&1 || continue
|
||||
local t="$(sched_unit "$name").timer"
|
||||
ut_interval_to_oncalendar "$JOB_INTERVAL" >/dev/null 2>&1 || continue
|
||||
local t="$(ut_unit_name "$SCHED_PREFIX" "$name").timer"
|
||||
if systemctl --user is-enabled "$t" >/dev/null 2>&1; then
|
||||
systemctl --user restart "$t" >/dev/null 2>&1 || true
|
||||
else
|
||||
@@ -483,8 +413,8 @@ sched_remove_orphans() {
|
||||
[ -f "$f" ] || continue
|
||||
p="${f##*/}"; p="${p#${SCHED_PREFIX}-}"; p="${p%.timer}"
|
||||
if ! sched_job_active "$p"; then
|
||||
systemctl --user disable --now "$(sched_unit "$p").timer" >/dev/null 2>&1 || true
|
||||
rm -f "$USER_SYSTEMD_DIR/$(sched_unit "$p").timer" "$USER_SYSTEMD_DIR/$(sched_unit "$p").service"
|
||||
systemctl --user disable --now "$(ut_unit_name "$SCHED_PREFIX" "$p").timer" >/dev/null 2>&1 || true
|
||||
rm -f "$USER_SYSTEMD_DIR/$(ut_unit_name "$SCHED_PREFIX" "$p").timer" "$USER_SYSTEMD_DIR/$(ut_unit_name "$SCHED_PREFIX" "$p").service"
|
||||
log "removed timer for '$p'"
|
||||
removed=1
|
||||
fi
|
||||
@@ -693,8 +623,8 @@ sched_status() {
|
||||
local st="enabled" oncal next=""
|
||||
[ "$JOB_ENABLED" = "false" ] && st="disabled"
|
||||
if [ "$st" = "enabled" ]; then
|
||||
if oncal="$(sched_interval_to_oncalendar "$JOB_INTERVAL")"; then
|
||||
next="$(systemctl --user list-timers "$(sched_unit "$name").timer" --no-legend 2>/dev/null | awk '{print $1, $2}' | head -1)"
|
||||
if oncal="$(ut_interval_to_oncalendar "$JOB_INTERVAL")"; then
|
||||
next="$(systemctl --user list-timers "$(ut_unit_name "$SCHED_PREFIX" "$name").timer" --no-legend 2>/dev/null | awk '{print $1, $2}' | head -1)"
|
||||
else
|
||||
oncal="(invalid interval: $JOB_INTERVAL)"
|
||||
fi
|
||||
@@ -743,7 +673,7 @@ sched_editor_add() {
|
||||
[ -f "$SCHEDULE_DIR/$name.env" ] && { warn "job '$name' already exists — use edit"; return; }
|
||||
read -rp "interval [5m..59m | 1h..23h | hourly daily weekly | OnCalendar=…] (blank = $SCHED_DEFAULT_INTERVAL): " interval
|
||||
interval="${interval:-$SCHED_DEFAULT_INTERVAL}"
|
||||
sched_interval_to_oncalendar "$interval" >/dev/null 2>&1 || warn "interval '$interval' not recognized — saved anyway, 'enable' will skip it"
|
||||
ut_interval_to_oncalendar "$interval" >/dev/null 2>&1 || warn "interval '$interval' not recognized — saved anyway, 'enable' will skip it"
|
||||
read -rp "notify [always|onchange|onerror|threshold|never] (blank = onchange): " notify
|
||||
notify="${notify:-onchange}"
|
||||
case "$notify" in always|onchange|onerror|threshold|never) ;; *) warn "unknown policy '$notify' — job will run silently" ;; esac
|
||||
@@ -775,7 +705,7 @@ sched_editor_edit() {
|
||||
local interval notify msg rule cmd
|
||||
read -rp "interval (blank keeps, current: $JOB_INTERVAL): " interval
|
||||
interval="${interval:-$JOB_INTERVAL}"
|
||||
sched_interval_to_oncalendar "$interval" >/dev/null 2>&1 || warn "interval '$interval' not recognized — saved anyway, 'enable' will skip it"
|
||||
ut_interval_to_oncalendar "$interval" >/dev/null 2>&1 || warn "interval '$interval' not recognized — saved anyway, 'enable' will skip it"
|
||||
read -rp "notify (blank keeps, current: $JOB_NOTIFY): " notify
|
||||
notify="${notify:-$JOB_NOTIFY}"
|
||||
read -rp "message (blank keeps, current: ${JOB_MSG:-<none>}): " msg
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# lib/user-timers-lib.sh — shared systemd **user** timer machinery for the pos
|
||||
# tools that schedule one-shot jobs: the entertainment module and the system
|
||||
# scheduler. Sourced by lib/entertainment-lib.sh and lib/scheduler-lib.sh AFTER
|
||||
# lib/common.sh. Defines only ut_* helpers so it never collides with either lib.
|
||||
#
|
||||
# Both libs previously re-implemented the same interval→OnCalendar mapping, unit
|
||||
# pair writer and linger bootstrap (the two copies drifted — e.g. the
|
||||
# TimeoutStopSec fix had to be applied to both). This is the single source for:
|
||||
# ut_interval_to_oncalendar ut_interval_label ut_unit_name
|
||||
# ut_write_unit_pair ut_ensure_linger
|
||||
|
||||
# common.sh helpers (guarded so the lib is safe if common.sh wasn't loaded)
|
||||
declare -F err >/dev/null || err() { echo "ERROR: $*" >&2; exit 1; }
|
||||
declare -F warn >/dev/null || warn() { echo "[!] $*"; }
|
||||
declare -F ok >/dev/null || ok() { echo " OK $*"; }
|
||||
declare -F log >/dev/null || log() { echo "[+] $*"; }
|
||||
|
||||
USER_SYSTEMD_DIR="${USER_SYSTEMD_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user}"
|
||||
|
||||
# ── Interval → systemd OnCalendar ─────────────────────────────────
|
||||
# Named + numeric intervals shared by both schedulers, or a raw OnCalendar=…
|
||||
# spec passed through unchanged. Returns non-zero for invalid values.
|
||||
ut_interval_to_oncalendar() {
|
||||
local i="$1"
|
||||
case "$i" in
|
||||
OnCalendar=*) printf '%s' "${i#OnCalendar=}"; return 0 ;;
|
||||
esac
|
||||
if [[ "$i" =~ ^([0-9]+)m$ ]]; then
|
||||
local n="${BASH_REMATCH[1]}"
|
||||
[ "$n" -ge 1 ] && [ "$n" -le 59 ] || return 1
|
||||
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 '*-*-* 00/%s:00:00' "$n"; return 0
|
||||
fi
|
||||
case "$i" in
|
||||
hourly) printf '*-*-* *:00:00' ;;
|
||||
daily) printf '*-*-* 08:00:00' ;;
|
||||
weekly) printf 'Mon *-*-* 08:00:00' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
ut_interval_label() {
|
||||
case "$1" in
|
||||
daily) echo "daily (08:00)" ;;
|
||||
weekly) echo "weekly (Mon 08:00)" ;;
|
||||
hourly) echo "hourly" ;;
|
||||
OnCalendar=*) echo "${1#OnCalendar=}" ;;
|
||||
*m|*h|*d) echo "every $1" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Unit naming / writer ──────────────────────────────────────────
|
||||
ut_unit_name() { # $1 = prefix (e.g. pos-entertainment), $2 = name → prefix-name
|
||||
printf '%s-%s\n' "$1" "$2"
|
||||
}
|
||||
|
||||
# Write one service/timer pair (oneshot, network-online, Persistent).
|
||||
# Usage: ut_write_unit_pair <service-file> <timer-file> <description> <execstart> <oncal>
|
||||
ut_write_unit_pair() {
|
||||
local svc="$1" timer="$2" desc="$3" execstart="$4" oncal="$5"
|
||||
mkdir -p "$USER_SYSTEMD_DIR"
|
||||
cat >"$svc" <<EOF
|
||||
[Unit]
|
||||
Description=$desc
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=$execstart
|
||||
TimeoutStopSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
cat >"$timer" <<EOF
|
||||
[Unit]
|
||||
Description=Schedule: $desc
|
||||
|
||||
[Timer]
|
||||
OnCalendar=$oncal
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
chmod 644 "$svc" "$timer"
|
||||
}
|
||||
|
||||
# ── Linger bootstrap ──────────────────────────────────────────────
|
||||
# Timers only fire without a login when the user has linger enabled; try once,
|
||||
# warn with the exact command otherwise (never fails the caller).
|
||||
ut_ensure_linger() {
|
||||
local user
|
||||
user="$(id -un)"
|
||||
[ "$user" = "root" ] && { warn "running as root — enable linger for your real user: sudo loginctl enable-linger <user>"; return 0; }
|
||||
command -v loginctl >/dev/null 2>&1 || return 0
|
||||
if loginctl show-user "$user" 2>/dev/null | grep -q '^Linger=yes'; then
|
||||
return 0
|
||||
fi
|
||||
if sudo -n loginctl enable-linger "$user" 2>/dev/null; then
|
||||
ok "enabled linger for $user (timers fire without login)"
|
||||
else
|
||||
warn "run once so timers fire without login: sudo loginctl enable-linger $user"
|
||||
fi
|
||||
}
|
||||
Reference in New Issue
Block a user