| name | waitfor |
| description | Pause a shell script or CI step until a duration elapses and/or events are satisfied (process alive/gone, file exists/absent), with an optional deadline that turns the wait into a hard timeout. |
waitfor — Skill Reference
waitfor is a small Rust CLI for synchronization in scripts. It is a strict
superset of sleep: the same waitfor 5s behaves identically, but additional
flags let the wait depend on external events and a deadline.
When to reach for it
Use waitfor instead of ad-hoc sleep / while kill -0 / until [ -f ... ]
loops when any of these apply:
- You need to wait for a process to exit (e.g. graceful shutdown polling).
- You need to wait for a file to appear or disappear (pidfiles, ready
flags, lockfiles, build artifacts).
- You need a bounded wait — the script must give up after N seconds
with a clear non-zero exit instead of hanging forever.
- You need multiple conditions combined with all-of / any-of semantics.
If you just need a fixed sleep, waitfor 5s is fine, but sleep 5 works too.
CLI surface
waitfor [OPTIONS] [DURATION]...
| Argument / flag | Effect |
|---|
DURATION... | Humantime durations (5s, 1m30s, 2h) or bare numbers as seconds (5, 1.25); summed. |
--pid <N> | Condition: process N is alive. |
--no-pid <N> | Condition: process N is gone. |
--file <PATH> | Condition: PATH exists. |
--no-file <PATH> | Condition: PATH does not exist. |
--deadline <DUR> | Hard cap on total wait. Exit 1 if reached. |
--interval <DUR> | Poll interval for events (default 100ms). |
--any | Exit on the first satisfied condition. |
-v, --verbose | Echo conditions + elapsed time to stderr. |
Each --pid / --no-pid / --file / --no-file may be repeated; the
combined set follows the chosen mode (all-of by default, any-of with --any).
Exit codes
| Code | Meaning |
|---|
0 | All conditions satisfied (or first one in --any). |
1 | --deadline exceeded before completion. |
2 | Usage error: no conditions, bad duration, etc. |
The deadline path is the only way waitfor returns 1, so scripts can
distinguish "happened" vs "timed out" without parsing stderr:
if waitfor --no-pid "$pid" --deadline 30s; then
echo "process exited cleanly"
else
echo "process still alive after 30s — escalating"
kill -9 "$pid"
fi
Semantics worth knowing
- Default mode is all-of. Every duration and every event must be satisfied.
waitfor 5s --no-pid 1234 waits at least 5 seconds AND for pid 1234 to die.
--any is or-of. First duration-elapsed or event-satisfied wins.
- Deadline is orthogonal to all-of / any-of. It is always a hard upper
bound; reaching it is exit
1.
- PID checks use
kill(pid, 0), so they detect any process the caller
could signal — including those owned by other users (treated as "alive but
EPERM"). PIDs <= 0 are rejected.
- Polling cap. The poll interval is auto-clamped to the remaining duration
and remaining deadline, so timing precision is bounded by
--interval
(~100ms by default) — not by overshoot at the end.
- Pure-sleep fast path.
waitfor 5s with no events and no deadline calls
thread::sleep once — it does not poll.
- Signals. Default Rust handlers; Ctrl-C exits 130.
Cookbook
Wait for a daemon's pidfile to show up, then a 2-second warmup, with a 10s cap:
waitfor --file /run/myapp.pid 2s --deadline 10s || {
echo "myapp failed to come up"; exit 1; }
Graceful shutdown with escalation:
kill -TERM "$pid"
waitfor --no-pid "$pid" --deadline 15s || kill -KILL "$pid"
Wait for either a result file or a five-minute ceiling, whichever first:
waitfor 5m --file /tmp/result.json --any
Wait for one process to exit and another to start, in sequence:
waitfor --no-pid "$old_pid" --deadline 30s && \
waitfor --pid "$new_pid" --deadline 30s
Debug a flaky CI wait:
waitfor --verbose --file /tmp/ready --no-pid "$bootstrap_pid" --deadline 2m
When NOT to use it
- Network readiness (TCP port open, HTTP 200) — out of scope; use
wait-for-it, dockerize, or a healthcheck loop. waitfor deliberately
has no network probes.
- PID-reuse-sensitive correctness — if a PID can be recycled before the
next poll, no PID-only tool can be correct. Prefer pidfiles, cgroups, or
shell job control (
wait $!) for tight loops.
- Sub-millisecond timing — polling is bounded by
--interval.
Implementation pointers
- Rust 2021, single binary at
src/main.rs.
- Deps:
clap (derive), humantime, libc.
- The polling loop lives in
main() after the fast-path sleep; events are
evaluated as a single Vec<bool> and combined per the --any flag.