##// END OF EJS Templates
lock: fix race in lock-breaking code...
Valentin Gatien-Baron -
r44108:039fbd14 default
parent child Browse files
Show More
@@ -1,441 +1,444 b''
1 # lock.py - simple advisory locking scheme for mercurial
1 # lock.py - simple advisory locking scheme for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import contextlib
10 import contextlib
11 import errno
11 import errno
12 import os
12 import os
13 import signal
13 import signal
14 import socket
14 import socket
15 import time
15 import time
16 import warnings
16 import warnings
17
17
18 from .i18n import _
18 from .i18n import _
19 from .pycompat import getattr
19 from .pycompat import getattr
20
20
21 from . import (
21 from . import (
22 encoding,
22 encoding,
23 error,
23 error,
24 pycompat,
24 pycompat,
25 util,
25 util,
26 )
26 )
27
27
28 from .utils import procutil
28 from .utils import procutil
29
29
30
30
31 def _getlockprefix():
31 def _getlockprefix():
32 """Return a string which is used to differentiate pid namespaces
32 """Return a string which is used to differentiate pid namespaces
33
33
34 It's useful to detect "dead" processes and remove stale locks with
34 It's useful to detect "dead" processes and remove stale locks with
35 confidence. Typically it's just hostname. On modern linux, we include an
35 confidence. Typically it's just hostname. On modern linux, we include an
36 extra Linux-specific pid namespace identifier.
36 extra Linux-specific pid namespace identifier.
37 """
37 """
38 result = encoding.strtolocal(socket.gethostname())
38 result = encoding.strtolocal(socket.gethostname())
39 if pycompat.sysplatform.startswith(b'linux'):
39 if pycompat.sysplatform.startswith(b'linux'):
40 try:
40 try:
41 result += b'/%x' % os.stat(b'/proc/self/ns/pid').st_ino
41 result += b'/%x' % os.stat(b'/proc/self/ns/pid').st_ino
42 except OSError as ex:
42 except OSError as ex:
43 if ex.errno not in (errno.ENOENT, errno.EACCES, errno.ENOTDIR):
43 if ex.errno not in (errno.ENOENT, errno.EACCES, errno.ENOTDIR):
44 raise
44 raise
45 return result
45 return result
46
46
47
47
48 @contextlib.contextmanager
48 @contextlib.contextmanager
49 def _delayedinterrupt():
49 def _delayedinterrupt():
50 """Block signal interrupt while doing something critical
50 """Block signal interrupt while doing something critical
51
51
52 This makes sure that the code block wrapped by this context manager won't
52 This makes sure that the code block wrapped by this context manager won't
53 be interrupted.
53 be interrupted.
54
54
55 For Windows developers: It appears not possible to guard time.sleep()
55 For Windows developers: It appears not possible to guard time.sleep()
56 from CTRL_C_EVENT, so please don't use time.sleep() to test if this is
56 from CTRL_C_EVENT, so please don't use time.sleep() to test if this is
57 working.
57 working.
58 """
58 """
59 assertedsigs = []
59 assertedsigs = []
60 blocked = False
60 blocked = False
61 orighandlers = {}
61 orighandlers = {}
62
62
63 def raiseinterrupt(num):
63 def raiseinterrupt(num):
64 if num == getattr(signal, 'SIGINT', None) or num == getattr(
64 if num == getattr(signal, 'SIGINT', None) or num == getattr(
65 signal, 'CTRL_C_EVENT', None
65 signal, 'CTRL_C_EVENT', None
66 ):
66 ):
67 raise KeyboardInterrupt
67 raise KeyboardInterrupt
68 else:
68 else:
69 raise error.SignalInterrupt
69 raise error.SignalInterrupt
70
70
71 def catchterm(num, frame):
71 def catchterm(num, frame):
72 if blocked:
72 if blocked:
73 assertedsigs.append(num)
73 assertedsigs.append(num)
74 else:
74 else:
75 raiseinterrupt(num)
75 raiseinterrupt(num)
76
76
77 try:
77 try:
78 # save handlers first so they can be restored even if a setup is
78 # save handlers first so they can be restored even if a setup is
79 # interrupted between signal.signal() and orighandlers[] =.
79 # interrupted between signal.signal() and orighandlers[] =.
80 for name in [
80 for name in [
81 b'CTRL_C_EVENT',
81 b'CTRL_C_EVENT',
82 b'SIGINT',
82 b'SIGINT',
83 b'SIGBREAK',
83 b'SIGBREAK',
84 b'SIGHUP',
84 b'SIGHUP',
85 b'SIGTERM',
85 b'SIGTERM',
86 ]:
86 ]:
87 num = getattr(signal, name, None)
87 num = getattr(signal, name, None)
88 if num and num not in orighandlers:
88 if num and num not in orighandlers:
89 orighandlers[num] = signal.getsignal(num)
89 orighandlers[num] = signal.getsignal(num)
90 try:
90 try:
91 for num in orighandlers:
91 for num in orighandlers:
92 signal.signal(num, catchterm)
92 signal.signal(num, catchterm)
93 except ValueError:
93 except ValueError:
94 pass # in a thread? no luck
94 pass # in a thread? no luck
95
95
96 blocked = True
96 blocked = True
97 yield
97 yield
98 finally:
98 finally:
99 # no simple way to reliably restore all signal handlers because
99 # no simple way to reliably restore all signal handlers because
100 # any loops, recursive function calls, except blocks, etc. can be
100 # any loops, recursive function calls, except blocks, etc. can be
101 # interrupted. so instead, make catchterm() raise interrupt.
101 # interrupted. so instead, make catchterm() raise interrupt.
102 blocked = False
102 blocked = False
103 try:
103 try:
104 for num, handler in orighandlers.items():
104 for num, handler in orighandlers.items():
105 signal.signal(num, handler)
105 signal.signal(num, handler)
106 except ValueError:
106 except ValueError:
107 pass # in a thread?
107 pass # in a thread?
108
108
109 # re-raise interrupt exception if any, which may be shadowed by a new
109 # re-raise interrupt exception if any, which may be shadowed by a new
110 # interrupt occurred while re-raising the first one
110 # interrupt occurred while re-raising the first one
111 if assertedsigs:
111 if assertedsigs:
112 raiseinterrupt(assertedsigs[0])
112 raiseinterrupt(assertedsigs[0])
113
113
114
114
115 def trylock(ui, vfs, lockname, timeout, warntimeout, *args, **kwargs):
115 def trylock(ui, vfs, lockname, timeout, warntimeout, *args, **kwargs):
116 """return an acquired lock or raise an a LockHeld exception
116 """return an acquired lock or raise an a LockHeld exception
117
117
118 This function is responsible to issue warnings and or debug messages about
118 This function is responsible to issue warnings and or debug messages about
119 the held lock while trying to acquires it."""
119 the held lock while trying to acquires it."""
120
120
121 def printwarning(printer, locker):
121 def printwarning(printer, locker):
122 """issue the usual "waiting on lock" message through any channel"""
122 """issue the usual "waiting on lock" message through any channel"""
123 # show more details for new-style locks
123 # show more details for new-style locks
124 if b':' in locker:
124 if b':' in locker:
125 host, pid = locker.split(b":", 1)
125 host, pid = locker.split(b":", 1)
126 msg = _(
126 msg = _(
127 b"waiting for lock on %s held by process %r on host %r\n"
127 b"waiting for lock on %s held by process %r on host %r\n"
128 ) % (
128 ) % (
129 pycompat.bytestr(l.desc),
129 pycompat.bytestr(l.desc),
130 pycompat.bytestr(pid),
130 pycompat.bytestr(pid),
131 pycompat.bytestr(host),
131 pycompat.bytestr(host),
132 )
132 )
133 else:
133 else:
134 msg = _(b"waiting for lock on %s held by %r\n") % (
134 msg = _(b"waiting for lock on %s held by %r\n") % (
135 l.desc,
135 l.desc,
136 pycompat.bytestr(locker),
136 pycompat.bytestr(locker),
137 )
137 )
138 printer(msg)
138 printer(msg)
139
139
140 l = lock(vfs, lockname, 0, *args, dolock=False, **kwargs)
140 l = lock(vfs, lockname, 0, *args, dolock=False, **kwargs)
141
141
142 debugidx = 0 if (warntimeout and timeout) else -1
142 debugidx = 0 if (warntimeout and timeout) else -1
143 warningidx = 0
143 warningidx = 0
144 if not timeout:
144 if not timeout:
145 warningidx = -1
145 warningidx = -1
146 elif warntimeout:
146 elif warntimeout:
147 warningidx = warntimeout
147 warningidx = warntimeout
148
148
149 delay = 0
149 delay = 0
150 while True:
150 while True:
151 try:
151 try:
152 l._trylock()
152 l._trylock()
153 break
153 break
154 except error.LockHeld as inst:
154 except error.LockHeld as inst:
155 if delay == debugidx:
155 if delay == debugidx:
156 printwarning(ui.debug, inst.locker)
156 printwarning(ui.debug, inst.locker)
157 if delay == warningidx:
157 if delay == warningidx:
158 printwarning(ui.warn, inst.locker)
158 printwarning(ui.warn, inst.locker)
159 if timeout <= delay:
159 if timeout <= delay:
160 raise error.LockHeld(
160 raise error.LockHeld(
161 errno.ETIMEDOUT, inst.filename, l.desc, inst.locker
161 errno.ETIMEDOUT, inst.filename, l.desc, inst.locker
162 )
162 )
163 time.sleep(1)
163 time.sleep(1)
164 delay += 1
164 delay += 1
165
165
166 l.delay = delay
166 l.delay = delay
167 if l.delay:
167 if l.delay:
168 if 0 <= warningidx <= l.delay:
168 if 0 <= warningidx <= l.delay:
169 ui.warn(_(b"got lock after %d seconds\n") % l.delay)
169 ui.warn(_(b"got lock after %d seconds\n") % l.delay)
170 else:
170 else:
171 ui.debug(b"got lock after %d seconds\n" % l.delay)
171 ui.debug(b"got lock after %d seconds\n" % l.delay)
172 if l.acquirefn:
172 if l.acquirefn:
173 l.acquirefn()
173 l.acquirefn()
174 return l
174 return l
175
175
176
176
177 class lock(object):
177 class lock(object):
178 '''An advisory lock held by one process to control access to a set
178 '''An advisory lock held by one process to control access to a set
179 of files. Non-cooperating processes or incorrectly written scripts
179 of files. Non-cooperating processes or incorrectly written scripts
180 can ignore Mercurial's locking scheme and stomp all over the
180 can ignore Mercurial's locking scheme and stomp all over the
181 repository, so don't do that.
181 repository, so don't do that.
182
182
183 Typically used via localrepository.lock() to lock the repository
183 Typically used via localrepository.lock() to lock the repository
184 store (.hg/store/) or localrepository.wlock() to lock everything
184 store (.hg/store/) or localrepository.wlock() to lock everything
185 else under .hg/.'''
185 else under .hg/.'''
186
186
187 # lock is symlink on platforms that support it, file on others.
187 # lock is symlink on platforms that support it, file on others.
188
188
189 # symlink is used because create of directory entry and contents
189 # symlink is used because create of directory entry and contents
190 # are atomic even over nfs.
190 # are atomic even over nfs.
191
191
192 # old-style lock: symlink to pid
192 # old-style lock: symlink to pid
193 # new-style lock: symlink to hostname:pid
193 # new-style lock: symlink to hostname:pid
194
194
195 _host = None
195 _host = None
196
196
197 def __init__(
197 def __init__(
198 self,
198 self,
199 vfs,
199 vfs,
200 fname,
200 fname,
201 timeout=-1,
201 timeout=-1,
202 releasefn=None,
202 releasefn=None,
203 acquirefn=None,
203 acquirefn=None,
204 desc=None,
204 desc=None,
205 inheritchecker=None,
205 inheritchecker=None,
206 parentlock=None,
206 parentlock=None,
207 signalsafe=True,
207 signalsafe=True,
208 dolock=True,
208 dolock=True,
209 ):
209 ):
210 self.vfs = vfs
210 self.vfs = vfs
211 self.f = fname
211 self.f = fname
212 self.held = 0
212 self.held = 0
213 self.timeout = timeout
213 self.timeout = timeout
214 self.releasefn = releasefn
214 self.releasefn = releasefn
215 self.acquirefn = acquirefn
215 self.acquirefn = acquirefn
216 self.desc = desc
216 self.desc = desc
217 self._inheritchecker = inheritchecker
217 self._inheritchecker = inheritchecker
218 self.parentlock = parentlock
218 self.parentlock = parentlock
219 self._parentheld = False
219 self._parentheld = False
220 self._inherited = False
220 self._inherited = False
221 if signalsafe:
221 if signalsafe:
222 self._maybedelayedinterrupt = _delayedinterrupt
222 self._maybedelayedinterrupt = _delayedinterrupt
223 else:
223 else:
224 self._maybedelayedinterrupt = util.nullcontextmanager
224 self._maybedelayedinterrupt = util.nullcontextmanager
225 self.postrelease = []
225 self.postrelease = []
226 self.pid = self._getpid()
226 self.pid = self._getpid()
227 if dolock:
227 if dolock:
228 self.delay = self.lock()
228 self.delay = self.lock()
229 if self.acquirefn:
229 if self.acquirefn:
230 self.acquirefn()
230 self.acquirefn()
231
231
232 def __enter__(self):
232 def __enter__(self):
233 return self
233 return self
234
234
235 def __exit__(self, exc_type, exc_value, exc_tb):
235 def __exit__(self, exc_type, exc_value, exc_tb):
236 self.release()
236 self.release()
237
237
238 def __del__(self):
238 def __del__(self):
239 if self.held:
239 if self.held:
240 warnings.warn(
240 warnings.warn(
241 "use lock.release instead of del lock",
241 "use lock.release instead of del lock",
242 category=DeprecationWarning,
242 category=DeprecationWarning,
243 stacklevel=2,
243 stacklevel=2,
244 )
244 )
245
245
246 # ensure the lock will be removed
246 # ensure the lock will be removed
247 # even if recursive locking did occur
247 # even if recursive locking did occur
248 self.held = 1
248 self.held = 1
249
249
250 self.release()
250 self.release()
251
251
252 def _getpid(self):
252 def _getpid(self):
253 # wrapper around procutil.getpid() to make testing easier
253 # wrapper around procutil.getpid() to make testing easier
254 return procutil.getpid()
254 return procutil.getpid()
255
255
256 def lock(self):
256 def lock(self):
257 timeout = self.timeout
257 timeout = self.timeout
258 while True:
258 while True:
259 try:
259 try:
260 self._trylock()
260 self._trylock()
261 return self.timeout - timeout
261 return self.timeout - timeout
262 except error.LockHeld as inst:
262 except error.LockHeld as inst:
263 if timeout != 0:
263 if timeout != 0:
264 time.sleep(1)
264 time.sleep(1)
265 if timeout > 0:
265 if timeout > 0:
266 timeout -= 1
266 timeout -= 1
267 continue
267 continue
268 raise error.LockHeld(
268 raise error.LockHeld(
269 errno.ETIMEDOUT, inst.filename, self.desc, inst.locker
269 errno.ETIMEDOUT, inst.filename, self.desc, inst.locker
270 )
270 )
271
271
272 def _trylock(self):
272 def _trylock(self):
273 if self.held:
273 if self.held:
274 self.held += 1
274 self.held += 1
275 return
275 return
276 if lock._host is None:
276 if lock._host is None:
277 lock._host = _getlockprefix()
277 lock._host = _getlockprefix()
278 lockname = b'%s:%d' % (lock._host, self.pid)
278 lockname = b'%s:%d' % (lock._host, self.pid)
279 retry = 5
279 retry = 5
280 while not self.held and retry:
280 while not self.held and retry:
281 retry -= 1
281 retry -= 1
282 try:
282 try:
283 with self._maybedelayedinterrupt():
283 with self._maybedelayedinterrupt():
284 self.vfs.makelock(lockname, self.f)
284 self.vfs.makelock(lockname, self.f)
285 self.held = 1
285 self.held = 1
286 except (OSError, IOError) as why:
286 except (OSError, IOError) as why:
287 if why.errno == errno.EEXIST:
287 if why.errno == errno.EEXIST:
288 locker = self._readlock()
288 locker = self._readlock()
289 if locker is None:
289 if locker is None:
290 continue
290 continue
291
291
292 # special case where a parent process holds the lock -- this
292 # special case where a parent process holds the lock -- this
293 # is different from the pid being different because we do
293 # is different from the pid being different because we do
294 # want the unlock and postrelease functions to be called,
294 # want the unlock and postrelease functions to be called,
295 # but the lockfile to not be removed.
295 # but the lockfile to not be removed.
296 if locker == self.parentlock:
296 if locker == self.parentlock:
297 self._parentheld = True
297 self._parentheld = True
298 self.held = 1
298 self.held = 1
299 return
299 return
300 locker = self._testlock(locker)
300 locker = self._testlock(locker)
301 if locker is not None:
301 if locker is not None:
302 raise error.LockHeld(
302 raise error.LockHeld(
303 errno.EAGAIN,
303 errno.EAGAIN,
304 self.vfs.join(self.f),
304 self.vfs.join(self.f),
305 self.desc,
305 self.desc,
306 locker,
306 locker,
307 )
307 )
308 else:
308 else:
309 raise error.LockUnavailable(
309 raise error.LockUnavailable(
310 why.errno, why.strerror, why.filename, self.desc
310 why.errno, why.strerror, why.filename, self.desc
311 )
311 )
312
312
313 if not self.held:
313 if not self.held:
314 # use empty locker to mean "busy for frequent lock/unlock
314 # use empty locker to mean "busy for frequent lock/unlock
315 # by many processes"
315 # by many processes"
316 raise error.LockHeld(
316 raise error.LockHeld(
317 errno.EAGAIN, self.vfs.join(self.f), self.desc, b""
317 errno.EAGAIN, self.vfs.join(self.f), self.desc, b""
318 )
318 )
319
319
320 def _readlock(self):
320 def _readlock(self):
321 """read lock and return its value
321 """read lock and return its value
322
322
323 Returns None if no lock exists, pid for old-style locks, and host:pid
323 Returns None if no lock exists, pid for old-style locks, and host:pid
324 for new-style locks.
324 for new-style locks.
325 """
325 """
326 try:
326 try:
327 return self.vfs.readlock(self.f)
327 return self.vfs.readlock(self.f)
328 except (OSError, IOError) as why:
328 except (OSError, IOError) as why:
329 if why.errno == errno.ENOENT:
329 if why.errno == errno.ENOENT:
330 return None
330 return None
331 raise
331 raise
332
332
333 def _lockshouldbebroken(self, locker):
333 def _lockshouldbebroken(self, locker):
334 if locker is None:
334 if locker is None:
335 return False
335 return False
336 try:
336 try:
337 host, pid = locker.split(b":", 1)
337 host, pid = locker.split(b":", 1)
338 except ValueError:
338 except ValueError:
339 return False
339 return False
340 if host != lock._host:
340 if host != lock._host:
341 return False
341 return False
342 try:
342 try:
343 pid = int(pid)
343 pid = int(pid)
344 except ValueError:
344 except ValueError:
345 return False
345 return False
346 if procutil.testpid(pid):
346 if procutil.testpid(pid):
347 return False
347 return False
348 return True
348 return True
349
349
350 def _testlock(self, locker):
350 def _testlock(self, locker):
351 if not self._lockshouldbebroken(locker):
351 if not self._lockshouldbebroken(locker):
352 return locker
352 return locker
353
353
354 # if locker dead, break lock. must do this with another lock
354 # if locker dead, break lock. must do this with another lock
355 # held, or can race and break valid lock.
355 # held, or can race and break valid lock.
356 try:
356 try:
357 with lock(self.vfs, self.f + b'.break', timeout=0):
357 with lock(self.vfs, self.f + b'.break', timeout=0):
358 locker = self._readlock()
359 if not self._lockshouldbebroken(locker):
360 return locker
358 self.vfs.unlink(self.f)
361 self.vfs.unlink(self.f)
359 except error.LockError:
362 except error.LockError:
360 return locker
363 return locker
361
364
362 def testlock(self):
365 def testlock(self):
363 """return id of locker if lock is valid, else None.
366 """return id of locker if lock is valid, else None.
364
367
365 If old-style lock, we cannot tell what machine locker is on.
368 If old-style lock, we cannot tell what machine locker is on.
366 with new-style lock, if locker is on this machine, we can
369 with new-style lock, if locker is on this machine, we can
367 see if locker is alive. If locker is on this machine but
370 see if locker is alive. If locker is on this machine but
368 not alive, we can safely break lock.
371 not alive, we can safely break lock.
369
372
370 The lock file is only deleted when None is returned.
373 The lock file is only deleted when None is returned.
371
374
372 """
375 """
373 locker = self._readlock()
376 locker = self._readlock()
374 return self._testlock(locker)
377 return self._testlock(locker)
375
378
376 @contextlib.contextmanager
379 @contextlib.contextmanager
377 def inherit(self):
380 def inherit(self):
378 """context for the lock to be inherited by a Mercurial subprocess.
381 """context for the lock to be inherited by a Mercurial subprocess.
379
382
380 Yields a string that will be recognized by the lock in the subprocess.
383 Yields a string that will be recognized by the lock in the subprocess.
381 Communicating this string to the subprocess needs to be done separately
384 Communicating this string to the subprocess needs to be done separately
382 -- typically by an environment variable.
385 -- typically by an environment variable.
383 """
386 """
384 if not self.held:
387 if not self.held:
385 raise error.LockInheritanceContractViolation(
388 raise error.LockInheritanceContractViolation(
386 b'inherit can only be called while lock is held'
389 b'inherit can only be called while lock is held'
387 )
390 )
388 if self._inherited:
391 if self._inherited:
389 raise error.LockInheritanceContractViolation(
392 raise error.LockInheritanceContractViolation(
390 b'inherit cannot be called while lock is already inherited'
393 b'inherit cannot be called while lock is already inherited'
391 )
394 )
392 if self._inheritchecker is not None:
395 if self._inheritchecker is not None:
393 self._inheritchecker()
396 self._inheritchecker()
394 if self.releasefn:
397 if self.releasefn:
395 self.releasefn()
398 self.releasefn()
396 if self._parentheld:
399 if self._parentheld:
397 lockname = self.parentlock
400 lockname = self.parentlock
398 else:
401 else:
399 lockname = b'%s:%d' % (lock._host, self.pid)
402 lockname = b'%s:%d' % (lock._host, self.pid)
400 self._inherited = True
403 self._inherited = True
401 try:
404 try:
402 yield lockname
405 yield lockname
403 finally:
406 finally:
404 if self.acquirefn:
407 if self.acquirefn:
405 self.acquirefn()
408 self.acquirefn()
406 self._inherited = False
409 self._inherited = False
407
410
408 def release(self):
411 def release(self):
409 """release the lock and execute callback function if any
412 """release the lock and execute callback function if any
410
413
411 If the lock has been acquired multiple times, the actual release is
414 If the lock has been acquired multiple times, the actual release is
412 delayed to the last release call."""
415 delayed to the last release call."""
413 if self.held > 1:
416 if self.held > 1:
414 self.held -= 1
417 self.held -= 1
415 elif self.held == 1:
418 elif self.held == 1:
416 self.held = 0
419 self.held = 0
417 if self._getpid() != self.pid:
420 if self._getpid() != self.pid:
418 # we forked, and are not the parent
421 # we forked, and are not the parent
419 return
422 return
420 try:
423 try:
421 if self.releasefn:
424 if self.releasefn:
422 self.releasefn()
425 self.releasefn()
423 finally:
426 finally:
424 if not self._parentheld:
427 if not self._parentheld:
425 try:
428 try:
426 self.vfs.unlink(self.f)
429 self.vfs.unlink(self.f)
427 except OSError:
430 except OSError:
428 pass
431 pass
429 # The postrelease functions typically assume the lock is not held
432 # The postrelease functions typically assume the lock is not held
430 # at all.
433 # at all.
431 if not self._parentheld:
434 if not self._parentheld:
432 for callback in self.postrelease:
435 for callback in self.postrelease:
433 callback()
436 callback()
434 # Prevent double usage and help clear cycles.
437 # Prevent double usage and help clear cycles.
435 self.postrelease = None
438 self.postrelease = None
436
439
437
440
438 def release(*locks):
441 def release(*locks):
439 for lock in locks:
442 for lock in locks:
440 if lock is not None:
443 if lock is not None:
441 lock.release()
444 lock.release()
General Comments 0
You need to be logged in to leave comments. Login now