From dc05ab4a51c08e5e273eff175f05eb8a17415476 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sat, 26 Sep 2026 13:12:20 -0500 Subject: [PATCH 1/2] fix(deploy): find the server by the port it listens on On macOS the Homebrew interpreter shows up in ps as .../MacOS/Python, so pgrep -f 'python -m dw\.serve' finds nothing and a deploy starts a second server on a port that's taken. Match what listens on DW_PORT instead (lsof), falling back to the old pgrep where lsof is missing. Same result on lem, where one server runs. Co-Authored-By: Claude Opus 5.5 --- scripts/deploy.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 8df6d313..9f8926ef 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -57,7 +57,17 @@ done ts() { date '+%H:%M:%S'; } say() { echo "[deploy $(ts)] $*"; } health() { curl -s -m 5 -H "Authorization: Bearer $DW_TOKEN" "$HEALTH" 2>/dev/null; } -server_pids() { pgrep -f 'python -m dw\.serve' || true; } +# The server is whatever listens on DW_PORT. Matching the command line +# missed it on macOS, where Homebrew's interpreter shows up in ps as +# .../MacOS/Python (capital P), and a broader match would stop every +# dw.serve on the machine rather than the one on this port. +server_pids() { + if command -v lsof >/dev/null 2>&1; then + lsof -ti "tcp:$DW_PORT" -sTCP:LISTEN 2>/dev/null || true + else + pgrep -f 'python -m dw\.serve' || true + fi +} # `ssh lem deploy.sh` runs a non-interactive shell, and a per-user node # install is usually put on PATH by ~/.bashrc *after* its "not interactive, From b9c7ec15b60958a81b0c09af98fd225c52614286 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Sat, 26 Sep 2026 13:29:34 -0500 Subject: [PATCH 2/2] fix(deploy): stop by port, but judge startup by the process dc05ab4 made every check ask what listens on DW_PORT. dw.serve imports torch for seconds before it binds, so the startup loop saw an empty port and reported 'exited during startup' on every deploy. Keep the port for choosing what to stop, wait on the pids actually signalled (uvicorn closes its socket early in a graceful shutdown), and judge startup by the process (pgrep -i, so Homebrew's .../MacOS/Python matches too). Co-Authored-By: Claude Opus 5.5 --- scripts/deploy.sh | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 9f8926ef..0e6b952c 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -57,17 +57,23 @@ done ts() { date '+%H:%M:%S'; } say() { echo "[deploy $(ts)] $*"; } health() { curl -s -m 5 -H "Authorization: Bearer $DW_TOKEN" "$HEALTH" 2>/dev/null; } -# The server is whatever listens on DW_PORT. Matching the command line -# missed it on macOS, where Homebrew's interpreter shows up in ps as -# .../MacOS/Python (capital P), and a broader match would stop every -# dw.serve on the machine rather than the one on this port. -server_pids() { +# Two questions, two answers. Which server to stop is whatever listens on +# DW_PORT: matching the command line missed it on macOS, where Homebrew's +# interpreter shows up in ps as .../MacOS/Python (capital P), and a broader +# match would stop every dw.serve on the machine rather than the one on this +# port. Whether the new server is still starting is its process: dw.serve +# imports torch for seconds before it binds, so an empty port means nothing +# then. Stopping waits on the pids it signalled, since uvicorn closes its +# socket early in a graceful shutdown while the process still holds memory. +listener_pids() { if command -v lsof >/dev/null 2>&1; then lsof -ti "tcp:$DW_PORT" -sTCP:LISTEN 2>/dev/null || true else - pgrep -f 'python -m dw\.serve' || true + pgrep -if 'python[0-9.]* -m dw\.serve' || true fi } +server_running() { pgrep -if 'python[0-9.]* -m dw\.serve' >/dev/null; } +any_alive() { local p; for p in "$@"; do kill -0 "$p" 2>/dev/null && return 0; done; return 1; } # `ssh lem deploy.sh` runs a non-interactive shell, and a per-user node # install is usually put on PATH by ~/.bashrc *after* its "not interactive, @@ -147,16 +153,19 @@ if systemctl --user cat dw-serve >/dev/null 2>&1; then else LOG_HINT="$LOG" tail_log() { tail -n 20 "$LOG"; } -pids="$(server_pids)" +pids="$(listener_pids)" if [ -n "$pids" ]; then say "stopping dw.serve (pid $pids)" + # shellcheck disable=SC2086 # one word per pid kill -TERM $pids for _ in $(seq 1 30); do - [ -z "$(server_pids)" ] && break + # shellcheck disable=SC2086 + any_alive $pids || break sleep 1 done - if [ -n "$(server_pids)" ]; then - say "dw.serve did not exit within 30 s of SIGTERM (pid $(server_pids)); not escalating - report this"; exit 1 + # shellcheck disable=SC2086 + if any_alive $pids; then + say "dw.serve did not exit within 30 s of SIGTERM (pid $pids); not escalating - report this"; exit 1 fi else say "no dw.serve running" @@ -180,7 +189,7 @@ for i in $(seq 1 120); do say "deployed $branch @ $(git rev-parse --short HEAD)" exit 0 fi - [ -z "$(server_pids)" ] && { say "dw.serve exited during startup; last log lines ($LOG_HINT):"; tail_log; exit 1; } + server_running || { say "dw.serve exited during startup; last log lines ($LOG_HINT):"; tail_log; exit 1; } sleep 1 done say "no healthy answer within 120 s; last log lines ($LOG_HINT):"; tail_log; exit 1