lockfile.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. # Copyright 2020 The Chromium Authors. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. """Exclusive filelocking for all supported platforms."""
  5. import contextlib
  6. import logging
  7. import os
  8. import sys
  9. import time
  10. class LockError(Exception):
  11. pass
  12. if sys.platform.startswith('win'):
  13. # Windows implementation
  14. import win32imports
  15. BYTES_TO_LOCK = 1
  16. def _open_file(lockfile):
  17. return win32imports.Handle(
  18. win32imports.CreateFileW(
  19. lockfile, # lpFileName
  20. win32imports.GENERIC_WRITE, # dwDesiredAccess
  21. 0, # dwShareMode=prevent others from opening file
  22. None, # lpSecurityAttributes
  23. win32imports.CREATE_ALWAYS, # dwCreationDisposition
  24. win32imports.FILE_ATTRIBUTE_NORMAL, # dwFlagsAndAttributes
  25. None # hTemplateFile
  26. ))
  27. def _close_file(handle, unlock):
  28. if unlock:
  29. # Locks are released *before* the CloseHandle function is finished
  30. # processing:
  31. # - https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-unlockfileex#remarks
  32. pass
  33. win32imports.CloseHandle(handle)
  34. def _lock_file(handle):
  35. ret = win32imports.LockFileEx(
  36. handle, # hFile
  37. win32imports.LOCKFILE_FAIL_IMMEDIATELY
  38. | win32imports.LOCKFILE_EXCLUSIVE_LOCK, # dwFlags
  39. 0, #dwReserved
  40. BYTES_TO_LOCK, # nNumberOfBytesToLockLow
  41. 0, # nNumberOfBytesToLockHigh
  42. win32imports.Overlapped() # lpOverlapped
  43. )
  44. # LockFileEx returns result as bool, which is converted into an integer
  45. # (1 == successful; 0 == not successful)
  46. if ret == 0:
  47. error_code = win32imports.GetLastError()
  48. raise OSError('Failed to lock handle (error code: %d).' %
  49. error_code)
  50. else:
  51. # Unix implementation
  52. import fcntl
  53. def _open_file(lockfile):
  54. open_flags = (os.O_CREAT | os.O_WRONLY)
  55. return os.open(lockfile, open_flags, 0o644)
  56. def _close_file(fd, unlock):
  57. # "man 2 fcntl" states that closing any file descriptor referring to
  58. # the lock file will release all the process locks on the file, but
  59. # there is no guarantee that the locks will be released atomically
  60. # before the closure.
  61. #
  62. # It's necessary to release the lock before the file close to avoid
  63. # possible race conditions.
  64. if unlock:
  65. fcntl.flock(fd, fcntl.LOCK_UN)
  66. os.close(fd)
  67. def _lock_file(fd):
  68. fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
  69. def _try_lock(lockfile):
  70. f = _open_file(lockfile)
  71. try:
  72. _lock_file(f)
  73. except Exception:
  74. _close_file(f, unlock=False)
  75. raise
  76. return lambda: _close_file(f, unlock=True)
  77. def _lock(path, timeout=0):
  78. """_lock returns function to release the lock if locking was successful.
  79. _lock also implements simple retry logic.
  80. NOTE: timeout value doesn't include time it takes to aquire lock, just
  81. overall sleep time."""
  82. elapsed = 0
  83. sleep_time = 0.1
  84. while True:
  85. try:
  86. return _try_lock(path + '.locked')
  87. except (OSError, IOError) as e:
  88. if elapsed < timeout:
  89. logging.info(
  90. 'Could not create git cache lockfile; '
  91. 'will retry after sleep(%d).', sleep_time)
  92. elapsed += sleep_time
  93. time.sleep(sleep_time)
  94. continue
  95. raise LockError("Error locking %s (err: %s)" % (path, str(e)))
  96. @contextlib.contextmanager
  97. def lock(path, timeout=0):
  98. """Get exclusive lock to path.
  99. Usage:
  100. import lockfile
  101. with lockfile.lock(path, timeout):
  102. # Do something
  103. pass
  104. """
  105. release_fn = _lock(path, timeout)
  106. try:
  107. yield
  108. finally:
  109. release_fn()