mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-17 04:31:55 +00:00
Fix auth store file lock for Windows (msvcrt) with reentrancy support
fcntl is not available on Windows. This adds msvcrt.locking as a fallback for cross-process advisory locking on Windows. msvcrt.locking is not reentrant within the same thread, unlike fcntl.flock. This matters because resolve_codex_runtime_credentials holds the lock and then calls _save_codex_tokens, which tries to acquire it again. Without reentrancy tracking, this deadlocks on Windows after a 15-second timeout. Uses threading.local() to track lock depth per thread, allowing nested acquisitions to pass through without re-acquiring the underlying lock. Also handles msvcrt-specific requirements: file must be opened in r+ mode (not a+), must have at least 1 byte of content, and the file pointer must be at position 0 before locking.
This commit is contained in:
parent
21d61bdd71
commit
48e65631f6
1 changed files with 45 additions and 7 deletions
|
|
@ -23,6 +23,7 @@ import stat
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
@ -44,6 +45,10 @@ try:
|
||||||
import fcntl
|
import fcntl
|
||||||
except Exception:
|
except Exception:
|
||||||
fcntl = None
|
fcntl = None
|
||||||
|
try:
|
||||||
|
import msvcrt
|
||||||
|
except Exception:
|
||||||
|
msvcrt = None
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Constants
|
# Constants
|
||||||
|
|
@ -186,31 +191,64 @@ def _auth_lock_path() -> Path:
|
||||||
return _auth_file_path().with_suffix(".lock")
|
return _auth_file_path().with_suffix(".lock")
|
||||||
|
|
||||||
|
|
||||||
|
_auth_lock_holder = threading.local()
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _auth_store_lock(timeout_seconds: float = AUTH_LOCK_TIMEOUT_SECONDS):
|
def _auth_store_lock(timeout_seconds: float = AUTH_LOCK_TIMEOUT_SECONDS):
|
||||||
"""Cross-process advisory lock for auth.json reads+writes."""
|
"""Cross-process advisory lock for auth.json reads+writes. Reentrant."""
|
||||||
|
# Reentrant: if this thread already holds the lock, just yield.
|
||||||
|
if getattr(_auth_lock_holder, "depth", 0) > 0:
|
||||||
|
_auth_lock_holder.depth += 1
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
_auth_lock_holder.depth -= 1
|
||||||
|
return
|
||||||
|
|
||||||
lock_path = _auth_lock_path()
|
lock_path = _auth_lock_path()
|
||||||
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
with lock_path.open("a+") as lock_file:
|
if fcntl is None and msvcrt is None:
|
||||||
if fcntl is None:
|
_auth_lock_holder.depth = 1
|
||||||
|
try:
|
||||||
yield
|
yield
|
||||||
|
finally:
|
||||||
|
_auth_lock_holder.depth = 0
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# On Windows, msvcrt.locking needs the file to have content and the
|
||||||
|
# file pointer at position 0. Ensure the lock file has at least 1 byte.
|
||||||
|
if msvcrt and (not lock_path.exists() or lock_path.stat().st_size == 0):
|
||||||
|
lock_path.write_text(" ", encoding="utf-8")
|
||||||
|
|
||||||
|
with lock_path.open("r+" if msvcrt else "a+") as lock_file:
|
||||||
deadline = time.time() + max(1.0, timeout_seconds)
|
deadline = time.time() + max(1.0, timeout_seconds)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
if fcntl:
|
||||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
else:
|
||||||
|
lock_file.seek(0)
|
||||||
|
msvcrt.locking(lock_file.fileno(), msvcrt.LK_NBLCK, 1)
|
||||||
break
|
break
|
||||||
except BlockingIOError:
|
except (BlockingIOError, OSError, PermissionError):
|
||||||
if time.time() >= deadline:
|
if time.time() >= deadline:
|
||||||
raise TimeoutError("Timed out waiting for auth store lock")
|
raise TimeoutError("Timed out waiting for auth store lock")
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
_auth_lock_holder.depth = 1
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
|
_auth_lock_holder.depth = 0
|
||||||
|
if fcntl:
|
||||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
|
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
|
||||||
|
elif msvcrt:
|
||||||
|
try:
|
||||||
|
lock_file.seek(0)
|
||||||
|
msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
|
||||||
|
except (OSError, IOError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _load_auth_store(auth_file: Optional[Path] = None) -> Dict[str, Any]:
|
def _load_auth_store(auth_file: Optional[Path] = None) -> Dict[str, Any]:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue