Skip to content

Commit 6306c5c

Browse files
authored
Merge pull request #2267 from radhika1314/lock-file-exclusive-create
fix(util): create lock files in one exclusive step
2 parents f3ee6d4 + 97a5468 commit 6306c5c

2 files changed

Lines changed: 115 additions & 8 deletions

File tree

‎git/util.py‎

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,15 +1166,56 @@ def _obtain_lock_or_raise(self) -> None:
11661166
if self._has_lock():
11671167
return
11681168
lock_file = self._lock_file_path()
1169-
if osp.isfile(lock_file):
1169+
# Create the lock in one step. Checking for it first would allow another
1170+
# holder to create it between the check and the open.
1171+
try:
1172+
if sys.platform == "win32":
1173+
if "\0" in lock_file:
1174+
raise ValueError("embedded null character")
1175+
1176+
import ctypes
1177+
from ctypes import wintypes
1178+
1179+
# Unlike POSIX, Windows follows dangling symlinks even with O_EXCL.
1180+
# Open the reparse point itself so an existing link is rejected.
1181+
# Call the Unicode API directly: older _winapi.CreateFile wrappers
1182+
# use the ANSI API and can create a lock under the wrong filename.
1183+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
1184+
create_file = kernel32.CreateFileW
1185+
create_file.argtypes = (
1186+
wintypes.LPCWSTR,
1187+
wintypes.DWORD,
1188+
wintypes.DWORD,
1189+
wintypes.LPVOID,
1190+
wintypes.DWORD,
1191+
wintypes.DWORD,
1192+
wintypes.HANDLE,
1193+
)
1194+
create_file.restype = wintypes.HANDLE
1195+
close_handle = kernel32.CloseHandle
1196+
close_handle.argtypes = (wintypes.HANDLE,)
1197+
close_handle.restype = wintypes.BOOL
1198+
handle = create_file(
1199+
lock_file,
1200+
0x40000000, # GENERIC_WRITE
1201+
0,
1202+
None,
1203+
1, # CREATE_NEW
1204+
0x00200000, # FILE_FLAG_OPEN_REPARSE_POINT
1205+
None,
1206+
)
1207+
if handle == wintypes.HANDLE(-1).value:
1208+
raise ctypes.WinError(ctypes.get_last_error())
1209+
if not close_handle(handle):
1210+
raise ctypes.WinError(ctypes.get_last_error())
1211+
else:
1212+
fd = os.open(lock_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
1213+
os.close(fd)
1214+
except FileExistsError as e:
11701215
raise OSError(
11711216
"Lock for file %r did already exist, delete %r in case the lock is illegal"
11721217
% (self._file_path, lock_file)
1173-
)
1174-
1175-
try:
1176-
with open(lock_file, mode="w"):
1177-
pass
1218+
) from e
11781219
except OSError as e:
11791220
raise OSError(str(e)) from e
11801221

‎test/test_util.py‎

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import subprocess
1313
import sys
1414
import tempfile
15+
import threading
1516
import time
1617
from unittest import SkipTest, mock
1718

@@ -418,9 +419,11 @@ def test_it_should_dashify(self):
418419
self.assertEqual("this-is-my-argument", dashify("this_is_my_argument"))
419420
self.assertEqual("foo", dashify("foo"))
420421

421-
def test_lock_file(self):
422+
@ddt.data("my-lock-file", "my-lock-file-\u0394", "\u0394/my-lock-file", "\U0001f680/my-lock-file")
423+
def test_lock_file(self, filename):
422424
with tempfile.TemporaryDirectory() as tdir:
423-
my_file = os.path.join(tdir, "my-lock-file")
425+
my_file = os.path.join(tdir, filename)
426+
os.makedirs(os.path.dirname(my_file), exist_ok=True)
424427
lock_file = LockFile(my_file)
425428
assert not lock_file._has_lock()
426429
# Release lock we don't have - fine.
@@ -429,6 +432,7 @@ def test_lock_file(self):
429432
# Get lock.
430433
lock_file._obtain_lock_or_raise()
431434
assert lock_file._has_lock()
435+
assert os.path.isfile(my_file + ".lock")
432436

433437
# Concurrent access.
434438
other_lock_file = LockFile(my_file)
@@ -437,6 +441,7 @@ def test_lock_file(self):
437441

438442
lock_file._release_lock()
439443
assert not lock_file._has_lock()
444+
assert not os.path.exists(my_file + ".lock")
440445

441446
other_lock_file._obtain_lock_or_raise()
442447
self.assertRaises(IOError, lock_file._obtain_lock_or_raise)
@@ -446,6 +451,67 @@ def test_lock_file(self):
446451
lock_file._obtain_lock_or_raise()
447452
lock_file._release_lock()
448453

454+
def test_lock_file_rejects_embedded_nul(self):
455+
with tempfile.TemporaryDirectory() as tdir:
456+
my_file = os.path.join(tdir, "my-lock-file")
457+
lock_file = LockFile(my_file + "\0suffix")
458+
self.assertRaises(ValueError, lock_file._obtain_lock_or_raise)
459+
assert not lock_file._has_lock()
460+
assert not os.path.exists(my_file)
461+
462+
@ddt.data(False, True)
463+
@requires_symlinks
464+
def test_lock_file_does_not_follow_a_symlink(self, target_exists):
465+
with tempfile.TemporaryDirectory() as tdir:
466+
my_file = os.path.join(tdir, "my-lock-file")
467+
outside = os.path.join(tdir, "outside-the-lock")
468+
content = b"Do not modify the symlink target."
469+
if target_exists:
470+
with open(outside, "wb") as stream:
471+
stream.write(content)
472+
os.symlink(outside, my_file + ".lock")
473+
474+
lock_file = LockFile(my_file)
475+
self.assertRaises(IOError, lock_file._obtain_lock_or_raise)
476+
assert not lock_file._has_lock()
477+
lock_file._release_lock()
478+
assert os.path.islink(my_file + ".lock")
479+
if target_exists:
480+
with open(outside, "rb") as stream:
481+
self.assertEqual(stream.read(), content)
482+
else:
483+
assert not os.path.exists(outside)
484+
485+
def test_lock_file_is_obtained_by_a_single_holder(self):
486+
with tempfile.TemporaryDirectory() as tdir:
487+
my_file = os.path.join(tdir, "my-lock-file")
488+
racers = 8
489+
at_the_line = threading.Barrier(racers)
490+
holders = []
491+
guard = threading.Lock()
492+
493+
def obtain():
494+
lock_file = LockFile(my_file)
495+
at_the_line.wait()
496+
try:
497+
lock_file._obtain_lock_or_raise()
498+
except OSError:
499+
return
500+
with guard:
501+
holders.append(lock_file)
502+
503+
threads = [threading.Thread(target=obtain) for _ in range(racers)]
504+
for thread in threads:
505+
thread.start()
506+
for thread in threads:
507+
thread.join()
508+
509+
try:
510+
self.assertEqual(1, len(holders))
511+
finally:
512+
for lock_file in holders:
513+
lock_file._release_lock()
514+
449515
def test_blocking_lock_file(self):
450516
with tempfile.TemporaryDirectory() as tdir:
451517
my_file = os.path.join(tdir, "my-lock-file")

0 commit comments

Comments
 (0)