# Sourced by the dev-*.sh scripts that need to replace a previous run. # # The thing this exists to stop: `pkill -f "didbot-dev --port 3000"` # matches any process whose *arguments* contain that string. That includes # another developer's server, an editor with the command in a buffer, and — on # a bad day — the shell running the script, which is a `kill` that takes out # the thing doing the killing. It has happened. # # A pidfile names one process. Before signalling it, /proc is consulted to # confirm the process is still the binary the file was written for: a pid is # reused, and a stale file must never be a licence to kill whatever inherited # its number. # Where a service's pidfile lives, keyed by name and port so that two stacks on # one machine never collide. _didbot_pidfile() { local base="${XDG_RUNTIME_DIR:-/tmp}/didbot" mkdir -p "$base" 2>/dev/null || true echo "$base/$1-$2.pid" } # Whether a pid is alive and is still running the expected binary. _didbot_is() { local pid="$1" binary="$2" [ -n "$pid" ] || return 1 kill -0 "$pid" 2>/dev/null || return 1 # The kernel's own answer, not a pattern over a rendered command line. # /proc//exe is the binary that is actually executing. local exe exe="$(readlink "/proc/$pid/exe" 2>/dev/null)" || return 1 case "$exe" in *"/$binary") return 0 ;; *"/$binary (deleted)") return 0 ;; *) return 1 ;; esac } # Stop a previous run of this service, if there is one, and claim the pidfile. # # claim_port # # Called before the build so that a port held by a previous run is released # early rather than after a minute of compiling. claim_port() { local name="$1" port="$2" binary="$3" local file previous file="$(_didbot_pidfile "$name" "$port")" if [ -r "$file" ]; then previous="$(cat "$file" 2>/dev/null)" if _didbot_is "$previous" "$binary"; then printf '\033[1m%s\033[0m\n' "stopping the previous $name on port $port (pid $previous)" kill "$previous" 2>/dev/null || true # Give it a moment to release the port, then insist. local waited=0 while kill -0 "$previous" 2>/dev/null && [ "$waited" -lt 50 ]; do sleep 0.1 waited=$((waited + 1)) done kill -9 "$previous" 2>/dev/null || true fi rm -f "$file" fi # This shell's pid, because every one of these scripts ends in `exec`: the # process that finally holds the port is this one, with a different # program running in it. echo "$$" > "$file" } # A stale pidfile is expected, and is not cleaned up. # # There used to be an `EXIT` trap here to remove it, and it never ran once: # every one of these scripts ends in `exec`, which replaces the shell with the # server and takes the trap with it. A trap that reads as if it tidies up and # does not is worse than no trap, because the next person to see a leftover # file goes looking for the bug in the wrong place. # # It costs nothing to leave. `claim_port` reads `/proc//exe` before it # signals anything, so a file naming a process that is gone — or one whose # number has since been reused by something else — is ignored and overwritten. # The file is a hint about who to ask, never a licence to kill.