Commit challs.txt once a day at 04:00, separate from the HedgeDoc refresh
The 30-minute timer now only updates the HedgeDoc note; a new ctf-challs.timer runs --challs daily at 04:00. Locks are per target so the two runs can overlap without one skipping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -74,7 +74,14 @@ deployment project. If the branch is protected, the token's user must be allowed
|
|||||||
to push to it. Without `CHALLS_BRANCH`, the default branch is looked up, which
|
to push to it. Without `CHALLS_BRANCH`, the default branch is looked up, which
|
||||||
also needs **Project: Read** there.
|
also needs **Project: Read** there.
|
||||||
|
|
||||||
## Every 30 minutes, on a server
|
## On a schedule, on a server
|
||||||
|
|
||||||
|
Two timers, independent of each other:
|
||||||
|
|
||||||
|
| timer | when | runs |
|
||||||
|
|---|---|---|
|
||||||
|
| `ctf-status.timer` | every 30 minutes | `--update` (HedgeDoc note) |
|
||||||
|
| `ctf-challs.timer` | daily at 04:00 (server local time) | `--challs` (challs.txt commit) |
|
||||||
|
|
||||||
Copy the repo to the server, create `ctf_status.env` or `.env` there (both are gitignored, so they
|
Copy the repo to the server, create `ctf_status.env` or `.env` there (both are gitignored, so they
|
||||||
do not travel with the code), then:
|
do not travel with the code), then:
|
||||||
@@ -84,27 +91,30 @@ do not travel with the code), then:
|
|||||||
```
|
```
|
||||||
|
|
||||||
It writes systemd **user** units into `~/.config/systemd/user/`, substituting
|
It writes systemd **user** units into `~/.config/systemd/user/`, substituting
|
||||||
whatever directory the repo actually sits in, enables the timer, turns on lingering
|
whatever directory the repo actually sits in, enables both timers, turns on lingering
|
||||||
so it keeps firing when you are not logged in, and does one run to prove it works.
|
so they keep firing when you are not logged in, and does one run of each to prove they work.
|
||||||
Re-run it any time; it just overwrites the units.
|
Re-run it any time; it just overwrites the units.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
systemctl --user list-timers ctf-status.timer # when it next fires
|
systemctl --user list-timers 'ctf-*' # when they next fire
|
||||||
journalctl --user -u ctf-status.service -n 50 # what it did
|
journalctl --user -u ctf-status.service -n 50 # what the HedgeDoc run did
|
||||||
systemctl --user start ctf-status.service # run it now
|
journalctl --user -u ctf-challs.service -n 50 # what the challs.txt run did
|
||||||
systemctl --user disable --now ctf-status.timer # stop it
|
systemctl --user start ctf-challs.service # commit challs.txt now
|
||||||
|
systemctl --user disable --now ctf-challs.timer # stop the daily commit
|
||||||
```
|
```
|
||||||
|
|
||||||
The timer fires at `:00` and `:30` with up to a minute of jitter, and
|
The HedgeDoc timer fires at `:00` and `:30` with up to a minute of jitter; both
|
||||||
`Persistent=true` catches up a run missed while the machine was down. Plain cron
|
timers have `Persistent=true`, so a run missed while the machine was down is caught up.
|
||||||
works too, if you would rather:
|
Plain cron works too, if you would rather:
|
||||||
|
|
||||||
```
|
```
|
||||||
*/30 * * * * /path/to/planner/ctf_status.py --update --challs --quiet
|
*/30 * * * * /path/to/planner/ctf_status.py --update --quiet
|
||||||
|
0 4 * * * /path/to/planner/ctf_status.py --challs --quiet
|
||||||
```
|
```
|
||||||
|
|
||||||
`--quiet` keeps a successful run silent, so you only hear about it when the note
|
`--quiet` keeps a successful run silent, so you only hear about it when something
|
||||||
changed or something broke. Overlapping runs are prevented by a lock file, and the
|
changed or broke. Overlapping runs of the same target are prevented by a lock file
|
||||||
|
per target (the 04:00 challs run and the HedgeDoc run can overlap), and the
|
||||||
HedgeDoc write is retried (`--retries`, default 3) before the run gives up with a
|
HedgeDoc write is retried (`--retries`, default 3) before the run gives up with a
|
||||||
one-line error and a non-zero exit.
|
one-line error and a non-zero exit.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Commit the testable challenges to challs.txt in the deployment repo
|
||||||
|
Documentation=file://__DIR__/README.md
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=__DIR__
|
||||||
|
ExecStart=__DIR__/ctf_status.py --challs --quiet
|
||||||
|
TimeoutStartSec=5min
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Refresh challs.txt in the deployment repo once a day at 04:00
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* 04:00:00
|
||||||
|
AccuracySec=1min
|
||||||
|
# catch up on the last run if the machine was down when it was due
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Push the CTF challenge status to HedgeDoc and challs.txt
|
Description=Push the CTF challenge status to HedgeDoc
|
||||||
Documentation=file://__DIR__/README.md
|
Documentation=file://__DIR__/README.md
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
@@ -7,6 +7,6 @@ Wants=network-online.target
|
|||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
WorkingDirectory=__DIR__
|
WorkingDirectory=__DIR__
|
||||||
ExecStart=__DIR__/ctf_status.py --update --challs --quiet
|
ExecStart=__DIR__/ctf_status.py --update --quiet
|
||||||
# GitLab walk + HedgeDoc write; never let one run squat on the next timer slot
|
# GitLab walk + HedgeDoc write; never let one run squat on the next timer slot
|
||||||
TimeoutStartSec=5min
|
TimeoutStartSec=5min
|
||||||
|
|||||||
+8
-6
@@ -690,13 +690,14 @@ def push_hedgedoc(base: str, md: str) -> str:
|
|||||||
return loc if loc.startswith("http") else base.rstrip("/") + loc
|
return loc if loc.startswith("http") else base.rstrip("/") + loc
|
||||||
|
|
||||||
|
|
||||||
def single_run_lock():
|
def single_run_lock(target: str):
|
||||||
"""Keep a slow or hung cron run from colliding with the next one."""
|
"""Keep a slow or hung cron run from colliding with the next one. One lock per
|
||||||
fh = open(Path(tempfile.gettempdir()) / "ctf_status.lock", "w")
|
target, so the daily challs.txt run and the HedgeDoc refresh can overlap."""
|
||||||
|
fh = open(Path(tempfile.gettempdir()) / f"ctf_status-{target}.lock", "w")
|
||||||
try:
|
try:
|
||||||
fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
except OSError:
|
except OSError:
|
||||||
sys.exit("another ctf_status.py run is still going; skipping this one")
|
sys.exit(f"another ctf_status.py {target} run is still going; skipping this one")
|
||||||
return fh # held until the process exits
|
return fh # held until the process exits
|
||||||
|
|
||||||
|
|
||||||
@@ -742,8 +743,9 @@ def main() -> None:
|
|||||||
sys.exit(str(e))
|
sys.exit(str(e))
|
||||||
if args.challs and not cfg.get("CHALLS_REPO_URL"):
|
if args.challs and not cfg.get("CHALLS_REPO_URL"):
|
||||||
sys.exit("--challs needs CHALLS_REPO_URL in the config")
|
sys.exit("--challs needs CHALLS_REPO_URL in the config")
|
||||||
if note_url or args.challs:
|
# held for the lifetime of the process
|
||||||
lock = single_run_lock() # noqa: F841 (held for the lifetime of the process)
|
locks = [single_run_lock(t) for t, on in (("hedgedoc", note_url), ("challs", args.challs))
|
||||||
|
if on] # noqa: F841
|
||||||
|
|
||||||
gl = GitLab(cfg["GITLAB_GROUP_URL"], cfg["GITLAB_TOKEN"])
|
gl = GitLab(cfg["GITLAB_GROUP_URL"], cfg["GITLAB_TOKEN"])
|
||||||
try:
|
try:
|
||||||
|
|||||||
+8
-7
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
# Install the 30-minute HedgeDoc refresh as systemd *user* units.
|
# Install the 30-minute HedgeDoc refresh and the daily 04:00 challs.txt commit
|
||||||
|
# as systemd *user* units.
|
||||||
# Run it as the user that owns ctf_status.env / .env, on the machine that should do the
|
# Run it as the user that owns ctf_status.env / .env, on the machine that should do the
|
||||||
# polling. Re-running it is safe; it just overwrites the units.
|
# polling. Re-running it is safe; it just overwrites the units.
|
||||||
set -eu
|
set -eu
|
||||||
@@ -16,20 +17,20 @@ done
|
|||||||
[ $# -gt 0 ] || {
|
[ $# -gt 0 ] || {
|
||||||
echo "no ctf_status.env or .env in $dir — copy ctf_status.env.example to one first" >&2
|
echo "no ctf_status.env or .env in $dir — copy ctf_status.env.example to one first" >&2
|
||||||
exit 1; }
|
exit 1; }
|
||||||
# the timer runs --update --challs, which need these two
|
# the timers run --update and --challs, which need these two
|
||||||
for key in HEDGEDOC_NOTE CHALLS_REPO_URL; do
|
for key in HEDGEDOC_NOTE CHALLS_REPO_URL; do
|
||||||
grep -qi "^$key=..*" "$@" || { echo "set $key=... in $* first" >&2; exit 1; }
|
grep -qi "^$key=..*" "$@" || { echo "set $key=... in $* first" >&2; exit 1; }
|
||||||
done
|
done
|
||||||
chmod 600 "$@"
|
chmod 600 "$@"
|
||||||
|
|
||||||
mkdir -p "$units"
|
mkdir -p "$units"
|
||||||
for u in ctf-status.service ctf-status.timer; do
|
for u in ctf-status.service ctf-status.timer ctf-challs.service ctf-challs.timer; do
|
||||||
sed "s|__DIR__|$dir|g" "$dir/$u" > "$units/$u"
|
sed "s|__DIR__|$dir|g" "$dir/$u" > "$units/$u"
|
||||||
echo "installed $units/$u"
|
echo "installed $units/$u"
|
||||||
done
|
done
|
||||||
|
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
systemctl --user enable --now ctf-status.timer
|
systemctl --user enable --now ctf-status.timer ctf-challs.timer
|
||||||
|
|
||||||
# without lingering, user units are torn down as soon as you log out
|
# without lingering, user units are torn down as soon as you log out
|
||||||
if ! loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null | grep -qx yes; then
|
if ! loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null | grep -qx yes; then
|
||||||
@@ -38,6 +39,6 @@ if ! loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null | grep -qx yes
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "one run now, to prove it works:"
|
echo "one run of each now, to prove they work:"
|
||||||
systemctl --user start ctf-status.service && echo " ok"
|
systemctl --user start ctf-status.service ctf-challs.service && echo " ok"
|
||||||
systemctl --user list-timers ctf-status.timer --no-pager
|
systemctl --user list-timers ctf-status.timer ctf-challs.timer --no-pager
|
||||||
|
|||||||
Reference in New Issue
Block a user