fix(audit): threading.Lock on AuditLog.log + close (P1 bug)

detection thread and async heartbeat call log() concurrently.
Without a lock, two threads can both see today != _current_date
and double-open the file, corrupting the handle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-17 10:16:28 +00:00
parent c6714e8d5e
commit fd04fcd5e6

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import threading
from datetime import datetime, date
from pathlib import Path
from typing import Callable, IO
@@ -16,28 +17,28 @@ class AuditLog:
self._clock: Callable[[], datetime] = clock or datetime.now
self._current_date: date | None = None
self._fh: IO[str] | None = None
self._lock = threading.Lock()
def log(self, event: dict) -> None:
now = self._clock()
today = now.date()
if today != self._current_date:
self._open(today)
if "ts" not in event:
event = {**event, "ts": now.isoformat()}
assert self._fh is not None
self._fh.write(json.dumps(event, separators=(",", ":")) + "\n")
with self._lock:
if today != self._current_date:
self._open(today)
assert self._fh is not None
self._fh.write(json.dumps(event, separators=(",", ":")) + "\n")
def close(self) -> None:
if self._fh is not None:
try:
self._fh.close()
except Exception:
pass
finally:
self._fh = None
with self._lock:
if self._fh is not None:
try:
self._fh.close()
except Exception:
pass
finally:
self._fh = None
@property
def current_path(self) -> Path: