diff --git a/tests/tui_gateway/test_compute_host_phase1.py b/tests/tui_gateway/test_compute_host_phase1.py index bb7368d..edf78cf 100644 --- a/tests/tui_gateway/test_compute_host_phase1.py +++ b/tests/tui_gateway/test_compute_host_phase1.py @@ -1,6 +1,7 @@ import io import json import os +import subprocess import sys import threading import time @@ -118,6 +119,35 @@ def test_append_log_record_single_write_lines(tmp_path): assert all(line.endswith("x" * 2000) for line in lines) +def test_append_log_record_across_processes_preserves_unicode(tmp_path): + path = tmp_path / "nested" / "shared.log" + child_code = """ +import sys, time +from tui_gateway.host_supervisor import append_log_record +for i in range(100): + append_log_record(sys.argv[1], f'{sys.argv[2]}:{i}:' + '\u0130\u015f\u011f\U0001f680' * 100) + time.sleep(0.001) +""" + children = [subprocess.Popen( + [sys.executable, "-c", child_code, str(path), str(index)], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) for index in range(4)] + try: + for child in children: + _, errors = child.communicate(timeout=30) + assert child.returncode == 0, errors.decode("utf-8", errors="replace") + finally: + for child in children: + if child.poll() is None: + child.kill() + child.communicate(timeout=10) + lines = path.read_text(encoding="utf-8").splitlines() + expected = {f"{process}:{index}:" + "İşğ🚀" * 100 for process in range(4) for index in range(100)} + assert len(lines) == len(expected) + assert set(lines) == expected + + def test_supervisor_startup_reconcile_pid_reuse_guard(tmp_path, monkeypatch): registry = tmp_path / "dashboard-compute-host.json" registry.write_text(json.dumps({"host_pid": os.getpid(), "boot_id": "stale"}), encoding="utf-8") diff --git a/tui_gateway/host_supervisor.py b/tui_gateway/host_supervisor.py index 9f8a7bd..ca50e3a 100644 --- a/tui_gateway/host_supervisor.py +++ b/tui_gateway/host_supervisor.py @@ -55,14 +55,35 @@ _LATE_CONTROL_MAX = 64 def append_log_record(path: str | Path, record: str) -> None: - """Append one log record using O_APPEND and exactly one os.write call.""" + """Append a record with one native write and kernel-owned EOF placement.""" p = Path(path) p.parent.mkdir(parents=True, exist_ok=True) text = record if record.endswith("\n") else f"{record}\n" data = text.encode("utf-8", errors="replace") + if sys.platform == "win32": + import ntsecuritycon + import win32con + import win32file + + # The Windows CRT's O_APPEND seeks before writing, so independent file + # descriptors can choose the same offset. Append-only access delegates + # EOF placement to the kernel, without granting overwrite permission. + handle = win32file.CreateFile( + str(p), ntsecuritycon.FILE_APPEND_DATA, + win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_DELETE, + None, win32con.OPEN_ALWAYS, win32con.FILE_ATTRIBUTE_NORMAL, None, + ) + try: + _, written = win32file.WriteFile(handle, data) + if written != len(data): + raise OSError("Incomplete log record write") + finally: + handle.Close() + return fd = os.open(str(p), os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600) try: - os.write(fd, data) + if os.write(fd, data) != len(data): + raise OSError("Incomplete log record write") finally: os.close(fd)