lockfile.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  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. from __future__ import print_function
  6. import contextlib
  7. import logging
  8. import os
  9. import sys
  10. import time
  11. class LockError(Exception):
  12. pass
  13. if sys.platform.startswith('win'):
  14. # Windows implementation
  15. import win32imports
  16. BYTES_TO_LOCK = 1
  17. def _open_file(lockfile):
  18. return win32imports.Handle(
  19. win32imports.CreateFileW(
  20. lockfile, # lpFileName
  21. win32imports.GENERIC_WRITE, # dwDesiredAccess
  22. 0, # dwShareMode=prevent others from opening file
  23. None, # lpSecurityAttributes
  24. win32imports.CREATE_ALWAYS, # dwCreationDisposition
  25. win32imports.FILE_ATTRIBUTE_NORMAL, # dwFlagsAndAttributes
  26. None # hTemplateFile
  27. ))
  28. def _close_file(handle):
  29. # CloseHandle releases lock too.
  30. win32imports.CloseHandle(handle)
  31. def _lock_file(handle):
  32. ret = win32imports.LockFileEx(
  33. handle, # hFile
  34. win32imports.LOCKFILE_FAIL_IMMEDIATELY
  35. | win32imports.LOCKFILE_EXCLUSIVE_LOCK, # dwFlags
  36. 0, #dwReserved
  37. BYTES_TO_LOCK, # nNumberOfBytesToLockLow
  38. 0, # nNumberOfBytesToLockHigh
  39. win32imports.Overlapped() # lpOverlapped
  40. )
  41. # LockFileEx returns result as bool, which is converted into an integer
  42. # (1 == successful; 0 == not successful)
  43. if ret == 0:
  44. error_code = win32imports.GetLastError()
  45. raise OSError('Failed to lock handle (error code: %d).' % error_code)
  46. else:
  47. # Unix implementation
  48. import fcntl
  49. def _open_file(lockfile):
  50. open_flags = (os.O_CREAT | os.O_WRONLY)
  51. return os.open(lockfile, open_flags, 0o644)
  52. def _close_file(fd):
  53. os.close(fd)
  54. def _lock_file(fd):
  55. fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
  56. def _try_lock(lockfile):
  57. f = _open_file(lockfile)
  58. try:
  59. _lock_file(f)
  60. except Exception:
  61. _close_file(f)
  62. raise
  63. return lambda: _close_file(f)
  64. def _lock(path, timeout=0):
  65. """_lock returns function to release the lock if locking was successful.
  66. _lock also implements simple retry logic."""
  67. elapsed = 0
  68. while True:
  69. try:
  70. return _try_lock(path + '.locked')
  71. except (OSError, IOError) as e:
  72. if elapsed < timeout:
  73. sleep_time = min(10, timeout - elapsed)
  74. logging.info(
  75. 'Could not create git cache lockfile; '
  76. 'will retry after sleep(%d).', sleep_time)
  77. elapsed += sleep_time
  78. time.sleep(sleep_time)
  79. continue
  80. raise LockError("Error locking %s (err: %s)" % (path, str(e)))
  81. @contextlib.contextmanager
  82. def lock(path, timeout=0):
  83. """Get exclusive lock to path.
  84. Usage:
  85. import lockfile
  86. with lockfile.lock(path, timeout):
  87. # Do something
  88. pass
  89. """
  90. release_fn = _lock(path, timeout)
  91. try:
  92. yield
  93. finally:
  94. release_fn()