mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-25 14:00:40 +00:00
d4ec587de9
install-refresh.sh stops the server, installs, and restarts it with the
captured argv. Any pad CLI call in between found nothing answering and
auto-started a server of its own: the caller's binary, cwd and default
flags. That server held the port against the refresh's restart (day 79:
127.0.0.1:7777 went dark and the script exited 1).
- The script writes <DataDir>/refresh.lock {pid, port, deadline} by rename
before the stop and removes it in its EXIT trap, only while it is still
its own. The deadline is 10s + PAD_PROBE_TIMEOUT (30s by default).
- EnsureServer defers to a live marker for its own port (writer pid alive
AND deadline not passed): it polls health and never spawns. If the
writer is still alive past the deadline it errors naming the pid. A dead
writer, a passed deadline, another port or an unparseable file means
the marker is ignored.
- The script names a holder that took the port before the restart spawns
(and does not spawn it), and refuses success when the listener after
the probe is not the restart it spawned. A health probe answers for
whoever holds the address.
- The script-test dependency guard now reads every install_refresh*_test.go.
Claude-Session: https://claude.ai/code/session_01A3rXV7GDLAhBe9mL4V6z2m
896 lines
39 KiB
Bash
Executable File
896 lines
39 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# install-refresh.sh — the honest half of `make install` (BUG-2897, TASK-2787).
|
|
#
|
|
# The target used to: kill the running pad, cp the binary, then bring the
|
|
# server back BY SIDE EFFECT (`pad auth whoami` triggers an auto-start) and
|
|
# print "Server restarted." unconditionally. Three things were wrong with
|
|
# that, and the third is the one that makes the other two invisible:
|
|
#
|
|
# 1. The auto-start does not know the killed process's argv, so a server
|
|
# running `--host 0.0.0.0` came back bound to the default host alone.
|
|
# Measured: curl 127.0.0.1:7777 -> 000 while the LAN address -> 200,
|
|
# with the process count and the version both reading correct.
|
|
# 2. `cp` copies whatever is at the repo path, not what THIS invocation
|
|
# built. Two sessions sharing the checkout interleave, and the loser's
|
|
# build is installed by the winner with every exit code green.
|
|
# 3. "Server restarted." was printed after a command ending in `|| true`,
|
|
# with no probe of any kind. It is not that the wrong address went
|
|
# unverified — nothing was verified.
|
|
#
|
|
# So this script checks OUTCOMES, not steps: what got installed, and what is
|
|
# answering afterwards.
|
|
#
|
|
# Usage: install-refresh.sh <built-binary> <install-path> <expected-commit>
|
|
#
|
|
# It performs the stop, the install and the restart; the caller does the
|
|
# build. Ordering is load-bearing and matches CONVE-2687's day-74 clause:
|
|
# nothing irreversible happens until the artifact has been checked.
|
|
|
|
set -uo pipefail
|
|
|
|
BUILT="${1:?built binary path required}"
|
|
INSTALLED="${2:?install path required}"
|
|
EXPECT_COMMIT="${3:-}"
|
|
# PORT and HOST are resolved AFTER the argv capture, from the same
|
|
# precedence the server itself uses: flag, then environment, then default.
|
|
# See the resolution block below the capture.
|
|
PORT=""
|
|
PROBE_TIMEOUT="${PAD_PROBE_TIMEOUT:-20}"
|
|
|
|
die() { printf 'install-refresh: %s\n' "$*" >&2; exit 1; }
|
|
note() { printf 'install-refresh: %s\n' "$*"; }
|
|
|
|
# flag_value prints the value of a long flag from the captured argv,
|
|
# accepting BOTH supported spellings: `--host 1.2.3.4` and `--host=1.2.3.4`.
|
|
#
|
|
# Codex round 2 (P1): the first version matched only the space-separated
|
|
# form. Cobra accepts both, so a server started `--port=8080` was restarted
|
|
# on 8080 and probed on 7777 — a healthy restart failed by this script's
|
|
# most confident message. A parser that reads a subset of the syntax it is
|
|
# quoting back is the same defect class as a probe that reads half an
|
|
# invocation.
|
|
flag_value() {
|
|
local want="$1"
|
|
shift
|
|
local i
|
|
for ((i = 0; i < $#; i++)); do
|
|
local a="${@:i+1:1}"
|
|
case "$a" in
|
|
"$want")
|
|
printf '%s' "${@:i+2:1}"
|
|
return 0
|
|
;;
|
|
"$want"=*)
|
|
printf '%s' "${a#*=}"
|
|
return 0
|
|
;;
|
|
esac
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# data_dir prints the directory the server and CLI keep their state in, by
|
|
# config.Load's rule: PAD_DB_PATH's directory, else PAD_DATA_DIR, else ~/.pad
|
|
# (PAD_DB_PATH is applied second there, so it wins). The refresh marker
|
|
# (BUG-3215) must be found by every CLI on the box, so it lives here and not
|
|
# anywhere the cwd or the linked workspace could change.
|
|
data_dir() {
|
|
if [ -n "${PAD_DB_PATH:-}" ]; then
|
|
dirname "$PAD_DB_PATH"
|
|
elif [ -n "${PAD_DATA_DIR:-}" ]; then
|
|
printf '%s' "$PAD_DATA_DIR"
|
|
else
|
|
printf '%s' "$HOME/.pad"
|
|
fi
|
|
}
|
|
|
|
# config_value prints a top-level key from the pad config file, or nothing.
|
|
#
|
|
# The file is FLAT TOML (internal/config/config.go: Host, Port, Mode, ... are
|
|
# all top-level, no sections), so a line match is a correct reader for it
|
|
# rather than a hopeful one. Path resolution mirrors userConfigPath().
|
|
#
|
|
# Codex round 3 (P1): without this, a server whose host or port comes from
|
|
# the config file alone restarts correctly and is then probed on
|
|
# 127.0.0.1:7777 and reported as failed — on EVERY install, for that user.
|
|
# A stated boundary was the previous answer; it is a bad one when the cost of
|
|
# closing it is a grep and the cost of leaving it is a permanent false alarm.
|
|
config_value() {
|
|
local key="$1" path
|
|
path="$(data_dir)/config.toml"
|
|
[ -r "$path" ] || return 1
|
|
# Strip an inline comment before taking the value: `port = 8080 # dev`
|
|
# is valid TOML and the Go parser accepts it, so a regex that swallows
|
|
# the comment resolves the port to "8080 # dev" and probes nothing
|
|
# (codex round 4). A quoted value is unquoted first, so a `#` inside
|
|
# quotes survives.
|
|
sed -n "s/^[[:space:]]*$key[[:space:]]*=[[:space:]]*//p" "$path" |
|
|
sed -e 's/^"\([^"]*\)".*$/\1/' -e 's/[[:space:]]*#.*$//' -e 's/[[:space:]]*$//' |
|
|
head -1
|
|
}
|
|
|
|
# probe_url builds a health URL, bracketing IPv6 literals.
|
|
#
|
|
# Codex round 3 (P2): `::1` produced http://::1:7777/... — not a URL, so a
|
|
# healthy IPv6-bound server reads as unreachable. Accepts a value that is
|
|
# already bracketed, since that is how a user would write it in a flag.
|
|
probe_url() {
|
|
local host="$1" port="$2"
|
|
case "$host" in
|
|
\[*\]) : ;;
|
|
*:*) host="[$host]" ;;
|
|
esac
|
|
printf 'http://%s:%s/api/v1/health' "$host" "$port"
|
|
}
|
|
|
|
# commit_of prints the commit token a `--version` line carries, or nothing.
|
|
# Output shape: `pad version dev (a3a1d58 2026-09-07T12:33:21Z)`.
|
|
commit_of() {
|
|
printf '%s' "$1" | sed -n 's/.*(\([0-9a-f][0-9a-f]*\)[ )].*/\1/p'
|
|
}
|
|
|
|
# commit_matches compares an EXPECTED abbreviation against a FOUND one,
|
|
# tolerating different abbreviation LENGTHS.
|
|
#
|
|
# This is not defensive slack. `git rev-parse --short` returns the shortest
|
|
# UNAMBIGUOUS prefix, so its length grows as the object database does — the
|
|
# end-to-end run that found this had a binary embedding `a3a1d58` (7) while
|
|
# the Makefile, computing the same expression minutes later after a fetch,
|
|
# produced `a3a1d586` (8). An equality test fails there and reports "another
|
|
# session rebuilt it", which is a confident, specific and completely wrong
|
|
# diagnosis of a healthy build.
|
|
#
|
|
# The unit tests could not have caught it: their fixtures use fixed strings
|
|
# of equal length, so the two sides always agreed on width. Production does
|
|
# not have that property, and neither does any check written against it.
|
|
commit_matches() {
|
|
local expect="$1" found="$2"
|
|
[ -n "$expect" ] && [ -n "$found" ] || return 1
|
|
[ "$expect" = "$found" ] && return 0
|
|
|
|
# Resolve both to FULL object ids and compare those. This is the exact
|
|
# answer and it is available because `make install` runs in the repo.
|
|
#
|
|
# Codex round 5 (P1) on the previous prefix comparison: `a3a1d586`
|
|
# accepted `a3a1d58` even when those are DIFFERENT commits that happen
|
|
# to share seven characters. That is not hypothetical — it is the same
|
|
# mechanism that made the abbreviation grow in the first place, so the
|
|
# fix for the false alarm had introduced a false pass in the case the
|
|
# check exists for. Prefix matching cannot tell the two apart; full ids
|
|
# can.
|
|
local a b
|
|
a="$(git rev-parse --verify --quiet "${expect}^{commit}" 2>/dev/null || true)"
|
|
b="$(git rev-parse --verify --quiet "${found}^{commit}" 2>/dev/null || true)"
|
|
if [ -n "$a" ] && [ -n "$b" ]; then
|
|
[ "$a" = "$b" ]
|
|
return
|
|
fi
|
|
|
|
# FAIL CLOSED when an id cannot be resolved (codex round 9). A prefix
|
|
# comparison admits exactly the collision this function was rewritten to
|
|
# stop admitting, so keeping it as a fallback would reintroduce the
|
|
# defect on the path where verification is weakest.
|
|
#
|
|
# What this refuses: a binary whose embedded commit is not an object in
|
|
# THIS checkout — built from another history, or from a repository this
|
|
# one does not share. `make install` computes the expected id with `git
|
|
# rev-parse` in the same checkout it builds from, so the supported path
|
|
# always resolves; anything that does not is a case where the script
|
|
# genuinely cannot tell whether the artifact is the right one, and
|
|
# saying so is the whole point of this unit.
|
|
return 1
|
|
}
|
|
|
|
# --- 1. Capture the running server's argv BEFORE anything is killed ---------
|
|
#
|
|
# Identity, not count: other `pad` processes may exist (a sibling's CLI call,
|
|
# an e2e-spawned server from another worktree) and none of them is the thing
|
|
# being replaced. The server is the one with `server start` in its argv.
|
|
# Read from /proc so the exact tokens come back, including `--host`.
|
|
#
|
|
# Empty capture is a legitimate state — a box with no server running — and
|
|
# is handled at restart time rather than treated as an error here.
|
|
SERVER_ARGV=()
|
|
SERVER_PIDS=()
|
|
# The argv of every captured server, joined per pid on the ASCII unit
|
|
# separator (a bash string cannot hold the NULs /proc uses), so the target can
|
|
# be chosen after the port is known (BUG-3194). No argument pad accepts
|
|
# contains that byte.
|
|
US=$'\x1f'
|
|
declare -A SERVER_ARGV_OF=()
|
|
# Every element TERMINATED by the separator, not joined by it, and read back
|
|
# with mapfile: `read -a` drops trailing empty fields, and a trailing
|
|
# separator also keeps command substitution from stripping a final newline
|
|
# (codex round 1).
|
|
join_us() {
|
|
local a
|
|
for a in "$@"; do printf '%s%s' "$a" "$US"; done
|
|
}
|
|
# capture_pid records $1 as a server if its argv carries the `server start`
|
|
# verb pair. Keyed by identity (the argv), not by the binary's file name
|
|
# (BUG-3196): the name of the binary being INSTALLED says nothing about the
|
|
# name of the one running.
|
|
#
|
|
# With a second argument `require-pad`, argv[0] must also name a pad binary
|
|
# (pad, pad-*, pad.*), checked before anything is recorded. The name-matched
|
|
# candidates do not need it; a listener found only by port does.
|
|
capture_pid() {
|
|
local pid="$1" require="${2:-}"
|
|
[ -n "$pid" ] || return 0
|
|
[ -n "${SERVER_ARGV_OF[$pid]+x}" ] && return 0
|
|
# /proc where it exists, `ps` where it does not.
|
|
#
|
|
# Codex round 5 (P1): macOS has no /proc, so this loop found
|
|
# nothing there, SERVER_ARGV stayed empty, and the restart fell
|
|
# back to defaults — the exact defect BUG-2897 is about, silently
|
|
# unfixed on the platform the setsid fallback had just been added
|
|
# for. A fix that is Linux-only while advertising portability is
|
|
# worse than one that admits its platform.
|
|
#
|
|
# /proc is preferred because it gives the argv NUL-separated and
|
|
# therefore exactly; `ps -o args=` returns one space-joined string,
|
|
# so an argument containing a space is split. Named rather than
|
|
# hidden: no `pad server start` flag takes such a value today.
|
|
# A function now (BUG-3196), so `local` is legal here; codex round 6 of
|
|
# BUG-3194 had removed it when this body ran at top level.
|
|
local argv=() line i
|
|
# PAD_NO_PROC forces the `ps` path. Same reasoning as PAD_NO_SETSID:
|
|
# the branch is unreachable on any machine with /proc, and an
|
|
# untestable branch is exactly how the macOS half of this fix would
|
|
# have shipped broken.
|
|
if [ -z "${PAD_NO_PROC:-}" ] && [ -r "/proc/$pid/cmdline" ]; then
|
|
mapfile -d '' -t argv < "/proc/$pid/cmdline"
|
|
else
|
|
line="$(ps -o args= -p "$pid" 2>/dev/null || true)"
|
|
[ -n "$line" ] || return 0
|
|
read -r -a argv <<<"$line"
|
|
fi
|
|
[ ${#argv[@]} -gt 0 ] || return 0
|
|
if [ "$require" = require-pad ]; then
|
|
case "$(basename "${argv[0]}")" in
|
|
pad | pad-* | pad.*) ;;
|
|
*) return 0 ;;
|
|
esac
|
|
fi
|
|
# argv[0] is the binary; look for the `server start` verb pair.
|
|
for ((i = 1; i < ${#argv[@]}; i++)); do
|
|
if [ "${argv[i]}" = "server" ] && [ "${argv[i + 1]:-}" = "start" ]; then
|
|
SERVER_PIDS+=("$pid")
|
|
SERVER_ARGV_OF[$pid]="$(join_us "${argv[@]}")"
|
|
break
|
|
fi
|
|
done
|
|
}
|
|
if command -v pgrep >/dev/null 2>&1; then
|
|
# Candidates by name: the running binary is normally the installed one,
|
|
# and the built one's name is kept for a server started from a build.
|
|
while read -r pid; do
|
|
capture_pid "$pid"
|
|
# A pid found under both names is captured once: capture_pid skips a pid
|
|
# it has already recorded.
|
|
done < <({ pgrep -x "$(basename "$INSTALLED")"; pgrep -x "$(basename "$BUILT")"; } 2>/dev/null || true)
|
|
fi
|
|
|
|
# --- 1b. Choose the TARGET: the server holding the port this refresh is for ----
|
|
#
|
|
# BUG-3194. The target is the `server start` process LISTENING on the port the
|
|
# server itself would resolve with no flag (PAD_PORT, then the config file,
|
|
# then 7777), not "the only `server start` on the box". A sibling's e2e server
|
|
# on its own port is a different server; it used to make this script refuse
|
|
# (the old guard counted every `server start`), and the stop below used to be
|
|
# a system-wide `pkill -x` that would have killed it.
|
|
#
|
|
# Refused, still, where the answer is genuinely ambiguous:
|
|
# - the port is held by something that is not a captured `server start`;
|
|
# - more than one captured server holds it;
|
|
# - nothing holds it and more than one server is running elsewhere.
|
|
# With exactly one server, nothing on the port, and an explicit `--port` in its
|
|
# argv (a server deliberately started elsewhere), that server is the target.
|
|
# Stated limits: an argument containing the \x1f separator would be split
|
|
# (no pad flag takes one), and a pid can in principle be recycled between
|
|
# the last argv check and a signal.
|
|
#
|
|
# The listener is read with `ss` (Linux) or `lsof` (macOS). Where neither can
|
|
# see listeners, only the unambiguous one-server case proceeds.
|
|
numeric() { case "$1" in '' | *[!0-9]*) return 1 ;; *) return 0 ;; esac; }
|
|
TARGET_PORT=""
|
|
for candidate in "${PAD_PORT:-}" "$(config_value port || true)"; do
|
|
if numeric "$candidate"; then
|
|
TARGET_PORT="$candidate"
|
|
break
|
|
fi
|
|
done
|
|
TARGET_PORT="${TARGET_PORT:-7777}"
|
|
|
|
# listener_pids prints the pids listening on TCP port $1, one per line, and
|
|
# prints the word HIDDEN when a listener is visible but its owner is not (ss
|
|
# without process info, lsof unable to read another process). A hidden owner
|
|
# is refused below rather than read as "nothing is on the port" (codex
|
|
# round 1).
|
|
listener_pids() {
|
|
local lines pids
|
|
if command -v ss >/dev/null 2>&1; then
|
|
lines="$(ss -Hltnp "sport = :$1" 2>/dev/null)"
|
|
[ -n "$lines" ] || return 0
|
|
while IFS= read -r l; do
|
|
case "$l" in *pid=*) ;; *) echo HIDDEN ;; esac
|
|
done <<<"$lines"
|
|
grep -o 'pid=[0-9]*' <<<"$lines" | cut -d= -f2 | sort -u
|
|
elif command -v lsof >/dev/null 2>&1; then
|
|
lines="$(lsof -nP -iTCP:"$1" -sTCP:LISTEN 2>/dev/null | tail -n +2)"
|
|
[ -n "$lines" ] || return 0
|
|
pids="$(awk '{print $2}' <<<"$lines" | grep -E '^[0-9]+$' | sort -u)"
|
|
[ -n "$pids" ] || echo HIDDEN
|
|
printf '%s\n' "$pids"
|
|
fi
|
|
}
|
|
|
|
TARGET_PID=""
|
|
LISTENERS=()
|
|
HIDDEN=""
|
|
while read -r lp; do
|
|
case "$lp" in
|
|
HIDDEN) HIDDEN=1 ;;
|
|
'') ;;
|
|
*) LISTENERS+=("$lp") ;;
|
|
esac
|
|
done < <(listener_pids "$TARGET_PORT")
|
|
|
|
# A listener is judged by what it IS rather than by the name of the binary
|
|
# being installed (BUG-3196): a pad `server start` holding the port is a server
|
|
# even when neither candidate name matched it (a binary started as
|
|
# pad-<commit>, say). Its OWN argv[0] must still be a pad binary: without that
|
|
# any tool on this port whose argv happens to read `server start` would be
|
|
# stopped and replaced.
|
|
for lp in "${LISTENERS[@]}"; do
|
|
capture_pid "$lp" require-pad
|
|
done
|
|
|
|
matched=()
|
|
foreign=()
|
|
for lp in "${LISTENERS[@]}"; do
|
|
if [ -n "${SERVER_ARGV_OF[$lp]+x}" ]; then
|
|
matched+=("$lp")
|
|
else
|
|
foreign+=("$lp")
|
|
fi
|
|
done
|
|
|
|
# A listener whose owner cannot be read, or ANY non-captured listener on the
|
|
# port (even beside a captured one, on another address), makes the port
|
|
# ambiguous: refused before anything else is considered (codex round 1).
|
|
if [ -n "$HIDDEN" ]; then
|
|
die "something is listening on port $TARGET_PORT but its owning process cannot be read here.
|
|
Nothing was stopped or installed; this script will not guess which server that is."
|
|
fi
|
|
if [ "${#foreign[@]}" -gt 0 ]; then
|
|
die "port $TARGET_PORT is held by pid(s) ${foreign[*]}, which is not a running \`server start\`.
|
|
Nothing was stopped or installed; find out what holds the port first."
|
|
fi
|
|
|
|
if [ "${#matched[@]}" -gt 1 ]; then
|
|
die "more than one \`server start\` is listening on port $TARGET_PORT (pids: ${matched[*]}).
|
|
Nothing was stopped or installed. Use the manual sibling-safe refresh."
|
|
elif [ "${#matched[@]}" -eq 1 ]; then
|
|
TARGET_PID="${matched[0]}"
|
|
elif [ "${#SERVER_PIDS[@]}" -eq 1 ]; then
|
|
# The only server, and nothing on the target port: this is the target
|
|
# ONLY if it was deliberately started elsewhere, with an explicit
|
|
# --port. Without that it is most likely a sibling's server (an e2e
|
|
# webServer takes its port from the environment) while this box's own
|
|
# server is down, and replacing it would restart someone else's server
|
|
# from this install (codex round 2).
|
|
only=()
|
|
mapfile -d "$US" -t only < <(printf '%s' "${SERVER_ARGV_OF[${SERVER_PIDS[0]}]}")
|
|
if flag_value --port "${only[@]}" >/dev/null; then
|
|
TARGET_PID="${SERVER_PIDS[0]}"
|
|
else
|
|
die "nothing is listening on port $TARGET_PORT, and the one \`server start\` running (pid ${SERVER_PIDS[0]}) was not started with --port, so it is not this box's server on another port.
|
|
Nothing was stopped or installed. Start the server, or use the manual refresh."
|
|
fi
|
|
elif [ "${#SERVER_PIDS[@]}" -gt 1 ]; then
|
|
die "more than one \`server start\` process is running (pids: ${SERVER_PIDS[*]}) and none is listening on port $TARGET_PORT.
|
|
This script cannot tell which one to replace.
|
|
Nothing was stopped or installed. Use the manual sibling-safe refresh."
|
|
fi
|
|
|
|
if [ -n "$TARGET_PID" ]; then
|
|
mapfile -d "$US" -t SERVER_ARGV < <(printf '%s' "${SERVER_ARGV_OF[$TARGET_PID]}")
|
|
OTHERS=()
|
|
for sp in "${SERVER_PIDS[@]}"; do
|
|
[ "$sp" != "$TARGET_PID" ] && OTHERS+=("$sp")
|
|
done
|
|
if [ "${#OTHERS[@]}" -gt 0 ]; then
|
|
note "other \`server start\` processes are left running: ${OTHERS[*]}"
|
|
fi
|
|
fi
|
|
|
|
if [ ${#SERVER_ARGV[@]} -gt 0 ]; then
|
|
note "captured server argv: ${SERVER_ARGV[*]}"
|
|
else
|
|
note "no running server found; will start with defaults after install"
|
|
fi
|
|
|
|
# --- 1c. Capture the TARGET's working directory, with its argv ----------------
|
|
#
|
|
# BUG-3196. The restart used to inherit the INVOKING shell's cwd, so a refresh
|
|
# run from a worktree left the server in that worktree, and removing the
|
|
# worktree left it in a deleted directory. The server's cwd is part of how it
|
|
# was started, like its argv, so it is captured and restored the same way.
|
|
#
|
|
# Refused BEFORE anything is stopped:
|
|
# - the captured cwd no longer exists: the server cannot be put back where
|
|
# it was, and the refusal names the path so the operator can choose;
|
|
# - the cwd cannot be read at all: restoring both argv and cwd is then
|
|
# impossible, and falling back to the caller's cwd is this bug.
|
|
# With no server running there is nothing to restore; the caller's cwd is used,
|
|
# as before.
|
|
SERVER_CWD=""
|
|
if [ -n "$TARGET_PID" ]; then
|
|
# What was read and why it failed, for the refusal: the path asked and
|
|
# the tool's own error text, which carries the errno (e.g. "Permission
|
|
# denied"). Lead ruling on BUG-3196.
|
|
cwd_source="" cwd_err=""
|
|
if [ -z "${PAD_NO_PROC:-}" ] && [ -d /proc ]; then
|
|
cwd_source="/proc/$TARGET_PID/cwd"
|
|
cwd_err="$(readlink "$cwd_source" 2>&1 >/dev/null)" || true
|
|
SERVER_CWD="$(readlink "$cwd_source" 2>/dev/null || true)"
|
|
elif command -v lsof >/dev/null 2>&1; then
|
|
cwd_source="lsof -a -p $TARGET_PID -d cwd"
|
|
cwd_err="$(lsof -a -p "$TARGET_PID" -d cwd -Fn 2>&1 >/dev/null)" || true
|
|
SERVER_CWD="$(lsof -a -p "$TARGET_PID" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -1)"
|
|
else
|
|
cwd_source="(no /proc and no lsof)"
|
|
fi
|
|
if [ -z "$SERVER_CWD" ]; then
|
|
die "the working directory of the running server (pid $TARGET_PID) cannot be read: $cwd_source: ${cwd_err:-no output}
|
|
Nothing was stopped or installed; the restart could not put it back where it was."
|
|
fi
|
|
# Linux reads a removed directory back with a " (deleted)" suffix. Strip
|
|
# it so the refusal names the real path; the one existence check below
|
|
# then refuses it, since that path is gone.
|
|
SERVER_CWD="${SERVER_CWD% (deleted)}"
|
|
if [ ! -d "$SERVER_CWD" ]; then
|
|
die "the running server (pid $TARGET_PID) is in a directory that no longer exists: $SERVER_CWD
|
|
Nothing was stopped or installed. Restart it from the directory it should run in, then refresh."
|
|
fi
|
|
# Stated limit: command substitution strips trailing newlines, so a
|
|
# directory whose NAME ends in one is not captured exactly.
|
|
note "captured server cwd: $SERVER_CWD"
|
|
fi
|
|
|
|
# --- 2. SNAPSHOT the artifact, then check the snapshot ------------------------
|
|
#
|
|
# The copy happens BEFORE the stop, into a temporary file beside the final
|
|
# path, and the check runs on that snapshot.
|
|
#
|
|
# Codex round 2 (P1) found the window this closes: checking $BUILT and then
|
|
# copying it later leaves room for another session to rebuild the shared repo
|
|
# binary in between, so the destination check catches the swap only AFTER the
|
|
# old server has been killed — turning a detectable race into an outage. A
|
|
# snapshot cannot change under us, so the expensive failure mode disappears
|
|
# rather than being detected later.
|
|
#
|
|
# The temp file lives in the DESTINATION directory so the final move is a
|
|
# rename within one filesystem, which is atomic: no window in which
|
|
# ~/.local/bin/pad is half-written.
|
|
mkdir -p "$(dirname "$INSTALLED")"
|
|
STAGED="$(mktemp "$(dirname "$INSTALLED")/.$(basename "$INSTALLED").XXXXXX")" ||
|
|
die "could not stage the install next to $INSTALLED"
|
|
# The marker is removed only while it is still OURS: a second refresh that
|
|
# started after this one owns the file now, and removing it would reopen the
|
|
# window it is holding.
|
|
cleanup() {
|
|
[ -n "${STAGED:-}" ] && rm -f "$STAGED"
|
|
if [ -n "${MARKER_WRITTEN:-}" ] &&
|
|
[ "$(sed -n 's/.*"pid": *\([0-9]*\).*/\1/p' "$REFRESH_MARKER" 2>/dev/null)" = "$$" ]; then
|
|
rm -f "$REFRESH_MARKER"
|
|
fi
|
|
return 0
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
cp -f "$BUILT" "$STAGED" || die "cp failed"
|
|
chmod +x "$STAGED"
|
|
|
|
# The ARTIFACT check, on the snapshot, while the old server is still serving.
|
|
# Failing here costs nothing.
|
|
if [ -n "$EXPECT_COMMIT" ]; then
|
|
staged_version="$("$STAGED" --version 2>/dev/null || true)"
|
|
if ! commit_matches "$EXPECT_COMMIT" "$(commit_of "$staged_version")"; then
|
|
die "the binary at $BUILT is not this invocation's build; refusing to stop the server.
|
|
expected commit: $EXPECT_COMMIT
|
|
$BUILT reads: ${staged_version:-<no version output>}
|
|
Another session very likely rebuilt it between this build and now.
|
|
Nothing was stopped or installed; re-run the build and install together."
|
|
fi
|
|
fi
|
|
|
|
# PORT resolution, in the server's own precedence order. Resolved before the
|
|
# stop because the refresh marker names it (BUG-3215); the probe uses it too.
|
|
#
|
|
# Codex round 1: probing a fixed 7777 while faithfully restarting a server
|
|
# that was started with `--port 8080` fails a HEALTHY restart, and does it
|
|
# with this script's most confident message. The flag exists
|
|
# (cmd_server.go: `--port`, default 7777) so the case is reachable, and the
|
|
# whole point of this script is that the restart preserves the invocation —
|
|
# a probe that ignores half of that invocation is checking the wrong server.
|
|
configured_port="$(flag_value --port "${SERVER_ARGV[@]}" || true)"
|
|
# Each source is accepted only if it is NUMERIC, mirroring the application:
|
|
# config.go ignores a non-integer PAD_PORT and falls through to the next
|
|
# source. Without that, a mistyped or inherited PAD_PORT is probed
|
|
# literally and every refresh fails against a server that is running fine
|
|
# (codex round 7).
|
|
PORT=""
|
|
for candidate in "$configured_port" "${PAD_PORT:-}" "$(config_value port || true)"; do
|
|
if numeric "$candidate"; then
|
|
PORT="$candidate"
|
|
break
|
|
fi
|
|
if [ -n "$candidate" ]; then
|
|
note "ignoring non-numeric port value \"$candidate\" (as the server does)"
|
|
fi
|
|
done
|
|
PORT="${PORT:-7777}"
|
|
|
|
# --- 2b. Claim the stop->restart window (BUG-3215) ---------------------------
|
|
#
|
|
# Between the stop below and the restart answering, nothing serves this port,
|
|
# and any pad CLI call in that window used to AUTO-START a server of its own:
|
|
# the caller's binary (a worktree build, or the old inode before the rename),
|
|
# from the caller's cwd, with default flags. It then held the port against
|
|
# this script's restart. Measured on day 79: the lead's `pad item comment`
|
|
# took the port during a bounce, 127.0.0.1:7777 went dark, and the script
|
|
# exited 1.
|
|
#
|
|
# The marker hands the window to this script. EnsureServer
|
|
# (internal/cli/refresh_marker.go) reads it and waits instead of spawning
|
|
# while it is LIVE: the pid below alive AND the deadline not passed, for the
|
|
# port it names. Written by rename, so a reader never sees half of it; removed
|
|
# by the EXIT trap, on success and on every refusal after this point.
|
|
#
|
|
# The deadline is short on purpose: the 5s TERM wait, a 5s margin for the
|
|
# install and the spawn, and one probe timeout (PAD_PROBE_TIMEOUT, 20s by
|
|
# default), so 30s by default. A healthy bounce is over in seconds and the
|
|
# waiting CLI returns the moment the server answers; the deadline only
|
|
# decides how long a CLI waits on a refresh that is failing, after which it
|
|
# errors naming this pid rather than spawning.
|
|
REFRESH_MARKER="$(data_dir)/refresh.lock"
|
|
MARKER_WRITTEN=""
|
|
probe_budget="$PROBE_TIMEOUT"
|
|
numeric "$probe_budget" || probe_budget=20
|
|
mkdir -p "$(dirname "$REFRESH_MARKER")" || die "could not create $(dirname "$REFRESH_MARKER") for the refresh marker"
|
|
marker_tmp="$(mktemp "$REFRESH_MARKER.XXXXXX")" || die "could not stage the refresh marker next to $REFRESH_MARKER"
|
|
printf '{"pid": %d, "port": %d, "deadline": %d}\n' "$$" "$PORT" "$(($(date +%s) + 10 + probe_budget))" >"$marker_tmp" &&
|
|
mv -f "$marker_tmp" "$REFRESH_MARKER" ||
|
|
{
|
|
rm -f "$marker_tmp"
|
|
die "could not write the refresh marker $REFRESH_MARKER.
|
|
Nothing was stopped or installed."
|
|
}
|
|
MARKER_WRITTEN=1
|
|
|
|
# --- 3. Stop -----------------------------------------------------------------
|
|
#
|
|
# The TARGET only, by pid (BUG-3194). This used to be `pkill -x <name>`, which
|
|
# reached every process with the binary's name: a sibling's e2e server, and
|
|
# any `pad` CLI call in flight. The argv is re-read immediately before the
|
|
# kill, because the pid was captured before the artifact check and a pid can
|
|
# be recycled: a mismatch refuses rather than killing whatever now holds it.
|
|
# SIGTERM first so the graceful-shutdown path runs (BUG-1531).
|
|
BIN_NAME="$(basename "$BUILT")"
|
|
if [ -n "$TARGET_PID" ]; then
|
|
# still_target re-reads the pid's argv the way the capture read it and
|
|
# succeeds only if it is still the captured server. Asked before EACH
|
|
# signal: a pid that exits and is recycled during the TERM wait must not
|
|
# receive the KILL (codex round 1). A pid that is already gone is not
|
|
# signalled at all.
|
|
still_target() {
|
|
local now=()
|
|
if [ -z "${PAD_NO_PROC:-}" ] && [ -r "/proc/$TARGET_PID/cmdline" ]; then
|
|
mapfile -d '' -t now < "/proc/$TARGET_PID/cmdline" 2>/dev/null
|
|
else
|
|
read -r -a now <<<"$(ps -o args= -p "$TARGET_PID" 2>/dev/null || true)"
|
|
fi
|
|
[ ${#now[@]} -gt 0 ] && [ "$(join_us "${now[@]}")" = "${SERVER_ARGV_OF[$TARGET_PID]}" ]
|
|
}
|
|
if kill -0 "$TARGET_PID" 2>/dev/null; then
|
|
# The cwd is checked again at the last moment before the stop
|
|
# (BUG-3196, codex round 1): it was checked before the snapshot, and
|
|
# a directory removed since then would leave the server stopped with
|
|
# nowhere to restart it. RESIDUAL, stated: one removed between this
|
|
# check and the restart's `cd` still can, and the restart then fails
|
|
# loudly (naming the port to check) rather than starting elsewhere.
|
|
if [ -n "$SERVER_CWD" ] && [ ! -d "$SERVER_CWD" ]; then
|
|
die "the running server's directory no longer exists: $SERVER_CWD
|
|
Nothing was stopped or installed. Restart it from the directory it should run in, then refresh."
|
|
fi
|
|
if ! still_target; then
|
|
die "pid $TARGET_PID no longer runs the captured server.
|
|
Nothing was stopped or installed; re-run."
|
|
fi
|
|
kill -TERM "$TARGET_PID" 2>/dev/null || true
|
|
for _ in 1 2 3 4 5; do
|
|
kill -0 "$TARGET_PID" 2>/dev/null || break
|
|
sleep 1
|
|
done
|
|
if kill -0 "$TARGET_PID" 2>/dev/null && still_target; then
|
|
kill -KILL "$TARGET_PID" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# --- 4. Move into place, then check the OUTCOME -------------------------------
|
|
mv -f "$STAGED" "$INSTALLED" || die "could not move the staged binary into $INSTALLED"
|
|
STAGED=""
|
|
rm -f "$HOME/.pad/pad.pid"
|
|
|
|
# The check TASK-2787 asks for, and it is deliberately on the installed file
|
|
# rather than on any step: read what is now at the install path and require it
|
|
# to carry the commit this invocation built with. Distinct from step 2 — that
|
|
# asked "is the source right", this asks "is the DESTINATION right", and it
|
|
# is the leg that survives another writer racing the final path.
|
|
#
|
|
# WHAT THIS ANSWERS, stated because the check reads stronger than it is: it
|
|
# catches "the binary here was built at a DIFFERENT commit", which is the
|
|
# reversed-ordering case TASK-2787 names. It does NOT establish byte
|
|
# provenance: a sibling who built the SAME commit leaves a binary this check
|
|
# accepts. That case is benign by the task's own scope note, and pretending
|
|
# otherwise would be the kind of overclaimed instrument this repo keeps
|
|
# filing bugs about.
|
|
if [ -n "$EXPECT_COMMIT" ]; then
|
|
installed_version="$("$INSTALLED" --version 2>/dev/null || true)"
|
|
if ! commit_matches "$EXPECT_COMMIT" "$(commit_of "$installed_version")"; then
|
|
die "installed binary is not the one this invocation built.
|
|
expected commit: $EXPECT_COMMIT
|
|
installed reads: ${installed_version:-<no version output>}
|
|
Another session very likely wrote $INSTALLED between this move and this read.
|
|
Re-run the build and install together; do not trust the previous output."
|
|
fi
|
|
fi
|
|
note "installed $BIN_NAME to $INSTALLED ($EXPECT_COMMIT)"
|
|
|
|
# --- 5. Restart with the argv that was killed --------------------------------
|
|
#
|
|
# The whole point of BUG-2897: a replacement is only correct if it preserves
|
|
# what it replaced, and that includes the INVOCATION, not just the bytes.
|
|
if [ ${#SERVER_ARGV[@]} -gt 0 ]; then
|
|
restart=("$INSTALLED" "${SERVER_ARGV[@]:1}")
|
|
else
|
|
restart=("$INSTALLED" server start)
|
|
fi
|
|
# mkdir before the redirect: a missing ~/.pad makes the redirection fail and
|
|
# the server never starts, which the probe then reports as "did not answer" —
|
|
# true, but three steps downstream of the cause. Found by the probe-failure
|
|
# test on a fresh HOME, where the directory does not exist; on a developer's
|
|
# box it exists by luck of history, which is why nothing had caught it.
|
|
mkdir -p "$HOME/.pad"
|
|
# setsid where it exists, plain nohup where it does not. Codex round 2
|
|
# (P1): setsid is Linux-specific and absent from a default macOS
|
|
# environment, where an unconditional call would fail AFTER the old server
|
|
# had been stopped — the one ordering this script exists to avoid. nohup
|
|
# plus the background job is sufficient detachment on both.
|
|
# PAD_NO_SETSID forces the fallback branch. It exists because the branch is
|
|
# otherwise unreachable on the machines this is developed and tested on —
|
|
# every Linux box has setsid — and an untestable branch is how the macOS
|
|
# case would have shipped broken a second time. Test-only knob, documented
|
|
# rather than hidden.
|
|
# Re-read the installed binary IMMEDIATELY before executing it.
|
|
#
|
|
# Codex round 4 (P1): a concurrent `make install` can replace $INSTALLED
|
|
# between the outcome check above and this exec, so this invocation would
|
|
# launch a build it never verified and report success.
|
|
#
|
|
# RESIDUAL, stated rather than glossed: the window is narrowed, not closed —
|
|
# the complete answer is an exclusive lock across the whole
|
|
# check-install-restart sequence. Deliberately not added here: flock is
|
|
# Linux-only, and a half-portable lock that silently does nothing on macOS is
|
|
# worse than a named residual. Filed separately.
|
|
if [ -n "$EXPECT_COMMIT" ]; then
|
|
preexec_version="$("$INSTALLED" --version 2>/dev/null || true)"
|
|
if ! commit_matches "$EXPECT_COMMIT" "$(commit_of "$preexec_version")"; then
|
|
die "another session replaced $INSTALLED between the install check and the restart.
|
|
expected commit: $EXPECT_COMMIT
|
|
installed reads: ${preexec_version:-<no version output>}
|
|
Refusing to start a binary this invocation did not verify. No server is running."
|
|
fi
|
|
fi
|
|
|
|
# In the captured cwd (BUG-3196). The binary path is made absolute first,
|
|
# since the install path may have been given relative to the caller's cwd.
|
|
case "${restart[0]}" in
|
|
/*) ;;
|
|
*) restart[0]="$(cd "$(dirname "${restart[0]}")" && pwd)/$(basename "${restart[0]}")" ;;
|
|
esac
|
|
if [ -n "$SERVER_CWD" ]; then
|
|
cd "$SERVER_CWD" || die "could not enter $SERVER_CWD to restart the server there.
|
|
The old server was signalled to stop and the new binary is installed; nothing was restarted. Check what is running on port $TARGET_PORT."
|
|
fi
|
|
# describe_pid prints "pid N (argv)" for a report. Best effort: a pid that has
|
|
# gone by the time it is read is named alone.
|
|
describe_pid() {
|
|
local args
|
|
args="$(ps -o args= -p "$1" 2>/dev/null || true)"
|
|
if [ -n "$args" ]; then
|
|
printf 'pid %s (%s)' "$1" "$args"
|
|
else
|
|
printf 'pid %s' "$1"
|
|
fi
|
|
}
|
|
|
|
# The old server is stopped, so anything holding the port NOW took it in the
|
|
# window (BUG-3215): a CLI auto-start from a binary that predates the marker,
|
|
# or a start by hand. The restart could not bind, so it is not spawned, and
|
|
# the holder is named instead of left for the probe to misread.
|
|
early=()
|
|
while read -r lp; do
|
|
case "$lp" in HIDDEN | '') ;; *) early+=("$lp") ;; esac
|
|
done < <(listener_pids "$PORT")
|
|
if [ "${#early[@]}" -gt 0 ]; then
|
|
held_by=()
|
|
for lp in "${early[@]}"; do held_by+=("$(describe_pid "$lp")"); done
|
|
die "port $PORT was taken after the old server stopped and before the restart, by: ${held_by[*]}.
|
|
The binary is installed; nothing was restarted, because the restart could not bind.
|
|
Most likely a pad CLI call auto-started a server in the window. Stop that process and re-run the refresh.
|
|
Restart argv would have been: ${restart[*]}"
|
|
fi
|
|
|
|
if [ -z "${PAD_NO_SETSID:-}" ] && command -v setsid >/dev/null 2>&1; then
|
|
setsid nohup "${restart[@]}" >>"$HOME/.pad/server.log" 2>&1 </dev/null &
|
|
else
|
|
nohup "${restart[@]}" >>"$HOME/.pad/server.log" 2>&1 </dev/null &
|
|
fi
|
|
RESTART_PID=$!
|
|
disown 2>/dev/null || true
|
|
|
|
# --- 6. Probe BOTH addresses before claiming anything -------------------------
|
|
#
|
|
# `--host 0.0.0.0` is a bind spec, not an address to curl, so the configured
|
|
# host resolves to the primary LAN address in that case. Loopback is always
|
|
# probed: it is the address the previous defect killed, and the one every
|
|
# local CLI call and browser tab uses.
|
|
configured_host="$(flag_value --host "${SERVER_ARGV[@]}" || true)"
|
|
|
|
|
|
# HOST likewise. When the argv carries no --host, fall back to PAD_HOST,
|
|
# which config.go reads for exactly this purpose.
|
|
#
|
|
# Precedence: flag, then environment, then the config file, then the
|
|
# default — the same order the server itself resolves them in. The config
|
|
# file is read because leaving it out meant a permanent false failure for
|
|
# anyone who configures the bind there (codex round 3).
|
|
if [ -z "$configured_host" ]; then
|
|
configured_host="${PAD_HOST:-$(config_value host || true)}"
|
|
fi
|
|
|
|
# The probe set is exactly what the BIND PROMISES, no more and no less.
|
|
#
|
|
# A wildcard bind promises loopback AND the LAN address, and the original
|
|
# defect was precisely a wildcard server coming back bound to one host with
|
|
# 127.0.0.1 dead — so both are required there. An EXPLICIT single host
|
|
# promises only itself: demanding 127.0.0.1 from a server deliberately bound
|
|
# to ::1 or to a LAN interface would fail a correct configuration, which is
|
|
# the same false alarm in the opposite direction.
|
|
#
|
|
# The first version required 127.0.0.1 unconditionally, and the IPv6 test
|
|
# caught it: a server bound ::1 restarted correctly and was reported dead
|
|
# because a v4 loopback it never promised did not answer.
|
|
probe_hosts=()
|
|
case "$configured_host" in
|
|
"" | "127.0.0.1")
|
|
probe_hosts=("127.0.0.1")
|
|
;;
|
|
"localhost")
|
|
# `localhost` may resolve to either loopback family, and which one the
|
|
# server binds is not knowable from here (codex round 8). Requiring both
|
|
# would fail a healthy v4-only or v6-only bind, so this is the one case
|
|
# where ANY member answering is the pass — recorded because it is a
|
|
# deliberate exception to "every probed address must answer".
|
|
probe_any=1
|
|
probe_hosts=("127.0.0.1" "::1")
|
|
;;
|
|
"0.0.0.0")
|
|
probe_hosts=("127.0.0.1")
|
|
lan="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
|
if [ -z "$lan" ]; then
|
|
lan="$(ipconfig getifaddr en0 2>/dev/null || true)"
|
|
fi
|
|
if [ -n "$lan" ]; then
|
|
probe_hosts+=("$lan")
|
|
else
|
|
# FAIL CLOSED (codex rounds 5 and 8). A wildcard bind promises the
|
|
# external address, and being unable to check it is not the same as
|
|
# it being fine. The first version warned and exited 0, which is
|
|
# this unit's own defect wearing a different hat: a success line
|
|
# covering something that was not verified.
|
|
die "cannot determine a LAN address to verify the wildcard bind (tried \`hostname -I\` and \`ipconfig getifaddr en0\`).
|
|
The binary is installed and loopback answers, but the external half of
|
|
\`--host 0.0.0.0\` is unverified and this refuses to claim otherwise.
|
|
Start the server with an explicit --host, or set PAD_HOST, to make the
|
|
address checkable."
|
|
fi
|
|
;;
|
|
"::")
|
|
probe_hosts=("127.0.0.1" "::1")
|
|
;;
|
|
*)
|
|
probe_hosts=("$configured_host")
|
|
;;
|
|
esac
|
|
|
|
probe() {
|
|
local host="$1" deadline=$((SECONDS + PROBE_TIMEOUT))
|
|
while [ $SECONDS -lt $deadline ]; do
|
|
if curl -fsS -m 2 -o /dev/null "$(probe_url "$host" "$PORT")" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
return 1
|
|
}
|
|
|
|
failed=()
|
|
answered=0
|
|
for host in "${probe_hosts[@]}"; do
|
|
if probe "$host"; then
|
|
answered=$((answered + 1))
|
|
else
|
|
failed+=("$host")
|
|
fi
|
|
done
|
|
|
|
# Every probed address must answer, EXCEPT the `localhost` case above, where
|
|
# one loopback family answering is the whole of what can be required.
|
|
if [ "${probe_any:-0}" = "1" ] && [ "$answered" -gt 0 ]; then
|
|
failed=()
|
|
probe_hosts=("localhost")
|
|
fi
|
|
|
|
# WHO answered (BUG-3215). A health probe answers for whatever holds the
|
|
# address, so a server that took the port in the window passes it: on day 79
|
|
# the stray bound the LAN address with default flags, and had it bound
|
|
# loopback this script would have printed success over the wrong server. The
|
|
# listener must be the restart this script spawned (or its direct child,
|
|
# should `setsid` have had to fork). Checked on the failure path too, where a
|
|
# named holder is the diagnosis a bare "did not answer" is not.
|
|
owner_ok() {
|
|
[ "$1" = "$RESTART_PID" ] && return 0
|
|
[ "$(ps -o ppid= -p "$1" 2>/dev/null | tr -d ' ')" = "$RESTART_PID" ]
|
|
}
|
|
late=()
|
|
late_hidden=""
|
|
while read -r lp; do
|
|
case "$lp" in HIDDEN) late_hidden=1 ;; '') ;; *) late+=("$lp") ;; esac
|
|
done < <(listener_pids "$PORT")
|
|
foreign_late=()
|
|
for lp in "${late[@]}"; do
|
|
owner_ok "$lp" || foreign_late+=("$(describe_pid "$lp")")
|
|
done
|
|
if [ "${#foreign_late[@]}" -gt 0 ]; then
|
|
die "port $PORT is held by ${foreign_late[*]}, not by this refresh's restart (pid $RESTART_PID).
|
|
The binary is installed, but the server answering is not the one this refresh started.
|
|
Most likely a pad CLI call auto-started a server in the window. Stop that process and re-run the refresh.
|
|
Restart argv was: ${restart[*]}"
|
|
fi
|
|
if [ -n "$late_hidden" ]; then
|
|
die "a listener on port $PORT cannot be attributed to a process here, so this refresh cannot confirm the restart (pid $RESTART_PID) is what answers.
|
|
The binary is installed. Check what holds port $PORT."
|
|
fi
|
|
if [ "${#late[@]}" -eq 0 ] && [ ${#failed[@]} -eq 0 ]; then
|
|
# Neither ss nor lsof can see listeners here: the probe passed, but
|
|
# whose server answered is unverified, and the success line says so.
|
|
unattributed=1
|
|
fi
|
|
|
|
if [ ${#failed[@]} -gt 0 ]; then
|
|
die "server did not answer on: ${failed[*]} (port $PORT, waited ${PROBE_TIMEOUT}s each).
|
|
The binary is installed; the server is not serving those addresses.
|
|
Restart argv was: ${restart[*]}"
|
|
fi
|
|
|
|
note "server restarted and answering on: ${probe_hosts[*]} (port $PORT)"
|
|
if [ -n "${unattributed:-}" ]; then
|
|
note "listener ownership NOT verified: neither ss nor lsof can see listeners here, so the process answering was not confirmed to be the restart (pid $RESTART_PID)"
|
|
fi
|