diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cb98f4e..1aabd8f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ - **Windows pytest-harness compatibility (#3664).** Hardened the test suite to run on Windows: profile-home fallback paths are path-normalized, strict POSIX file-mode (`0o600`) assertions are gated behind `os.name != "nt"` (Linux still asserts them at full strictness), the conftest cleanup handles Windows process-tree/port teardown and the Py3.12+ `shutil.rmtree` `onexc` shim, and tests that require `fork`/`fcntl` carry `@requires_fork` / `@requires_fcntl` markers (which never skip on Linux). Test-only — no runtime or app behavior change, no Linux CI behavior change. (#4254, #4255, #4256, #4257, #4259, #4263, #4266, #4274) +## [v0.51.456] — 2026-06-16 — Release PQ (start.sh tells you when the server is already running) + +### Changed + +- **`./start.sh` now detects an already-running server instead of pretending to start a new one.** Previously, running `./start.sh` while a server was already bound to the host:port would have `bootstrap.py` spawn a child that fails to bind and dies, while the existing instance answered the `/health` probe — so the script reported "ready" and exited 0 without actually starting anything. `start.sh` now probes `/health` first (best-effort, 2s timeout, via `curl`/`wget`; skipped if neither is present) and, when a server already answers, prints a clear message that nothing was (re)started plus how to restart (`./ctl.sh restart`, or stop-the-PID + `./start.sh`), then exits 0. This brings the detached `start.sh` path to parity with `ctl.sh`'s existing double-start refusal. The container path (which runs `server.py` directly) is unaffected. (#4308) + ## [v0.51.455] — 2026-06-16 — Release PP (drop stale background-process completions instead of prepending them to a later turn) ### Fixed diff --git a/start.sh b/start.sh index 8a37bf40..f3d1d14d 100755 --- a/start.sh +++ b/start.sh @@ -62,4 +62,95 @@ if [[ -z "${PYTHON}" ]]; then fi fi +# Pre-flight: detect an already-running server before launching bootstrap.py. +# +# bootstrap.py's detached (non-foreground) path spawns server.py, then probes +# /health and reports success once *anything* answers. If a server is already +# bound to this host:port, the freshly spawned child fails to bind and dies, +# but the EXISTING (often orphaned) server answers the /health probe — so +# bootstrap.py prints "ready" and exits 0 without having started anything. The +# user just keeps re-confirming the old instance on every run. +# +# To avoid that, probe /health here first. If a server is already up, tell the +# user plainly that nothing was (re)started and how to restart, then exit 0. +# ctl.sh already refuses to double-start via PID/state tracking; this brings +# the detached start.sh path to parity using a health probe (start.sh keeps no +# PID file of its own). +# +# Resolve host/port the same way bootstrap.py does: HERMES_WEBUI_HOST / +# HERMES_WEBUI_PORT (possibly just sourced from .env above), else the +# bootstrap.py defaults of 127.0.0.1 / 8787. A 0.0.0.0 / :: bind is probed via +# loopback, matching server.py's _abort_if_already_serving. +_hermes_host="${HERMES_WEBUI_HOST:-127.0.0.1}" +_hermes_port="${HERMES_WEBUI_PORT:-8787}" + +# CLI args override the env/defaults exactly as bootstrap.py's argparse does +# (`port` is the first bare numeric positional; `--host VALUE` / `--host=VALUE`). +# Without this, `./start.sh ` or `--host X` would probe the wrong endpoint +# and could falsely report "already running" against a different instance. +_hermes_args=("$@") +_hermes_i=0 +while [[ ${_hermes_i} -lt ${#_hermes_args[@]} ]]; do + _hermes_arg="${_hermes_args[${_hermes_i}]}" + case "${_hermes_arg}" in + --host) + _hermes_next=$(( _hermes_i + 1 )) + if [[ ${_hermes_next} -lt ${#_hermes_args[@]} ]]; then + _hermes_host="${_hermes_args[${_hermes_next}]}" + _hermes_i=${_hermes_next} + fi + ;; + --host=*) + _hermes_host="${_hermes_arg#--host=}" + ;; + --*) + : # other flags (e.g. --no-browser) carry no positional value here + ;; + *) + # First bare numeric positional is the port (bootstrap.py: nargs="?"). + if [[ "${_hermes_arg}" =~ ^[0-9]+$ ]]; then + _hermes_port="${_hermes_arg}" + fi + ;; + esac + _hermes_i=$(( _hermes_i + 1 )) +done + +case "${_hermes_host}" in + 0.0.0.0|""|::|"[::]") _hermes_probe_host="127.0.0.1" ;; + *) _hermes_probe_host="${_hermes_host}" ;; +esac +_hermes_health_url="http://${_hermes_probe_host}:${_hermes_port}/health" + +# Best-effort probe. If neither curl nor wget is present we skip the check and +# fall through to the normal launch (unchanged behavior). Short 2s timeout so a +# normal cold start is not delayed. +_hermes_already_up="" +if command -v curl >/dev/null 2>&1; then + _hermes_already_up="$(curl -fsS --max-time 2 "${_hermes_health_url}" 2>/dev/null || true)" +elif command -v wget >/dev/null 2>&1; then + _hermes_already_up="$(wget -qO- --timeout=2 --tries=1 "${_hermes_health_url}" 2>/dev/null || true)" +fi + +if [[ -n "${_hermes_already_up}" ]]; then + cat >&2 < + 3. Start it again: + ./start.sh +EOF + exit 0 +fi + exec "${PYTHON}" "${REPO_ROOT}/bootstrap.py" --no-browser "$@"