Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion hermes_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,39 @@
import os
import sys
import threading
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Optional, Sequence

# On Windows, stdlib ``RotatingFileHandler`` calls ``os.rename()`` in
# ``doRollover()`` and fails with ``PermissionError [WinError 32]`` whenever
# another process holds an append-mode handle on ``agent.log`` — which is
# essentially always in Hermes (TUI, gateway, ``hy_memory`` server, MCP
# servers, and on-demand CLI commands all log from separate processes),
# pinning ``agent.log`` at the 5 MiB threshold and spamming stderr with
# a traceback on every emit. ``concurrent-log-handler`` wraps the rename in a
# cross-process file lock (via ``portalocker``: pywin32 on Windows) so only
# one process rotates at a time and the others wait their turn.
#
# This swap is Windows-ONLY and deliberately so:
# * The bug (WinError 32 on rename-while-open) is specific to Windows file
# locking semantics — POSIX renames an open file fine, so stdlib already
# works correctly on Linux/macOS.
# * On POSIX, managed-mode (NixOS) relies on the exact ``_open()`` /
# ``doRollover()`` lifecycle of stdlib ``RotatingFileHandler`` (the
# ``_ManagedRotatingFileHandler`` subclass chmods 0660 after each). CLH
# opens lazily and rotates differently, which breaks the group-writable
# guarantee and the eager file-creation those paths depend on.
# Aliasing keeps every existing ``RotatingFileHandler`` reference in this
# module (class declaration, ``isinstance`` checks, docstring) working
# unchanged. See #44873.
if sys.platform == "win32":
from concurrent_log_handler import ( # noqa: E402
ConcurrentRotatingFileHandler as RotatingFileHandler,
)
else:
from logging.handlers import RotatingFileHandler # noqa: E402


from hermes_constants import get_config_path, get_hermes_home

# Sentinel to track whether setup_logging() has already run. The function
Expand Down
14 changes: 14 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ dependencies = [
# install rather than gating it behind an extra + a mid-session lazy install
# (which deadlocked the CLI under prompt_toolkit — see #40490).
"Pillow==12.2.0",
# Windows log rotation. Stdlib ``RotatingFileHandler.doRollover()`` uses
# ``os.rename()`` which fails with ``PermissionError [WinError 32]`` on
# Windows whenever any other process holds an append-mode handle on
# ``agent.log`` (always the case in Hermes — TUI, gateway, ``hy_memory``
# server, MCP servers, and on-demand CLI commands all log from separate
# processes), pinning ``agent.log`` at the 5 MiB threshold and spamming
# stderr on every emit (see #44873). ``concurrent-log-handler`` wraps the
# rename in a cross-process file lock (via ``portalocker``: pywin32 on
# Windows) so only one process rotates at a time. ``hermes_logging.py``
# aliases it ONLY on Windows — POSIX renames an open file fine, so stdlib
# already works there and managed-mode perms depend on its exact lifecycle.
# Hence the ``sys_platform == 'win32'`` marker: the dep (and its portalocker
# / pywin32 tree) ships only where it's actually used.
"concurrent-log-handler==0.9.29; sys_platform == 'win32'",
]

[project.optional-dependencies]
Expand Down
10 changes: 8 additions & 2 deletions tests/test_hermes_logging.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
"""Tests for hermes_logging — centralized logging setup."""

import io
import logging
import os
import stat
import sys
import threading
from logging.handlers import RotatingFileHandler
from pathlib import Path
from unittest.mock import patch

import pytest

import hermes_logging
# Use whatever RotatingFileHandler class hermes_logging actually resolved so
# the autouse fixture's isinstance checks (which strip rotating handlers
# between tests) match the real handlers on every platform. hermes_logging
# aliases concurrent-log-handler's ConcurrentRotatingFileHandler on Windows
# (the #44873 fix) but keeps stdlib RotatingFileHandler on POSIX, so importing
# the name from the module under test keeps the two in lockstep.
from hermes_logging import RotatingFileHandler


@pytest.fixture(autouse=True)
Expand Down
26 changes: 26 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading