feat(cron): Implement cron scheduling for daily and q4h presets with logging and locking

This commit is contained in:
Nik Afiq 2026-07-16 13:29:25 +09:00
parent 0946ce4f65
commit 5a78e8ef11
3 changed files with 86 additions and 0 deletions

View File

@ -232,6 +232,22 @@ rsync -av \
Do not sync Claude Code internal temporary folders.
**The rsync target and the runtime paths are two different locations that can drift.** `rsync` above only updates the git checkout at `~/repo/ba-auto-daily/` on nik-gpu -- it does NOT touch `~/ba_dailies.sh`/`~/ba_daily.py`/`~/ba_auto/`, the actual fixed paths cron (see below) and any manually-run `~/ba_dailies.sh` invocation use. After rsyncing code changes, re-run `./setup.sh` from within `~/repo/ba-auto-daily/` on nik-gpu to propagate them to the runtime paths (it's idempotent -- safe to re-run any time, also re-checks host tools/venv/assets). Confirmed live (2026-07-16): the two locations had drifted (runtime paths were missing a same-day `ba_daily.py` edit) before this was caught and fixed by re-running `setup.sh`.
### Scheduled runs (cron)
As of 2026-07-16, `ba_dailies.sh` presets run unattended on a schedule via cron on nik-gpu, not just via manual/CLI invocation. `crontab -l` on nik-gpu is the source of truth; as installed:
```text
30 3 * * * /home/nik/ba_cron_run.sh daily
30 4 * * * /home/nik/ba_cron_run.sh daily
0 1,5,9,13,17,21 * * * /home/nik/ba_cron_run.sh q4h
```
Times are nik-gpu's local system time (`Asia/Tokyo`/JST). `ba_cron_run.sh` (repo root, deployed to `~/ba_cron_run.sh`) wraps `~/ba_dailies.sh <preset>` in a shared, non-blocking `flock` (`~/ba_logs/ba_dailies.lock`) so overlapping fire times can never launch concurrent `xdotool`/`scrot` sessions against the same game window, and logs each run to its own `~/ba_logs/<preset>.log` (`daily.log`/`q4h.log`, never interleaved). It contains no game-automation logic -- pure locking/logging plumbing, same category as `ba_dailies.sh` itself. To check whether a scheduled run actually worked: `grep -E 'FAILED|SKIPPED' ~/ba_logs/*.log` -- every run logs one of `OK`/`FAILED (exit N)`/`SKIPPED (previous run still in progress)`, deliberately distinct outcomes (a lock-skip and a genuinely crashed task both exit 1, so the wrapper checks lock acquisition as its own explicit step rather than folding it into the wrapped command's exit code).
This means: resources (AP, tickets, currency) may already be spent by the time a session starts, independent of anything done in that session -- check `~/ba_logs/` before assuming a given game state is from manual testing. The `daily`/`q4h` preset definitions themselves live in `ba_daily.py`'s `PRESETS` dict (see `plan.md`'s Phase 18 follow-ups for the full writeup), not here -- keep this section in sync if the schedule or preset names change.
## Runtime dependencies on nik-gpu
These are host-level dependencies. Confirm they exist before assuming a bug is in the project code.

45
ba_cron_run.sh Normal file
View File

