From 908c5a6a69a94a081807f10b4d1d1e069e10f082 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 16 Aug 2026 09:24:13 -0700 Subject: [PATCH] =?UTF-8?q?fix(upgrade):=20/gstack-upgrade=20stops=20a=20s?= =?UTF-8?q?tale=20daemon=20=E2=80=94=20deferring=20to=20a=20busy=20one=20(?= =?UTF-8?q?#2551)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A browse daemon started before an upgrade keeps serving the OLD binary's code after `git reset --hard` + `./setup` — the running process holds the old executable, so users on the "new" version kept getting pre-upgrade behavior (and config-mismatch refusals against the new CLI) until they happened to stop it by hand. New unconditional Step 4.8 in gstack-upgrade/SKILL.md.tmpl (+ regen, same commit): compare the running daemon's recorded binaryVersion (the readVersionHash git-SHA the server stamps into its state file) against the freshly built browse/dist/.version. - Stale + responsive → `browse stop` (graceful), telling the user old→new hash; the next command boots a daemon on the new binary. - Stale + BUSY → DEFER (decision 10): never kill a busy daemon during upgrade. Print the old→new hash and the escape hatch — `browse stop` when it finishes, or `browse --force-restart stop` now. - Dead pid / matching hash / no state → silent no-op. Tests: skill-validation + gen-skill-docs 731 pass after regen. Fixes #2551. Co-Authored-By: Claude Fable 5 --- gstack-upgrade/SKILL.md | 44 ++++++++++++++++++++++++++++++++++++ gstack-upgrade/SKILL.md.tmpl | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/gstack-upgrade/SKILL.md b/gstack-upgrade/SKILL.md index 9f0f2f7ea..747685b86 100644 --- a/gstack-upgrade/SKILL.md +++ b/gstack-upgrade/SKILL.md @@ -220,6 +220,50 @@ Migrations are idempotent bash scripts in `gstack-upgrade/migrations/`. Each is `v{VERSION}.sh` and runs only when upgrading from an older version. See CONTRIBUTING.md for how to add new migrations. +### Step 4.8: Stop any stale daemon (unconditional) + +A browse daemon started before the upgrade keeps serving the OLD binary's code +until it is stopped — it survives `git reset --hard` and `./setup` because the +running process holds the old executable (#2551). Always run this step, using +the install directory detected in Step 2. + +```bash +INSTALL_DIR_PLACEHOLDER="" +NEW_HASH=$(cat "$INSTALL_DIR_PLACEHOLDER/browse/dist/.version" 2>/dev/null || echo "") +_STATE_FILE="${BROWSE_STATE_FILE:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/browse.json}" +if [ -z "$NEW_HASH" ] || [ ! -f "$_STATE_FILE" ]; then + echo "DAEMON_CHECK=none (no state file or no fresh build hash)" +else + DAEMON_PID=$(sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$_STATE_FILE" | head -1) + DAEMON_PORT=$(sed -n 's/.*"port"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$_STATE_FILE" | head -1) + OLD_HASH=$(sed -n 's/.*"binaryVersion"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_STATE_FILE" | head -1) + if [ -z "$DAEMON_PID" ] || ! kill -0 "$DAEMON_PID" 2>/dev/null; then + echo "DAEMON_CHECK=dead (no live daemon to stop)" + elif [ "$OLD_HASH" = "$NEW_HASH" ]; then + echo "DAEMON_CHECK=current (daemon already runs the new binary)" + elif curl -fsS --max-time 2 "http://127.0.0.1:$DAEMON_PORT/health" 2>/dev/null | grep -q '"status":"healthy"'; then + echo "DAEMON_CHECK=stale-responsive pid=$DAEMON_PID hash=${OLD_HASH:-unknown} -> $NEW_HASH" + "$INSTALL_DIR_PLACEHOLDER/browse/dist/browse" stop && echo "DAEMON_STOPPED=yes" + else + echo "DAEMON_CHECK=stale-busy pid=$DAEMON_PID hash=${OLD_HASH:-unknown} -> $NEW_HASH" + fi +fi +``` + +Replace `` with the actual install directory before +running. Interpret the `DAEMON_CHECK` result: + +1. **`stale-responsive` + `DAEMON_STOPPED=yes`:** Tell the user "Stopped the + old browse daemon (binary {OLD_HASH} → {NEW_HASH}). The next browse command + starts a fresh daemon on the new binary." +2. **`stale-busy`:** The daemon runs the old binary but is mid-work — DEFER to + it, never kill a busy daemon during upgrade. Tell the user: "A browse daemon + is still running the pre-upgrade binary ({OLD_HASH} → {NEW_HASH}) but is + busy right now. When it finishes, stop it with `browse stop` — or force it + immediately with `browse --force-restart stop` (loses that session's + tabs/cookies/logins)." +3. **`none` / `dead` / `current`:** Nothing to do — say nothing. + ### Step 5: Write marker + clear cache ```bash diff --git a/gstack-upgrade/SKILL.md.tmpl b/gstack-upgrade/SKILL.md.tmpl index 5402a1da3..0abe9ce50 100644 --- a/gstack-upgrade/SKILL.md.tmpl +++ b/gstack-upgrade/SKILL.md.tmpl @@ -217,6 +217,50 @@ Migrations are idempotent bash scripts in `gstack-upgrade/migrations/`. Each is `v{VERSION}.sh` and runs only when upgrading from an older version. See CONTRIBUTING.md for how to add new migrations. +### Step 4.8: Stop any stale daemon (unconditional) + +A browse daemon started before the upgrade keeps serving the OLD binary's code +until it is stopped — it survives `git reset --hard` and `./setup` because the +running process holds the old executable (#2551). Always run this step, using +the install directory detected in Step 2. + +```bash +INSTALL_DIR_PLACEHOLDER="" +NEW_HASH=$(cat "$INSTALL_DIR_PLACEHOLDER/browse/dist/.version" 2>/dev/null || echo "") +_STATE_FILE="${BROWSE_STATE_FILE:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.gstack/browse.json}" +if [ -z "$NEW_HASH" ] || [ ! -f "$_STATE_FILE" ]; then + echo "DAEMON_CHECK=none (no state file or no fresh build hash)" +else + DAEMON_PID=$(sed -n 's/.*"pid"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$_STATE_FILE" | head -1) + DAEMON_PORT=$(sed -n 's/.*"port"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$_STATE_FILE" | head -1) + OLD_HASH=$(sed -n 's/.*"binaryVersion"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_STATE_FILE" | head -1) + if [ -z "$DAEMON_PID" ] || ! kill -0 "$DAEMON_PID" 2>/dev/null; then + echo "DAEMON_CHECK=dead (no live daemon to stop)" + elif [ "$OLD_HASH" = "$NEW_HASH" ]; then + echo "DAEMON_CHECK=current (daemon already runs the new binary)" + elif curl -fsS --max-time 2 "http://127.0.0.1:$DAEMON_PORT/health" 2>/dev/null | grep -q '"status":"healthy"'; then + echo "DAEMON_CHECK=stale-responsive pid=$DAEMON_PID hash=${OLD_HASH:-unknown} -> $NEW_HASH" + "$INSTALL_DIR_PLACEHOLDER/browse/dist/browse" stop && echo "DAEMON_STOPPED=yes" + else + echo "DAEMON_CHECK=stale-busy pid=$DAEMON_PID hash=${OLD_HASH:-unknown} -> $NEW_HASH" + fi +fi +``` + +Replace `` with the actual install directory before +running. Interpret the `DAEMON_CHECK` result: + +1. **`stale-responsive` + `DAEMON_STOPPED=yes`:** Tell the user "Stopped the + old browse daemon (binary {OLD_HASH} → {NEW_HASH}). The next browse command + starts a fresh daemon on the new binary." +2. **`stale-busy`:** The daemon runs the old binary but is mid-work — DEFER to + it, never kill a busy daemon during upgrade. Tell the user: "A browse daemon + is still running the pre-upgrade binary ({OLD_HASH} → {NEW_HASH}) but is + busy right now. When it finishes, stop it with `browse stop` — or force it + immediately with `browse --force-restart stop` (loses that session's + tabs/cookies/logins)." +3. **`none` / `dead` / `current`:** Nothing to do — say nothing. + ### Step 5: Write marker + clear cache ```bash