blob: 9e416358605e7d709112fa2c8cfa9e82cf7923a3 [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):
16 pass
17
18
19if sys.platform.startswith('win'):
20 # Windows implementation
21 import win32imports
22
23 BYTES_TO_LOCK = 1
24
25 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 ))
36
37 def _close_file(handle):
38 # CloseHandle releases lock too.
39 win32imports.CloseHandle(handle)
40
41 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).' % error_code)
56else:
57 # Unix implementation
58 import fcntl
59
60 def _open_file(lockfile):
61 open_flags = (os.O_CREAT | os.O_WRONLY)
62 return os.open(lockfile, open_flags, 0o644)
63
64 def _close_file(fd):
65 os.close(fd)
66
67 def _lock_file(fd):
68 fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
69
70
71def _try_lock(lockfile):
72 f = _open_file(lockfile)
73 try:
74 _lock_file(f)
75 except Exception:
76 _close_file(f)
77 raise
78 return lambda: _close_file(f)
79
80
81def _lock(path, timeout=0):
82 """_lock returns function to release the lock if locking was successful.
83
84 _lock also implements simple retry logic."""
85 elapsed = 0
86 while True:
87 try:
88 return _try_lock(path + '.locked')
89 except (OSError, IOError) as e:
90 if elapsed < timeout:
91 sleep_time = min(10, timeout - elapsed)
92 logging.info(
93 'Could not create git cache lockfile; '
94 'will retry after sleep(%d).', sleep_time)
95 elapsed += sleep_time
96 time.sleep(sleep_time)
97 continue
98 raise LockError("Error locking %s (err: %s)" % (path, str(e)))
99
100
101@contextlib.contextmanager
102def lock(path, timeout=0):
103 """Get exclusive lock to path.
104
105 Usage:
106 import lockfile
107 with lockfile.lock(path, timeout):
108 # Do something
109 pass
110
111 """
112 release_fn = _lock(path, timeout)
113 try:
114 yield
115 finally:
116 release_fn()