@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Cron entry point for ba_dailies.sh presets. Pure process-locking/logging
# plumbing -- no game-automation logic, same "thin wrapper" spirit as
# ba_dailies.sh itself (see CLAUDE.md's Bash policy).
#
# The shared flock lock exists because "daily" fires twice a day (3:30 and
# 4:30) and "q4h" fires every 4 hours -- without a lock, two overlapping
# invocations would both drive xdotool/scrot against the same game window
# at once, which is unsafe. -n (non-blocking): if a previous run is still
# in progress, this invocation is skipped entirely rather than queued, so
# a scheduled fire time can never silently run late.
#
# Logs to ~/ba_logs/<preset>.log -- one file per preset, so "daily" and
# "q4h" never interleave. Every line is one of three explicitly-tagged
# outcomes (OK / FAILED / SKIPPED) specifically so a real failure can be
# found with a single command, e.g.:
# grep -E 'FAILED|SKIPPED' ~/ba_logs/*.log
# A bare exit code alone can't do this: flock and a genuinely crashed task
# both exit 1, so lock acquisition is checked as its own explicit step
# (via an fd-based flock, not the "flock -n LOCKFILE COMMAND" form used
# originally) rather than folded into the wrapped command's own exit code.
set -uo pipefail
PRESET="${1:?usage: ba_cron_run.sh <preset>}"
LOG_DIR="$HOME/ba_logs"
LOG_FILE="$LOG_DIR/$PRESET.log"
LOCK_FILE="$LOG_DIR/ba_dailies.lock"
mkdir -p "$LOG_DIR"
{
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
echo "=== $(date -Iseconds) SKIPPED $PRESET -- previous run still in progress ==="
exit 0
fi
echo "=== $(date -Iseconds) starting $PRESET ==="
"$HOME/ba_dailies.sh" "$PRESET"
status=$?
if [ "$status" -eq 0 ]; then
echo "=== $(date -Iseconds) finished $PRESET OK (exit 0) ==="
else
echo "=== $(date -Iseconds) finished $PRESET FAILED (exit $status) ==="
fi
} >> "$LOG_FILE" 2>&1

25
plan.md
View File

