claude-dev-env 8.42.3 → 8.42.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.42.3",
3
+ "version": "8.42.4",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import errno
5
6
  import json
6
7
  import os
7
8
  import subprocess
@@ -11,7 +12,7 @@ import threading
11
12
  from contextlib import contextmanager
12
13
  from datetime import datetime, timezone
13
14
  from pathlib import Path
14
- from typing import Iterator, Sequence
15
+ from typing import Callable, Iterator, Sequence
15
16
 
16
17
  if sys.platform == "win32":
17
18
  import msvcrt
@@ -229,11 +230,39 @@ def _load_state(path: Path) -> dict[str, object]:
229
230
  return {key: parsed_state.get(key) if isinstance(parsed_state.get(key), dict) else {} for key in ("meters", "spent", "affinity")}
230
231
 
231
232
 
233
+ def _acquire_windows_lock(lock_descriptor: int, locking: Callable[[int, int, int], None], lock_mode: int) -> None:
234
+ """Lock the first byte of *lock_descriptor*, retrying while another process holds it.
235
+
236
+ ``msvcrt.locking`` with ``LK_LOCK`` gives up after ten one-second attempts and
237
+ raises ``OSError`` with ``EDEADLOCK``. That error means the lock is still busy, so
238
+ the call repeats until it succeeds, matching ``fcntl.flock`` on POSIX. Any other
239
+ ``OSError`` propagates.
240
+
241
+ Args:
242
+ lock_descriptor: Open descriptor of the lock file.
243
+ locking: The ``msvcrt.locking`` function.
244
+ lock_mode: The blocking lock mode passed to ``locking``.
245
+ """
246
+ is_locked = False
247
+ while not is_locked:
248
+ is_locked = _attempt_windows_lock(lock_descriptor, locking, lock_mode)
249
+
250
+
251
+ def _attempt_windows_lock(lock_descriptor: int, locking: Callable[[int, int, int], None], lock_mode: int) -> bool:
252
+ os.lseek(lock_descriptor, 0, os.SEEK_SET)
253
+ try:
254
+ locking(lock_descriptor, lock_mode, 1)
255
+ except OSError as error:
256
+ if error.errno != errno.EDEADLOCK:
257
+ raise
258
+ return False
259
+ return True
260
+
261
+
232
262
  if sys.platform == "win32":
233
263
 
234
264
  def _acquire_state_lock(lock_descriptor: int) -> None:
235
- os.lseek(lock_descriptor, 0, os.SEEK_SET)
236
- msvcrt.locking(lock_descriptor, msvcrt.LK_LOCK, 1)
265
+ _acquire_windows_lock(lock_descriptor, msvcrt.locking, msvcrt.LK_LOCK)
237
266
 
238
267
  def _release_state_lock(lock_descriptor: int) -> None:
239
268
  os.lseek(lock_descriptor, 0, os.SEEK_SET)
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import errno
3
4
  import json
5
+ import os
4
6
  import subprocess
5
7
  from datetime import datetime, timezone
6
8
  from pathlib import Path
@@ -125,3 +127,37 @@ def test_should_keep_every_concurrent_spent_mark_and_newest_meter_reading(tmp_pa
125
127
  assert saved["spent"] == {"codex:one": 2000.0, "codex:two": 3000.0}
126
128
  assert saved["meters"]["codex:one"]["read_at"] == 500.0
127
129
  assert saved["affinity"] == {"session": "one"}
130
+
131
+
132
+ def test_should_retry_windows_lock_until_contention_clears(tmp_path: Path) -> None:
133
+ lock_descriptor = os.open(tmp_path / "state.json.lock", os.O_CREAT | os.O_RDWR)
134
+ all_calls: list[tuple[int, int, int]] = []
135
+
136
+ def locking(descriptor: int, mode: int, byte_count: int) -> None:
137
+ all_calls.append((descriptor, mode, byte_count))
138
+ if len(all_calls) < 3:
139
+ raise OSError(errno.EDEADLOCK, "Resource deadlock avoided")
140
+
141
+ try:
142
+ support._acquire_windows_lock(lock_descriptor, locking, 7)
143
+ finally:
144
+ os.close(lock_descriptor)
145
+
146
+ assert all_calls == [(lock_descriptor, 7, 1)] * 3
147
+
148
+
149
+ def test_should_raise_windows_lock_error_other_than_contention(tmp_path: Path) -> None:
150
+ lock_descriptor = os.open(tmp_path / "state.json.lock", os.O_CREAT | os.O_RDWR)
151
+ all_calls: list[int] = []
152
+
153
+ def locking(descriptor: int, mode: int, byte_count: int) -> None:
154
+ all_calls.append(descriptor)
155
+ raise OSError(errno.EBADF, "Bad file descriptor")
156
+
157
+ try:
158
+ with pytest.raises(OSError):
159
+ support._acquire_windows_lock(lock_descriptor, locking, 7)
160
+ finally:
161
+ os.close(lock_descriptor)
162
+
163
+ assert all_calls == [lock_descriptor]