## Release v0.51.234 — Release HB (stage-q4) Two medium-risk backend/infra fixes. All gates green. ### Fixes | PR | Author | Fix | |----|--------|-----| | #3289 | @rodboev | Refuse server startup when a live instance already serves the port (Windows/macOS silent port-sharing hazard). Live-listener probe (`GET /health`, 2s timeout) + Windows `SO_EXCLUSIVEADDRUSE` — **preserves fast restart** (POSIX keeps `allow_reuse_address=True`; a dying socket in the kernel backlog times out → startup proceeds). | | #3486 | @dso2ng | Allow remote/SSH terminal profiles to use target-side workspace paths under `terminal.cwd` without a server-local `stat()`. Local profiles unchanged — bypass only fires for remote backends and only for paths contained within `terminal.cwd`. | ### History note on #3289 This PR was **held earlier this sweep** — its original form globally disabled `SO_REUSEADDR`, which a Codex gate flagged as breaking fast restart (TIME_WAIT bricks rebind for ~60s). The contributor reworked it along the suggested lines (live-listener probe instead of the global disable). This release ships the reworked version. Unheld → full pickup → full gate. ### Gate results - **Full pytest suite**: 7458 passed, 8 skipped, 3 xpassed, **0 failed** - **ruff forward gate**: CLEAN - **browser-smoke gate**: CLEAN (real server boots fine with the new startup probe) - **Codex (regression)**: SAFE TO SHIP (verified fast-rebind preserved + remote bypass gated on backend+containment, local validation unchanged) - **Opus (correctness + security)**: SAFE TO SHIP (probe false-positive, `_is_within` containment, local-profile bypass all hold up; applied its one minor double-call cleanup note) Closes #3289. Co-authored-by: rodboev <rodboev@users.noreply.github.com> Co-authored-by: dso2ng <dso2ng@users.noreply.github.com>
91 lines
3.0 KiB
Python
91 lines
3.0 KiB
Python
"""Duplicate-instance guard: a second server on the same port must be detected
|
|
and refused before bind, not silently shared (#3289)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import socket
|
|
import sys
|
|
import threading
|
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
|
|
import pytest
|
|
|
|
from tests._pytest_port import TEST_PORT
|
|
|
|
|
|
# ── SO_EXCLUSIVEADDRUSE on Windows ──────────────────────────────────────────
|
|
|
|
@pytest.mark.skipif(sys.platform != 'win32', reason='Windows-only socket option')
|
|
def test_exclusive_addr_use_set_on_windows():
|
|
from server import QuietHTTPServer
|
|
port = TEST_PORT + 901
|
|
httpd = QuietHTTPServer(('127.0.0.1', port), BaseHTTPRequestHandler)
|
|
try:
|
|
val = httpd.socket.getsockopt(
|
|
socket.SOL_SOCKET,
|
|
getattr(socket, 'SO_EXCLUSIVEADDRUSE', -5),
|
|
)
|
|
assert val != 0, 'SO_EXCLUSIVEADDRUSE should be set on Windows'
|
|
finally:
|
|
httpd.server_close()
|
|
|
|
|
|
# ── Live-listener probe ─────────────────────────────────────────────────────
|
|
|
|
def test_probe_detects_live_server():
|
|
"""_abort_if_already_serving must call sys.exit when a live server responds."""
|
|
from server import _abort_if_already_serving
|
|
|
|
port = TEST_PORT + 902
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def do_GET(self): # noqa: N802
|
|
self.send_response(200)
|
|
self.end_headers()
|
|
self.wfile.write(b'ok')
|
|
def log_message(self, *a):
|
|
pass
|
|
|
|
httpd = HTTPServer(('127.0.0.1', port), Handler)
|
|
t = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
t.start()
|
|
try:
|
|
with pytest.raises(SystemExit):
|
|
_abort_if_already_serving('127.0.0.1', port)
|
|
finally:
|
|
httpd.shutdown()
|
|
httpd.server_close()
|
|
|
|
|
|
def test_probe_allows_startup_when_nothing_listening():
|
|
"""_abort_if_already_serving must return normally on a free port."""
|
|
from server import _abort_if_already_serving
|
|
|
|
port = TEST_PORT + 903
|
|
_abort_if_already_serving('127.0.0.1', port)
|
|
|
|
|
|
def test_probe_allows_startup_on_unresponsive_socket():
|
|
"""A socket that accepts but never responds (e.g. dying instance still in
|
|
kernel backlog) should not block startup."""
|
|
from server import _abort_if_already_serving
|
|
|
|
port = TEST_PORT + 904
|
|
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
srv.bind(('127.0.0.1', port))
|
|
srv.listen(1)
|
|
try:
|
|
_abort_if_already_serving('127.0.0.1', port)
|
|
finally:
|
|
srv.close()
|
|
|
|
|
|
def test_probe_normalizes_wildcard_host():
|
|
"""0.0.0.0 and :: should probe 127.0.0.1, not the literal wildcard."""
|
|
from server import _abort_if_already_serving
|
|
|
|
port = TEST_PORT + 905
|
|
_abort_if_already_serving('0.0.0.0', port)
|
|
_abort_if_already_serving('::', port)
|