screen-locker/scripts/runnerup_watch.sh

103 lines
3.7 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# Credit a RunnerUp run the moment its TCX upload lands on this machine.
#
# RunnerUp PUTs each export to dufs, which writes it straight into
# ~/data/cloud/RunnerUp: CREATE fires when the upload starts, CLOSE_WRITE when
# it ends. So this reacts to close_write/moved_to only -- a systemd .path unit
# (PathChanged=) fires on CREATE, hands the pass a half-written TCX and drops
# the close event while that pass runs, which left a 2026-10-07 run uncredited
# until the next timer tick.
#
# Each finished *.tcx starts workout-sync.service (blocking), which ingests it
# and applies the shutdown credit. systemd serialises that against the 15-min
# timer run; when one was already in flight it started before this file was
# complete, so the start merged into it and a second start is needed.
#
# Only a FRESH top-level *.tcx counts. endurain-import's adb fallback re-pulls
# every phone export it does not see at the top level -- including the ones it
# already moved into processed/ -- and renames each into place, so every
# 15-min import used to fire ~27 moved_to events and as many workout-sync
# starts (~370 on 2026-10-09). A name already in processed/ was handled
# before, and dotfiles are partial transfers; both are skipped with zero forks.
#
# Run by runnerup-watch.service (Restart=always). Event-driven: no polling.
# ============================================================================
set -euo pipefail
readonly WATCH_DIR="${RUNNERUP_WATCH_DIR:-$HOME/data/cloud/RunnerUp}"
readonly SYNC_UNIT="${RUNNERUP_SYNC_UNIT:-workout-sync.service}"
readonly PROCESSED_DIR="$WATCH_DIR/processed"
log() {
printf '%s\n' "$*" >&2
}
start_sync() {
if ! systemctl --user start "$SYNC_UNIT"; then
log "WARNING: $SYNC_UNIT failed after a RunnerUp upload — the run" \
"is NOT credited yet; see journalctl --user -u $SYNC_UNIT." \
"The 15-min timer retries it."
return 1
fi
}
ingest() {
local name="$1" state
state="$(systemctl --user is-active "$SYNC_UNIT" || true)"
log "RunnerUp upload finished: $name — starting $SYNC_UNIT"
start_sync || return 0
if [[ $state == activating ]]; then
log "$SYNC_UNIT was already running when $name landed — running it again"
start_sync || return 0
fi
}
# is_fresh_upload NAME -- true for a new top-level TCX, false for anything
# endurain-import already handled (its copy is in processed/) or a partial.
# Builtins only: this runs once per inotify event.
is_fresh_upload() {
local name="$1"
[[ $name == *.tcx && $name != .* ]] || return 1
[[ ! -e $PROCESSED_DIR/$name ]]
}
validate_requirements() {
if ! command -v inotifywait >/dev/null 2>&1; then
log "ERROR: inotifywait missing (pacman -S inotify-tools) — RunnerUp" \
"uploads are only credited by the 15-min timer"
exit 1
fi
if [[ ! -d $WATCH_DIR ]]; then
log "ERROR: $WATCH_DIR does not exist — is dufs-cloud set up?" \
"RunnerUp uploads are only credited by the 15-min timer"
exit 1
fi
}
main() {
validate_requirements
log "Watching $WATCH_DIR for finished RunnerUp TCX uploads"
local name now last_skip_log=0
while IFS= read -r name; do
if ! is_fresh_upload "$name"; then
[[ $name == *.tcx ]] || continue
# One line per burst, not per file: the re-pull lands ~27 at once.
printf -v now '%(%s)T' -1
if ((now - last_skip_log > 60)); then
log "Ignoring re-pulled $name (already in processed/; no" \
"workout-sync) — further re-pulls this minute not logged"
last_skip_log=$now
fi
continue
fi
ingest "$name"
done < <(inotifywait -m -q -e close_write -e moved_to --format '%f' "$WATCH_DIR")
log "ERROR: inotifywait on $WATCH_DIR exited — restarting via systemd"
exit 1
}
main "$@"