Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
841311a6bd | ||
|
|
dd19be8bbf | ||
|
|
c43725ca7f | ||
|
|
a1712ae178 | ||
|
|
c1cddb8ea7 | ||
|
|
9cbd93e1f5 |
No files matched your search
@@ -11,6 +11,30 @@ 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
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Stale UI after updating the server** — Flask served `/static/` files with no `Cache-Control` header, so browsers cached JS/CSS heuristically (often for days). After a server update — including the new in-app self-update, whose post-install reload hit the cache — the backend was new but the frontend stayed old, and fixes appeared "not to work" until a hard refresh. `SEND_FILE_MAX_AGE_DEFAULT = 0` now makes every static file revalidate via ETag: unchanged files answer with a cheap 304, changed files are re-fetched immediately on the next normal page load.
|
||||
|
||||
---
|
||||
|
||||
## [1.7.4] — 2026-06-10
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Share modal kept stale input after creating a link** — clicking "Create" only cleared the label field; scope type, user email, date range, and expiry kept their values, so the next link silently inherited the previous link's scope settings. The form-reset logic from `openShareModal()` is now a shared `_resetShareForm()` helper called after every successful create (the generated link row stays visible for copying).
|
||||
|
||||
---
|
||||
|
||||
## [1.7.3] — 2026-06-10
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# GDPR Scanner — Brugermanual
|
||||
|
||||
Version 1.7.3
|
||||
Version 1.7.6
|
||||
|
||||
---
|
||||
|
||||
@@ -671,4 +671,4 @@ For en typisk skole- eller kommunescanning er omkostningen ubetydelig — Claude
|
||||
|
||||
---
|
||||
|
||||
*GDPR Scanner v1.7.3 — 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
|
||||
|
||||
Version 1.7.3
|
||||
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.3 — for technical setup and configuration see README.md*
|
||||
*GDPR Scanner v1.7.6 — for technical setup and configuration see README.md*
|
||||
@@ -317,6 +317,11 @@ app = Flask(__name__,
|
||||
template_folder=_os.path.join(_BASE_DIR, "templates"),
|
||||
static_folder=_os.path.join(_BASE_DIR, "static"))
|
||||
|
||||
# Static files must revalidate on every load (cheap 304s via ETag). Without
|
||||
# this there is no Cache-Control header and browsers cache JS/CSS heuristically
|
||||
# for days — after a self-update the backend is new but the UI stays stale.
|
||||
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0
|
||||
|
||||
# Session secret — derived from machine_id so it survives restarts without a separate file.
|
||||
# machine_id is also the Fernet key (base64-encoded 32 bytes); we use its raw bytes as the secret.
|
||||
try:
|
||||
|
||||
+23
-2
@@ -118,13 +118,34 @@ def apply_update() -> dict:
|
||||
"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:
|
||||
"""Re-exec the current process so the updated code is loaded.
|
||||
|
||||
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
|
||||
the new process can rebind the port.
|
||||
manually via start_gdpr.sh.
|
||||
"""
|
||||
_mark_fds_cloexec()
|
||||
try:
|
||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||
except OSError:
|
||||
|
||||
+8
-4
@@ -138,9 +138,7 @@ function _shareScopeTypeChanged() {
|
||||
if (type === 'user') _initUserAutocomplete();
|
||||
}
|
||||
|
||||
function openShareModal() {
|
||||
document.getElementById('shareBackdrop').classList.add('open');
|
||||
document.getElementById('shareNewLinkRow').style.display = 'none';
|
||||
function _resetShareForm() {
|
||||
document.getElementById('shareLabel').value = '';
|
||||
document.getElementById('shareExpiry').value = '30';
|
||||
const scopeType = document.getElementById('shareScopeType');
|
||||
@@ -152,6 +150,12 @@ function openShareModal() {
|
||||
if (scopeDrop) scopeDrop.style.display = 'none';
|
||||
const vf = document.getElementById('shareValidFrom'); if (vf) vf.value = '';
|
||||
const vt = document.getElementById('shareValidTo'); if (vt) vt.value = '';
|
||||
}
|
||||
|
||||
function openShareModal() {
|
||||
document.getElementById('shareBackdrop').classList.add('open');
|
||||
document.getElementById('shareNewLinkRow').style.display = 'none';
|
||||
_resetShareForm();
|
||||
_renderTokenList();
|
||||
fetch('/api/viewer/pin').then(function(r){ return r.json(); }).then(function(d) {
|
||||
const el = document.getElementById('sharePinStatus');
|
||||
@@ -265,7 +269,7 @@ async function createShareLink() {
|
||||
urlInput.value = url;
|
||||
document.getElementById('shareNewLinkRow').style.display = 'block';
|
||||
document.getElementById('shareCopyBtn').textContent = t('log_copy', 'Copy');
|
||||
document.getElementById('shareLabel').value = '';
|
||||
_resetShareForm();
|
||||
_renderTokenList();
|
||||
} catch(e) {
|
||||
alert(t('share_create_error', 'Failed to create link:') + ' ' + e.message);
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
# ── 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 ──────────────────────────────────────────────────────
|
||||
|
||||
def test_settings_roundtrip(client, monkeypatch):
|
||||
|
||||
Reference in new issue
Block a user