# Sourced by every dev-*.sh: `--watch` reruns the script when the source moves. # # Not a command anyone runs. `. scripts/dev-watch.sh` near the top of a script, # and then `watch_or_continue "$@"` before it does any work. # # # Why it reruns the script rather than the binary # # Each of these scripts is a sequence — rebuild, run that half's tests, print a # banner, then run the thing in the foreground — and every step of that # sequence is what a developer wants repeated. A watcher that restarted only # the final process would skip the tests, which is the step most likely to have # something to say about the change that triggered it. # # So the loop spawns the script again, without `--watch`, and kills that child # when something moves. The child is the whole sequence. # # # Why polling # # `inotifywait` is not installed on every machine this runs on, and a second of # latency on a rebuild that takes fourteen is not worth a dependency that has # to be present. The poll walks `crates/` and the workspace manifest, which is # a few hundred files and costs nothing next to what it triggers. # How often to look, in seconds. DIDBOT_WATCH_INTERVAL="${DIDBOT_WATCH_INTERVAL:-1}" # Whether `--watch` appears in the arguments. _didbot_wants_watch() { local arg for arg in "$@"; do [ "$arg" = "--watch" ] && return 0 done return 1 } # The arguments with `--watch` taken out. _didbot_without_watch() { local arg for arg in "$@"; do [ "$arg" = "--watch" ] || printf '%s\n' "$arg" done } # Every file whose modification time the loop is watching, newest first. _didbot_newest() { find crates Cargo.toml Cargo.lock -type f \ \( -name '*.rs' -o -name 'Cargo.toml' -o -name 'Cargo.lock' -o -name '*.json' \) \ -printf '%T@\n' 2>/dev/null | sort -rn | head -1 } # Stops one run, and everything it started. # # Every step here ends in `|| true`, and it has to. These scripts run under # `set -e`, killing a process makes `wait` report the signal that killed it, # and a `wait` whose status nobody tolerates takes the watcher down with the # thing it was restarting — which looks exactly like a watcher that works once # and then stops, because that is what it is. It did. _didbot_stop() { local child="${1:-0}" if [ "$child" -eq 0 ]; then return 0 fi # The group first, then the process: the group covers a `cargo` the shell # is waiting on, and the fallback covers a run that has already `exec`ed # into the server and is a group of one. kill -TERM -- "-$child" 2>/dev/null || kill -TERM "$child" 2>/dev/null || true wait "$child" 2>/dev/null || true return 0 } # Runs the script again on every change, until interrupted. # # Call it with the script's own arguments. It returns immediately when # `--watch` is not among them, so a script can call it unconditionally. watch_or_continue() { _didbot_wants_watch "$@" || return 0 local rest=() while IFS= read -r arg; do rest+=("$arg"); done < <(_didbot_without_watch "$@") # Job control, so that each run is its own process group and can be killed # as one. Without it a kill during the build phase reaches the shell and # leaves the `cargo` it was waiting on behind, holding the target lock the # next run needs. set -m local child=0 # Ctrl-C is how a person stops watching, and it has to take the run with # it: a loop that left a server holding the port would make the next run # fail with an address already in use. trap '_didbot_stop "$child"; exit 0' INT TERM local stamp stamp="$(_didbot_newest)" while true; do printf '\033[1m%s\033[0m\n' "watching crates/ — Ctrl-C to stop" "$0" "${rest[@]}" & child=$! # Wait for either a change or the child giving up. A child that exits # on its own is usually a compile error, and the loop stays up so that # fixing the error is what restarts it. while true; do sleep "$DIDBOT_WATCH_INTERVAL" local now now="$(_didbot_newest)" if [ "$now" != "$stamp" ]; then stamp="$now" break fi done printf '\033[1m%s\033[0m\n' "something moved; restarting" _didbot_stop "$child" child=0 done }