Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

"""Implements file locks compatible with Linux and Windows for locking files and directories.""" 

import errno 

import logging 

import os 

try: 

import fcntl # pylint: disable=import-error 

except ImportError: 

import msvcrt # pylint: disable=import-error 

POSIX_MODE = False 

else: 

POSIX_MODE = True 

 

from certbot import errors 

from acme.magic_typing import Optional, Callable # pylint: disable=unused-import, no-name-in-module 

 

logger = logging.getLogger(__name__) 

 

 

def lock_dir(dir_path): 

# type: (str) -> LockFile 

"""Place a lock file on the directory at dir_path. 

 

The lock file is placed in the root of dir_path with the name 

.certbot.lock. 

 

:param str dir_path: path to directory 

 

:returns: the locked LockFile object 

:rtype: LockFile 

 

:raises errors.LockError: if unable to acquire the lock 

 

""" 

return LockFile(os.path.join(dir_path, '.certbot.lock')) 

 

 

class LockFile(object): 

""" 

Platform independent file lock system. 

LockFile accepts a parameter, the path to a file acting as a lock. Once the LockFile, 

instance is created, the associated file is 'locked from the point of view of the OS, 

meaning that if another instance of Certbot try at the same time to acquire the same lock, 

it will raise an Exception. Calling release method will release the lock, and make it 

available to every other instance. 

Upon exit, Certbot will also release all the locks. 

This allows us to protect a file or directory from being concurrently accessed 

or modified by two Certbot instances. 

LockFile is platform independent: it will proceed to the appropriate OS lock mechanism 

depending on Linux or Windows. 

""" 

def __init__(self, path): 

# type: (str) -> None 

""" 

Create a LockFile instance on the given file path, and acquire lock. 

:param str path: the path to the file that will hold a lock 

""" 

self._path = path 

mechanism = _UnixLockMechanism if POSIX_MODE else _WindowsLockMechanism 

self._lock_mechanism = mechanism(path) 

 

self.acquire() 

 

def __repr__(self): 

# type: () -> str 

repr_str = '{0}({1}) <'.format(self.__class__.__name__, self._path) 

if self.is_locked(): 

repr_str += 'acquired>' 

else: 

repr_str += 'released>' 

return repr_str 

 

def acquire(self): 

# type: () -> None 

""" 

Acquire the lock on the file, forbidding any other Certbot instance to acquire it. 

:raises errors.LockError: if unable to acquire the lock 

""" 

self._lock_mechanism.acquire() 

 

def release(self): 

# type: () -> None 

""" 

Release the lock on the file, allowing any other Certbot instance to acquire it. 

""" 

self._lock_mechanism.release() 

 

def is_locked(self): 

# type: () -> bool 

""" 

Check if the file is currently locked. 

:return: True if the file is locked, False otherwise 

""" 

return self._lock_mechanism.is_locked() 

 

 

class _BaseLockMechanism(object): 

def __init__(self, path): 

# type: (str) -> None 

""" 

Create a lock file mechanism for Unix. 

:param str path: the path to the lock file 

""" 

self._path = path 

self._fd = None # type: Optional[int] 

 

def is_locked(self): 

# type: () -> bool 

"""Check if lock file is currently locked. 

:return: True if the lock file is locked 

:rtype: bool 

""" 

return self._fd is not None 

 

def acquire(self): # pylint: disable=missing-docstring 

pass # pragma: no cover 

 

def release(self): # pylint: disable=missing-docstring 

pass # pragma: no cover 

 

 

class _UnixLockMechanism(_BaseLockMechanism): 

""" 

A UNIX lock file mechanism. 

This lock file is released when the locked file is closed or the 

process exits. It cannot be used to provide synchronization between 

threads. It is based on the lock_file package by Martin Horcicka. 

""" 

def acquire(self): 

# type: () -> None 

"""Acquire the lock.""" 

while self._fd is None: 

# Open the file 

fd = os.open(self._path, os.O_CREAT | os.O_WRONLY, 0o600) 

