1# Copyright 2023 The Chromium Authors 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 6Copied from third_party/depot_tools/lockfile.py. 7""" 8 9import contextlib 10import fcntl 11import logging 12import os 13import time 14 15 16class LockError(Exception): 17 """Error raised if timeout or lock (without timeout) fails.""" 18 19 20def _open_file(lockfile): 21 open_flags = (os.O_CREAT | os.O_WRONLY) 22 return os.open(lockfile, open_flags, 0o644) 23 24 25def _close_file(file_descriptor): 26 os.close(file_descriptor) 27 28 29def _lock_file(file_descriptor): 30 fcntl.flock(file_descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB) 31 32 33def _try_lock(lockfile): 34 f = _open_file(lockfile) 35 try: 36 _lock_file(f) 37 except Exception: 38 _close_file(f) 39 raise 40 return lambda: _close_file(f) 41 42 43def _lock(path, timeout=0): 44 """_lock returns function to release the lock if locking was successful. 45 46 _lock also implements simple retry logic.""" 47 elapsed = 0 48 while True: 49 try: 50 return _try_lock(path + '.locked') 51 except (OSError, IOError) as error: 52 if elapsed < timeout: 53 sleep_time = min(10, timeout - elapsed) 54 logging.info( 55 'Could not create lockfile; will retry after sleep(%d).', 56 sleep_time) 57 elapsed += sleep_time 58 time.sleep(sleep_time) 59 continue 60 raise LockError("Error locking %s (err: %s)" % 61 (path, str(error))) from error 62 63 64@contextlib.contextmanager 65def lock(path, timeout=0): 66 """Get exclusive lock to path. 67 68 Usage: 69 import lockfile 70 with lockfile.lock(path, timeout): 71 # Do something 72 pass 73 74 """ 75 release_fn = _lock(path, timeout) 76 try: 77 yield 78 finally: 79 release_fn() 80