From fccdfd58df66f0710909f9f86ee2ecaa7eabb1fa Mon Sep 17 00:00:00 2001 From: HumbleTurtle Date: Sat, 1 Aug 2026 04:18:00 -0300 Subject: [PATCH] Preserve live gateway log during compaction --- resources/scripts/napseer_mcp_server.py | 20 ++++++++++-------- tests/test_gateway_log_compaction.py | 27 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 tests/test_gateway_log_compaction.py diff --git a/resources/scripts/napseer_mcp_server.py b/resources/scripts/napseer_mcp_server.py index 9f50212..1ca2eaa 100755 --- a/resources/scripts/napseer_mcp_server.py +++ b/resources/scripts/napseer_mcp_server.py @@ -574,6 +574,7 @@ GATEWAY_LISTENER_ACTIVE_LANES = set() GATEWAY_LISTENER_UNSTABLE_UNTIL = 0.0 GATEWAY_LISTENER_STABILITY_LOCK = threading.RLock() AUTH_RENEW_LOCK = threading.Lock() +GATEWAY_LOG_LOCK = threading.Lock() GATEWAY_VAULT_SETUP_THREAD_STARTED = False GATEWAY_PERIODIC_LOG_STATE = {} WS_CLOSE_DETAILS = {} @@ -595,7 +596,8 @@ def gateway_log(event, level="info", **fields): payload[key] = value else: payload[key] = str(value) - print(json.dumps(payload, separators=(",", ":"), sort_keys=True), file=sys.stderr, flush=True) + with GATEWAY_LOG_LOCK: + print(json.dumps(payload, separators=(",", ":"), sort_keys=True), file=sys.stderr, flush=True) def gateway_periodic_log(key, interval_seconds, event, level="info", **fields): @@ -629,15 +631,17 @@ def compact_log_file(path, max_bytes): newline_index = chunk.find(b"\n") if newline_index >= 0 and newline_index + 1 < len(chunk): chunk = chunk[newline_index + 1 :] - tmp_path = file_path.with_suffix(file_path.suffix + ".tmp") try: - tmp_path.write_bytes(chunk) - tmp_path.replace(file_path) + # Keep the inode stable: the daemon's stdout/stderr already point to + # this file. Replacing the pathname would leave the live process + # writing to a deleted inode and make `nap gateway logs` stale. + with GATEWAY_LOG_LOCK, file_path.open("r+b") as handle: + handle.seek(0) + handle.write(chunk) + handle.truncate() + handle.flush() except OSError: - try: - tmp_path.unlink(missing_ok=True) - except OSError: - pass + return def start_gateway_log_compactor_thread(): diff --git a/tests/test_gateway_log_compaction.py b/tests/test_gateway_log_compaction.py new file mode 100644 index 0000000..b957eb1 --- /dev/null +++ b/tests/test_gateway_log_compaction.py @@ -0,0 +1,27 @@ +import importlib.util +import pathlib + + +def load_module(): + script_path = pathlib.Path(__file__).resolve().parents[1] / "resources" / "scripts" / "napseer_mcp_server.py" + spec = importlib.util.spec_from_file_location("napseer_gateway_log_compaction_test", script_path) + module = importlib.util.module_from_spec(spec) + assert spec and spec.loader + spec.loader.exec_module(module) + return module + + +def test_log_compaction_preserves_live_writer_inode(tmp_path): + mod = load_module() + log_path = tmp_path / "gateway.log" + log_path.write_bytes((b"old-line\n" * 200) + b"recent-line\n") + inode_before = log_path.stat().st_ino + + with log_path.open("ab", buffering=0) as live_writer: + mod.compact_log_file(log_path, 128) + live_writer.write(b"after-compaction\n") + + assert log_path.stat().st_ino == inode_before + content = log_path.read_bytes() + assert content.endswith(b"after-compaction\n") + assert b"recent-line\n" in content