# Copyright 2023 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Exclusive filelocking for all supported platforms. Copied from third_party/depot_tools/lockfile.py. """ import contextlib import fcntl import logging import os import time class LockError(Exception): """Error raised if timeout or lock (without timeout) fails.""" def _open_file(lockfile): open_flags = (os.O_CREAT | os.O_WRONLY) return os.open(lockfile, open_flags, 0o644) def _close_file(file_descriptor): os.close(file_descriptor) def _lock_file(file_descriptor): fcntl.flock(file_descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB) def _try_lock(lockfile): f = _open_file(lockfile) try: _lock_file(f) except Exception: _close_file(f) raise return lambda: _close_file(f) def _lock(path, timeout=0): """_lock returns function to release the lock if locking was successful. _lock also implements simple retry logic.""" elapsed = 0 while True: try: return _try_lock(path + '.locked') except (OSError, IOError) as error: if elapsed < timeout: sleep_time = min(10, timeout - elapsed) logging.info( 'Could not create lockfile; will retry after sleep(%d).', sleep_time) elapsed += sleep_time time.sleep(sleep_time) continue raise LockError("Error locking %s (err: %s)" % (path, str(error))) from error @contextlib.contextmanager def lock(path, timeout=0): """Get exclusive lock to path. Usage: import lockfile with lockfile.lock(path, timeout): # Do something pass """ release_fn = _lock(path, timeout) try: yield finally: release_fn()