diff --git a/resources/scripts/nap_install.py b/resources/scripts/nap_install.py index ba35fd2..2726b49 100644 --- a/resources/scripts/nap_install.py +++ b/resources/scripts/nap_install.py @@ -682,15 +682,17 @@ def compact_log_file(path, max_bytes=MAX_SERVICE_LOG_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) + # Preserve the inode used by a running gateway process. Replacing the + # path here would leave its inherited stdout/stderr descriptors + # writing to a deleted file. + with 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 ensure_port_available(kind, port): diff --git a/tests/test_cli_command_system.py b/tests/test_cli_command_system.py index d935da0..7eba751 100644 --- a/tests/test_cli_command_system.py +++ b/tests/test_cli_command_system.py @@ -313,3 +313,19 @@ def test_manifest_rejects_a_minimum_contract_newer_than_installer(): assert "newer bootstrap installer" in str(exc) else: raise AssertionError("incompatible minimum contract must be rejected") + + +def test_cli_log_compaction_preserves_live_writer_inode(tmp_path): + module = load_installer() + 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: + module.compact_log_file(log_path, 128) + live_writer.write(b"after-cli-compaction\n") + + assert log_path.stat().st_ino == inode_before + content = log_path.read_bytes() + assert content.endswith(b"after-cli-compaction\n") + assert b"recent-line\n" in content