Keep the same port across app restarts

The port probe did a plain bind() without SO_REUSEADDR, so TIME_WAIT
connections left by the previous instance (e.g. the in-app update
restart) made the port look occupied and the app hopped to the next
one. Probe with SO_REUSEADDR like Werkzeug binds, and give the
requested port a 10-second grace period before auto-incrementing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
StyxX65 2026-06-10 15:23:18 +02:00
parent 679f91da2c
commit d6bf80a68a
2 changed files with 28 additions and 7 deletions

View File

@ -11,6 +11,8 @@ Version numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html
### Fixed
- **App restart no longer hops to a new port** — the in-app update restart (and any quick stop/start) left connections from the previous instance in TIME_WAIT, and the startup port probe did a plain `bind()` that treats TIME_WAIT as occupied — so the restarted app silently came up on 5101 and the browser's reload poll never found it. The probe now sets `SO_REUSEADDR` (matching how Werkzeug actually binds, so an actively listening port is still detected as occupied), and the requested port gets a 10-second grace period before the auto-increment fallback kicks in, covering the brief window where the old process hasn't fully released the socket.
- **Share links now respect a reverse proxy**`_getShareBaseUrl()` rewrote every copied share link to `http://<LAN-IP>:5100` (via `/api/local_ip`), which would bypass TLS when the scanner sits behind a reverse proxy (Zoraxy, Caddy, nginx, …): a DPO opening the link would silently fall back to plain HTTP. The LAN-IP rewrite now only applies in the case it was built for — browsing the app at `localhost` over HTTP, where `window.location.origin` would produce links unusable from other machines. Any HTTPS or non-localhost origin is used as-is.
---

View File

@ -2261,14 +2261,33 @@ Example --settings file with SMTP:
# Find a free port — auto-increment from the requested port if in use.
import socket as _socket
def _can_bind(p: int, host: str) -> bool:
with _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM) as s:
# Probe with SO_REUSEADDR, matching how Werkzeug binds.
# Without it, connections left in TIME_WAIT by a previous
# instance (e.g. the in-app update restart) make the port
# look occupied and the app silently moves to the next one.
s.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
try:
s.bind((host, p))
return True
except OSError:
return False
def _find_free_port(start: int, host: str) -> int:
for p in range(start, start + 100):
with _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM) as s:
try:
s.bind((host, p))
return p
except OSError:
continue
# Give the requested port a grace period — after a self-restart
# the previous process may not have released it yet.
deadline = time.time() + 10
while True:
if _can_bind(start, host):
return start
if time.time() >= deadline:
break
time.sleep(0.5)
for p in range(start + 1, start + 100):
if _can_bind(p, host):
return p
raise RuntimeError(f"No free port found in range {start}{start + 99}")
actual_port = _find_free_port(args.port, args.host)