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
|
@@ -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
|
-
|
|
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]
|