You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

lockfile.py 7.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. # -*- test-case-name: twisted.test.test_lockfile -*-
  2. # Copyright (c) 2005 Divmod, Inc.
  3. # Copyright (c) Twisted Matrix Laboratories.
  4. # See LICENSE for details.
  5. """
  6. Filesystem-based interprocess mutex.
  7. """
  8. from __future__ import absolute_import, division
  9. import errno
  10. import os
  11. from time import time as _uniquefloat
  12. from twisted.python.runtime import platform
  13. from twisted.python.compat import _PY3
  14. def unique():
  15. return str(int(_uniquefloat() * 1000))
  16. from os import rename
  17. if not platform.isWindows():
  18. from os import kill
  19. from os import symlink
  20. from os import readlink
  21. from os import remove as rmlink
  22. _windows = False
  23. else:
  24. _windows = True
  25. # On UNIX, a symlink can be made to a nonexistent location, and
  26. # FilesystemLock uses this by making the target of the symlink an
  27. # imaginary, non-existing file named that of the PID of the process with
  28. # the lock. This has some benefits on UNIX -- making and removing this
  29. # symlink is atomic. However, because Windows doesn't support symlinks (at
  30. # least as how we know them), we have to fake this and actually write a
  31. # file with the PID of the process holding the lock instead.
  32. # These functions below perform that unenviable, probably-fraught-with-
  33. # race-conditions duty. - hawkie
  34. try:
  35. from win32api import OpenProcess
  36. import pywintypes
  37. except ImportError:
  38. kill = None
  39. else:
  40. ERROR_ACCESS_DENIED = 5
  41. ERROR_INVALID_PARAMETER = 87
  42. def kill(pid, signal):
  43. try:
  44. OpenProcess(0, 0, pid)
  45. except pywintypes.error as e:
  46. if e.args[0] == ERROR_ACCESS_DENIED:
  47. return
  48. elif e.args[0] == ERROR_INVALID_PARAMETER:
  49. raise OSError(errno.ESRCH, None)
  50. raise
  51. else:
  52. raise RuntimeError("OpenProcess is required to fail.")
  53. # For monkeypatching in tests
  54. _open = open
  55. def symlink(value, filename):
  56. """
  57. Write a file at C{filename} with the contents of C{value}. See the
  58. above comment block as to why this is needed.
  59. """
  60. # XXX Implement an atomic thingamajig for win32
  61. newlinkname = filename + "." + unique() + '.newlink'
  62. newvalname = os.path.join(newlinkname, "symlink")
  63. os.mkdir(newlinkname)
  64. # Python 3 does not support the 'commit' flag of fopen in the MSVCRT
  65. # (http://msdn.microsoft.com/en-us/library/yeby3zcb%28VS.71%29.aspx)
  66. if _PY3:
  67. mode = 'w'
  68. else:
  69. mode = 'wc'
  70. with _open(newvalname, mode) as f:
  71. f.write(value)
  72. f.flush()
  73. try:
  74. rename(newlinkname, filename)
  75. except:
  76. os.remove(newvalname)
  77. os.rmdir(newlinkname)
  78. raise
  79. def readlink(filename):
  80. """
  81. Read the contents of C{filename}. See the above comment block as to why
  82. this is needed.
  83. """
  84. try:
  85. fObj = _open(os.path.join(filename, 'symlink'), 'r')
  86. except IOError as e:
  87. if e.errno == errno.ENOENT or e.errno == errno.EIO:
  88. raise OSError(e.errno, None)
  89. raise
  90. else:
  91. with fObj:
  92. result = fObj.read()
  93. return result
  94. def rmlink(filename):
  95. os.remove(os.path.join(filename, 'symlink'))
  96. os.rmdir(filename)
  97. class FilesystemLock(object):
  98. """
  99. A mutex.
  100. This relies on the filesystem property that creating
  101. a symlink is an atomic operation and that it will
  102. fail if the symlink already exists. Deleting the
  103. symlink will release the lock.
  104. @ivar name: The name of the file associated with this lock.
  105. @ivar clean: Indicates whether this lock was released cleanly by its
  106. last owner. Only meaningful after C{lock} has been called and
  107. returns True.
  108. @ivar locked: Indicates whether the lock is currently held by this
  109. object.
  110. """
  111. clean = None
  112. locked = False
  113. def __init__(self, name):
  114. self.name = name
  115. def lock(self):
  116. """
  117. Acquire this lock.
  118. @rtype: C{bool}
  119. @return: True if the lock is acquired, false otherwise.
  120. @raise: Any exception os.symlink() may raise, other than
  121. EEXIST.
  122. """
  123. clean = True
  124. while True:
  125. try:
  126. symlink(str(os.getpid()), self.name)
  127. except OSError as e:
  128. if _windows and e.errno in (errno.EACCES, errno.EIO):
  129. # The lock is in the middle of being deleted because we're
  130. # on Windows where lock removal isn't atomic. Give up, we
  131. # don't know how long this is going to take.
  132. return False
  133. if e.errno == errno.EEXIST:
  134. try:
  135. pid = readlink(self.name)
  136. except (IOError, OSError) as e:
  137. if e.errno == errno.ENOENT:
  138. # The lock has vanished, try to claim it in the
  139. # next iteration through the loop.
  140. continue
  141. elif _windows and e.errno == errno.EACCES:
  142. # The lock is in the middle of being
  143. # deleted because we're on Windows where
  144. # lock removal isn't atomic. Give up, we
  145. # don't know how long this is going to
  146. # take.
  147. return False
  148. raise
  149. try:
  150. if kill is not None:
  151. kill(int(pid), 0)
  152. except OSError as e:
  153. if e.errno == errno.ESRCH:
  154. # The owner has vanished, try to claim it in the
  155. # next iteration through the loop.
  156. try:
  157. rmlink(self.name)
  158. except OSError as e:
  159. if e.errno == errno.ENOENT:
  160. # Another process cleaned up the lock.
  161. # Race them to acquire it in the next
  162. # iteration through the loop.
  163. continue
  164. raise
  165. clean = False
  166. continue
  167. raise
  168. return False
  169. raise
  170. self.locked = True
  171. self.clean = clean
  172. return True
  173. def unlock(self):
  174. """
  175. Release this lock.
  176. This deletes the directory with the given name.
  177. @raise: Any exception os.readlink() may raise, or
  178. ValueError if the lock is not owned by this process.
  179. """
  180. pid = readlink(self.name)
  181. if int(pid) != os.getpid():
  182. raise ValueError(
  183. "Lock %r not owned by this process" % (self.name,))
  184. rmlink(self.name)
  185. self.locked = False
  186. def isLocked(name):
  187. """
  188. Determine if the lock of the given name is held or not.
  189. @type name: C{str}
  190. @param name: The filesystem path to the lock to test
  191. @rtype: C{bool}
  192. @return: True if the lock is held, False otherwise.
  193. """
  194. l = FilesystemLock(name)
  195. result = None
  196. try:
  197. result = l.lock()
  198. finally:
  199. if result:
  200. l.unlock()
  201. return not result
  202. __all__ = ['FilesystemLock', 'isLocked']