Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
841311a6bd | ||
|
|
dd19be8bbf |
No files matched your search
@@ -11,6 +11,14 @@ Version numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.7.6] — 2026-06-11
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Update restart leaked the listening socket and hopped to port 5101** — Werkzeug marks its server socket inheritable (`srv.socket.set_inheritable(True)`, unconditionally, for its debug reloader), so the in-app update's `os.execv` restart carried the old listening socket into the new process as a zombie listener: same PID listening on both 5100 (never accepted — clients hang) and 5101 (the actual server). The 1.7.3 `SO_REUSEADDR`/grace-period fix couldn't help because the port genuinely was occupied — by the restarting process itself. `_restart_self()` now marks every fd above stderr close-on-exec before the exec (`_mark_fds_cloexec()`, enumerating `/proc/self/fd` on Linux), so the old socket dies with the exec and the new server rebinds 5100 immediately.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.7.5] — 2026-06-11
|
## [1.7.5] — 2026-06-11
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# GDPR Scanner — Brugermanual
|
# GDPR Scanner — Brugermanual
|
||||||
|
|
||||||
Version 1.7.5
|
Version 1.7.6
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -671,4 +671,4 @@ For en typisk skole- eller kommunescanning er omkostningen ubetydelig — Claude
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*GDPR Scanner v1.7.5 — teknisk opsætning og konfiguration: se README.md*
|
*GDPR Scanner v1.7.6 — teknisk opsætning og konfiguration: se README.md*
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# GDPR Scanner — User Manual
|
# GDPR Scanner — User Manual
|
||||||
|
|
||||||
Version 1.7.5
|
Version 1.7.6
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -671,4 +671,4 @@ For a typical school or municipality scan the cost is negligible — Claude Haik
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*GDPR Scanner v1.7.5 — for technical setup and configuration see README.md*
|
*GDPR Scanner v1.7.6 — for technical setup and configuration see README.md*
|
||||||
+23
-2
@@ -118,13 +118,34 @@ def apply_update() -> dict:
|
|||||||
"from": chk["current"], "to": chk["latest"]}
|
"from": chk["current"], "to": chk["latest"]}
|
||||||
|
|
||||||
|
|
||||||
|
def _mark_fds_cloexec() -> None:
|
||||||
|
"""Mark every fd above stderr close-on-exec.
|
||||||
|
|
||||||
|
Werkzeug calls ``srv.socket.set_inheritable(True)`` unconditionally
|
||||||
|
(for its debug reloader), so without this the listening socket leaks
|
||||||
|
into the exec'd process: it sits on the port as a zombie listener no
|
||||||
|
one accepts from, the port probe sees the port as busy, and the new
|
||||||
|
server hops to port+1 while clients hang against the dead socket.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
fds = [int(f) for f in os.listdir("/proc/self/fd")] # Linux
|
||||||
|
except (OSError, ValueError):
|
||||||
|
fds = list(range(3, 4096))
|
||||||
|
for fd in fds:
|
||||||
|
if fd > 2:
|
||||||
|
try:
|
||||||
|
os.set_inheritable(fd, False)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _restart_self() -> None:
|
def _restart_self() -> None:
|
||||||
"""Re-exec the current process so the updated code is loaded.
|
"""Re-exec the current process so the updated code is loaded.
|
||||||
|
|
||||||
Keeps the same PID, so it works both under systemd and when launched
|
Keeps the same PID, so it works both under systemd and when launched
|
||||||
manually via start_gdpr.sh. Listening sockets are close-on-exec, so
|
manually via start_gdpr.sh.
|
||||||
the new process can rebind the port.
|
|
||||||
"""
|
"""
|
||||||
|
_mark_fds_cloexec()
|
||||||
try:
|
try:
|
||||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||||
except OSError:
|
except OSError:
|
||||||
|
|||||||
@@ -188,6 +188,23 @@ def test_apply_installs_requirements_when_changed(client, monkeypatch):
|
|||||||
assert "pip" in pip_calls[0] and "-r" in pip_calls[0]
|
assert "pip" in pip_calls[0] and "-r" in pip_calls[0]
|
||||||
|
|
||||||
|
|
||||||
|
# ── Restart fd hygiene ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_mark_fds_cloexec_unmarks_inheritable_socket():
|
||||||
|
"""Werkzeug sets the listening socket inheritable; the restart must undo
|
||||||
|
that or the socket leaks through execv and squats on the port."""
|
||||||
|
import socket
|
||||||
|
import routes.updates as upd
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
try:
|
||||||
|
s.set_inheritable(True)
|
||||||
|
assert s.get_inheritable() is True
|
||||||
|
upd._mark_fds_cloexec()
|
||||||
|
assert s.get_inheritable() is False
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
|
||||||
# ── /api/update/settings ──────────────────────────────────────────────────────
|
# ── /api/update/settings ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
def test_settings_roundtrip(client, monkeypatch):
|
def test_settings_roundtrip(client, monkeypatch):
|
||||||
|
|||||||
Reference in new issue
Block a user