@ -802,6 +802,31 @@ Per CLAUDE.md's Bash policy (`ba_dailies.sh` is a thin launcher only, no loops/s
**Verified on nik-gpu without spending anything**: `--list-commands` includes `daily`; requesting a bogus phase name correctly lists both valid phases and valid presets and exits 1. The `daily` preset's actual task sequence (which includes `event_sweep`/`arena`/`lesson`/`bounty`/`shop_common`/`shop_tactical`, all real-resource-spending) has not been run for real yet -- needs explicit user go-ahead before that first live run, per this project's own established live-testing discipline for resource-spending tasks.
The user then added a second preset directly (`"q4h": ["login", "cafe", "mailbox", "event_sweep"]`) and asked for actual `cron` scheduling: `daily` at 3:30 AM and 4:30 AM every day, `q4h` every 4 hours starting at 5:00 AM.
### Phase 18 follow-up #2: cron scheduling (2026-07-16)
Deployed a fresh copy first: the repo checkout (`~/repo/ba-auto-daily/`, where this session's rsyncs land) and the "fixed runtime paths" `setup.sh` copies to (`~/ba_dailies.sh`/`~/ba_daily.py`/`~/ba_auto/`, what CLAUDE.md documents as the actual invocation target) had drifted -- the runtime copy predated the user's own `q4h` edit. Re-ran `setup.sh` on nik-gpu to resync them (idempotent, confirmed via `diff -rq`) before pointing cron at `~/ba_dailies.sh`, rather than cron silently running a stale copy.
Added `ba_cron_run.sh` (repo root, deployed to `~/ba_cron_run.sh`) as the actual cron entry point instead of calling `~/ba_dailies.sh <preset>` directly. It is pure process-locking/logging plumbing -- no game-automation logic, same "thin wrapper" spirit CLAUDE.md's Bash policy already holds `ba_dailies.sh` to. Two things it adds that a bare crontab line can't:
1. **A shared `flock -n` lock** (`~/ba_logs/ba_dailies.lock`) across every preset invocation. `daily` fires twice an hour apart, and `q4h` fires independently every 4 hours -- without a lock, an overlapping fire time would launch a second concurrent `xdotool`/`scrot` session against the same game window while the first was still mid-navigation, an unsafe race this project has never needed to guard against before now (every previous invocation was a single manual/CLI run). `-n` (non-blocking): if a previous run is still going, the new one is skipped entirely rather than queued -- deliberately, since a queued-and-delayed run could land at an arbitrary later time with no relation to its intended fire time.
2. **Timestamped logging** to `~/ba_logs/<preset>.log` (start/finish lines wrapping the actual `ba_dailies.sh` output and exit code), since a cron job's own stdout/stderr otherwise either gets mailed (if a local MTA is configured, not assumed here) or silently dropped.
**Crontab installed** (`crontab -l` on nik-gpu):
```
30 3 * * * /home/nik/ba_cron_run.sh daily
30 4 * * * /home/nik/ba_cron_run.sh daily
0 1,5,9,13,17,21 * * * /home/nik/ba_cron_run.sh q4h
```
Times are nik-gpu's local system time, confirmed `Asia/Tokyo` (JST) via `timedatectl` -- matters since the user's chosen 3:30 fire time isn't arbitrary, it lines up with the reference project's own JP-server restart-check window (`module/restart.py`'s `check_need_restart` checks around hour=3), likely intentional on the user's part to mop up anything left just before the daily reset, with the 4:30 fire catching the freshly-reset dailies an hour later.
**Verified on nik-gpu without touching the game**: the cron daemon is active (`systemctl is-active cron` -> `active`, `cron` process confirmed running); `ba_cron_run.sh` invoked directly with a bogus preset name correctly logged timestamped start/finish lines wrapping `ba_daily.py`'s own "Unknown phase" error and exit code; a deliberately-forced lock-contention test (holding the lock via a background `flock ... sleep 5`, then invoking the wrapper again) confirmed the second invocation logged start/finish with *no* output in between -- i.e. it correctly skipped running `ba_dailies.sh` at all while the lock was held, rather than racing it.
**Not yet verified**: an actual scheduled fire (3:30/4:30/the next `q4h` slot) has not yet happened live, so the real end-to-end cron path (crond -> `ba_cron_run.sh` -> `ba_dailies.sh daily`/`q4h` -> real task execution) is confirmed piece-by-piece but not yet as one unattended whole. Also flagged, not fixed (outside this change's scope): whether nik-gpu's GDM desktop session (`DISPLAY=:0`) is guaranteed active at 3:30 AM unattended -- if the machine sleeps, locks in a way that tears down the X session, or otherwise isn't logged in at fire time, `xdotool`/`scrot` would fail and the run would just log an error with nothing to click. Worth checking nik-gpu's power/session settings, or watching the first few real unattended fires' logs, rather than assuming.
**Follow-up, same day**: per explicit user request ("I also want to have logs for the cron. So I can detect if anythings go wrong. daily and q4h should have separate .log file") -- the per-preset log file separation was already in place (`~/ba_logs/<preset>.log`, one file per `$PRESET`), but a real gap was found in the failure-detection half of the ask: a genuinely crashed task and a lock-skip (an overlapping fire time, expected/harmless) both exit 1, and the original wrapper logged only a bare `(exit N)`, making them indistinguishable at a glance. Fixed by switching from the `flock -n LOCKFILE COMMAND` form to an fd-based `flock` (`exec 9>"$LOCK_FILE"; flock -n 9`), checking lock acquisition as its own explicit step separate from the wrapped command's own exit code, so every log line is now tagged with one of three explicit outcomes: `starting`/`finished ... OK (exit 0)`/`finished ... FAILED (exit N)`/`SKIPPED ... -- previous run still in progress` -- a real problem can now be found with `grep -E 'FAILED|SKIPPED' ~/ba_logs/*.log` alone, without having to reason about ambiguous exit codes. **Verified on nik-gpu without touching the game**: a bogus-preset run correctly logged `FAILED (exit 1)`; a deliberately-forced lock-contention run (same technique as the original test) correctly logged `SKIPPED ... -- previous run still in progress` instead of a second ambiguous `(exit 1)`; the grep command above correctly surfaced both lines from `~/ba_logs/smoke_test_bad.log`. Test log removed afterward -- `~/ba_logs/` currently contains no `daily.log`/`q4h.log` yet, since no real scheduled fire has happened.
## Prerequisites
### OCR