blob: 3eedae172a2907378f1adb2461fe04bfb99eae86 [file] [log] [blame]
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00001# 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
Josip Sokcevic14a83ae2020-05-21 01:36:34 +00006import contextlib
7import logging
8import os
9import sys
10import time
11
12
13class LockError(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000014 pass
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000015
16
17if sys.platform.startswith('win'):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000018 # Windows implementation
19 import win32imports
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000020
Mike Frysinger124bb8e2023-09-06 05:48:55 +000021 BYTES_TO_LOCK = 1
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000022
Mike Frysinger124bb8e2023-09-06 05:48:55 +000023 def _open_file(lockfile):
24 return win32imports.Handle(
25 win32imports.CreateFileW(
26 lockfile, # lpFileName
27 win32imports.GENERIC_WRITE, # dwDesiredAccess
28 0, # dwShareMode=prevent others from opening file
29 None, # lpSecurityAttributes
30 win32imports.CREATE_ALWAYS, # dwCreationDisposition
31 win32imports.FILE_ATTRIBUTE_NORMAL, # dwFlagsAndAttributes
32 None # hTemplateFile
33 ))
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000034
Mike Frysinger124bb8e2023-09-06 05:48:55 +000035 def _close_file(handle):
36 # CloseHandle releases lock too.
37 win32imports.CloseHandle(handle)
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000038
Mike Frysinger124bb8e2023-09-06 05:48:55 +000039 def _lock_file(handle):
40 ret = win32imports.LockFileEx(
41 handle, # hFile
42 win32imports.LOCKFILE_FAIL_IMMEDIATELY
43 | win32imports.LOCKFILE_EXCLUSIVE_LOCK, # dwFlags
44 0, #dwReserved
45 BYTES_TO_LOCK, # nNumberOfBytesToLockLow
46 0, # nNumberOfBytesToLockHigh
47 win32imports.Overlapped() # lpOverlapped
48 )
49 # LockFileEx returns result as bool, which is converted into an integer
50 # (1 == successful; 0 == not successful)
51 if ret == 0:
52 error_code = win32imports.GetLastError()
53 raise OSError('Failed to lock handle (error code: %d).' %
54 error_code)
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000055else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +000056 # Unix implementation
57 import fcntl
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000058
Mike Frysinger124bb8e2023-09-06 05:48:55 +000059 def _open_file(lockfile):
60 open_flags = (os.O_CREAT | os.O_WRONLY)
61 return os.open(lockfile, open_flags, 0o644)
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000062
Mike Frysinger124bb8e2023-09-06 05:48:55 +000063 def _close_file(fd):
64 os.close(fd)
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000065
Mike Frysinger124bb8e2023-09-06 05:48:55 +000066 def _lock_file(fd):
67 fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000068
69
70def _try_lock(lockfile):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000071 f = _open_file(lockfile)
72 try:
73 _lock_file(f)
74 except Exception:
75 _close_file(f)
76 raise
77 return lambda: _close_file(f)
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000078
79
80def _lock(path, timeout=0):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000081 """_lock returns function to release the lock if locking was successful.
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000082
83 _lock also implements simple retry logic."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +000084 elapsed = 0
85 while True:
86 try:
87 return _try_lock(path + '.locked')
88 except (OSError, IOError) as e:
89 if elapsed < timeout:
90 sleep_time = min(10, timeout - elapsed)
91 logging.info(
92 'Could not create git cache lockfile; '
93 'will retry after sleep(%d).', sleep_time)
94 elapsed += sleep_time
95 time.sleep(sleep_time)
96 continue
97 raise LockError("Error locking %s (err: %s)" % (path, str(e)))
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000098
99
100@contextlib.contextmanager
101def lock(path, timeout=0):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000102 """Get exclusive lock to path.
Josip Sokcevic14a83ae2020-05-21 01:36:34 +0000103
104 Usage:
105 import lockfile
106 with lockfile.lock(path, timeout):
107 # Do something
108 pass
109
110 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000111 release_fn = _lock(path, timeout)
112 try:
113 yield
114 finally:
115 release_fn()