blob: 3cd5670365aea2618fc08afd26772c46b054d1b4 [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
6from __future__ import print_function
7
8import contextlib
9import logging
10import os
11import sys
12import time
13
14
15class LockError(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000016 pass
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000017
18
19if sys.platform.startswith('win'):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000020 # Windows implementation
21 import win32imports
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000022
Mike Frysinger124bb8e2023-09-06 05:48:55 +000023 BYTES_TO_LOCK = 1
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000024
Mike Frysinger124bb8e2023-09-06 05:48:55 +000025 def _open_file(lockfile):
26 return win32imports.Handle(
27 win32imports.CreateFileW(
28 lockfile, # lpFileName
29 win32imports.GENERIC_WRITE, # dwDesiredAccess
30 0, # dwShareMode=prevent others from opening file
31 None, # lpSecurityAttributes
32 win32imports.CREATE_ALWAYS, # dwCreationDisposition
33 win32imports.FILE_ATTRIBUTE_NORMAL, # dwFlagsAndAttributes
34 None # hTemplateFile
35 ))
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000036
Mike Frysinger124bb8e2023-09-06 05:48:55 +000037 def _close_file(handle):
38 # CloseHandle releases lock too.
39 win32imports.CloseHandle(handle)
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000040
Mike Frysinger124bb8e2023-09-06 05:48:55 +000041 def _lock_file(handle):
42 ret = win32imports.LockFileEx(
43 handle, # hFile
44 win32imports.LOCKFILE_FAIL_IMMEDIATELY
45 | win32imports.LOCKFILE_EXCLUSIVE_LOCK, # dwFlags
46 0, #dwReserved
47 BYTES_TO_LOCK, # nNumberOfBytesToLockLow
48 0, # nNumberOfBytesToLockHigh
49 win32imports.Overlapped() # lpOverlapped
50 )
51 # LockFileEx returns result as bool, which is converted into an integer
52 # (1 == successful; 0 == not successful)
53 if ret == 0:
54 error_code = win32imports.GetLastError()
55 raise OSError('Failed to lock handle (error code: %d).' %
56 error_code)
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000057else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +000058 # Unix implementation
59 import fcntl
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000060
Mike Frysinger124bb8e2023-09-06 05:48:55 +000061 def _open_file(lockfile):
62 open_flags = (os.O_CREAT | os.O_WRONLY)
63 return os.open(lockfile, open_flags, 0o644)
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000064
Mike Frysinger124bb8e2023-09-06 05:48:55 +000065 def _close_file(fd):
66 os.close(fd)
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000067
Mike Frysinger124bb8e2023-09-06 05:48:55 +000068 def _lock_file(fd):
69 fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000070
71
72def _try_lock(lockfile):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000073 f = _open_file(lockfile)
74 try:
75 _lock_file(f)
76 except Exception:
77 _close_file(f)
78 raise
79 return lambda: _close_file(f)
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000080
81
82def _lock(path, timeout=0):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000083 """_lock returns function to release the lock if locking was successful.
Josip Sokcevic14a83ae2020-05-21 01:36:34 +000084
85 _lock also implements simple retry logic."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +000086 elapsed = 0
87 while True:
88 try:
89 return _try_lock(path + '.locked')
90 except (OSError, IOError) as e:
91 if elapsed < timeout:
92 sleep_time = min(10, timeout - elapsed)
93 logging.info(
94 'Could not create git cache lockfile; '
95 'will retry after sleep(%d).', sleep_time)
96 elapsed += sleep_time
97 time.sleep(sleep_time)
98 continue
99 raise LockError("Error locking %s (err: %s)" % (path, str(e)))
Josip Sokcevic14a83ae2020-05-21 01:36:34 +0000100
101
102@contextlib.contextmanager
103def lock(path, timeout=0):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000104 """Get exclusive lock to path.
Josip Sokcevic14a83ae2020-05-21 01:36:34 +0000105
106 Usage:
107 import lockfile
108 with lockfile.lock(path, timeout):
109 # Do something
110 pass
111
112 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000113 release_fn = _lock(path, timeout)
114 try:
115 yield
116 finally:
117 release_fn()