try: 

self._try_lock(fd) 

if self._lock_success(fd): 

self._fd = fd 

finally: 

# Close the file if it is not the required one 

if self._fd is None: 

os.close(fd) 

 

def _try_lock(self, fd): 

# type: (int) -> None 

""" 

Try to acquire the lock file without blocking. 

:param int fd: file descriptor of the opened file to lock 

""" 

try: 

fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) 

except IOError as err: 

if err.errno in (errno.EACCES, errno.EAGAIN): 

logger.debug('A lock on %s is held by another process.', self._path) 

raise errors.LockError('Another instance of Certbot is already running.') 

raise 

 

def _lock_success(self, fd): 

# type: (int) -> bool 

""" 

Did we successfully grab the lock? 

Because this class deletes the locked file when the lock is 

released, it is possible another process removed and recreated 

the file between us opening the file and acquiring the lock. 

:param int fd: file descriptor of the opened file to lock 

:returns: True if the lock was successfully acquired 

:rtype: bool 

""" 

try: 

stat1 = os.stat(self._path) 

except OSError as err: 

if err.errno == errno.ENOENT: 

return False 

raise 

 

stat2 = os.fstat(fd) 

# If our locked file descriptor and the file on disk refer to 

# the same device and inode, they're the same file. 

return stat1.st_dev == stat2.st_dev and stat1.st_ino == stat2.st_ino 

 

def release(self): 

# type: () -> None 

"""Remove, close, and release the lock file.""" 

# It is important the lock file is removed before it's released, 

# otherwise: 

# 

# process A: open lock file 

# process B: release lock file 

# process A: lock file 

# process A: check device and inode 

# process B: delete file 

# process C: open and lock a different file at the same path 

try: 

os.remove(self._path) 

finally: 

# Following check is done to make mypy happy: it ensure that self._fd, marked 

# as Optional[int] is effectively int to make it compatible with os.close signature. 

if self._fd is None: # pragma: no cover 

raise TypeError('Error, self._fd is None.') 

try: 

os.close(self._fd) 

finally: 

self._fd = None 

 

 

class _WindowsLockMechanism(_BaseLockMechanism): 

""" 

A Windows lock file mechanism. 

By default on Windows, acquiring a file handler gives exclusive access to the process 

and results in an effective lock. However, it is possible to explicitly acquire the 

file handler in shared access in terms of read and write, and this is done by os.open 

and io.open in Python. So an explicit lock needs to be done through the call of 

msvcrt.locking, that will lock the first byte of the file. In theory, it is also 

possible to access a file in shared delete access, allowing other processes to delete an 

opened file. But this needs also to be done explicitly by all processes using the Windows 

low level APIs, and Python does not do it. As of Python 3.7 and below, Python developers 

state that deleting a file opened by a process from another process is not possible with 

os.open and io.open. 

Consequently, mscvrt.locking is sufficient to obtain an effective lock, and the race 

condition encountered on Linux is not possible on Windows, leading to a simpler workflow. 

""" 

def acquire(self): 

"""Acquire the lock""" 

open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC 

 

fd = os.open(self._path, open_mode, 0o600) 

try: 

msvcrt.locking(fd, msvcrt.LK_NBLCK, 1) 

except (IOError, OSError) as err: 

os.close(fd) 

# Anything except EACCES is unexpected. Raise directly the error in that case. 

if err.errno != errno.EACCES: 

raise 

logger.debug('A lock on %s is held by another process.', self._path) 

raise errors.LockError('Another instance of Certbot is already running.') 

 

self._fd = fd 

 

def release(self): 

"""Release the lock.""" 

try: 

msvcrt.locking(self._fd, msvcrt.LK_UNLCK, 1) 

os.close(self._fd) 

 

try: 

os.remove(self._path) 

except OSError as e: 

# If the lock file cannot be removed, it is not a big deal. 

# Likely another instance is acquiring the lock we just released. 

logger.debug(str(e)) 

finally: 

self._fd = None