Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
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.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  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. import errno
  9. import os
  10. from time import time as _uniquefloat
  11. from twisted.python.runtime import platform
  12. def unique():
  13. return str(int(_uniquefloat() * 1000))
  14. from os import rename
  15. if not platform.isWindows():
  16. from os import kill, readlink, remove as rmlink, symlink
  17. _windows = False
  18. else:
  19. _windows = True
  20. # On UNIX, a symlink can be made to a nonexistent location, and
  21. # FilesystemLock uses this by making the target of the symlink an
  22. # imaginary, non-existing file named that of the PID of the process with
  23. # the lock. This has some benefits on UNIX -- making and removing this
  24. # symlink is atomic. However, because Windows doesn't support symlinks (at
  25. # least as how we know them), we have to fake this and actually write a
  26. # file with the PID of the process holding the lock instead.
  27. # These functions below perform that unenviable, probably-fraught-with-
  28. # race-conditions duty. - hawkie
  29. try:
  30. import pywintypes # type: ignore[import]
  31. from win32api import OpenProcess # type: ignore[import]
  32. except ImportError:
  33. kill = None # type: ignore[assignment]
  34. else:
  35. ERROR_ACCESS_DENIED = 5
  36. ERROR_INVALID_PARAMETER = 87
  37. # typing ignored due to:
  38. # https://github.com/python/typeshed/issues/4249
  39. def kill(pid, signal): # type: ignore[misc]
  40. try:
  41. OpenProcess(0, 0, pid)
  42. except pywintypes.error as e:
  43. if e.args[0] == ERROR_ACCESS_DENIED:
  44. return
  45. elif e.args[0] == ERROR_INVALID_PARAMETER:
  46. raise OSError(errno.ESRCH, None)
  47. raise
  48. else:
  49. raise RuntimeError("OpenProcess is required to fail.")
  50. # For monkeypatching in tests
  51. _open = open
  52. # typing ignored due to:
  53. # https://github.com/python/typeshed/issues/4249
  54. def symlink(value, filename): # type: ignore[misc]
  55. """
  56. Write a file at C{filename} with the contents of C{value}. See the
  57. above comment block as to why this is needed.
  58. """
  59. # XXX Implement an atomic thingamajig for win32
  60. newlinkname = filename + "." + unique() + ".newlink"
  61. newvalname = os.path.join(newlinkname, "symlink")
  62. os.mkdir(newlinkname)
  63. # Python 3 does not support the 'commit' flag of fopen in the MSVCRT
  64. # (http://msdn.microsoft.com/en-us/library/yeby3zcb%28VS.71%29.aspx)
  65. mode = "w"
  66. with _open(newvalname, mode) as f:
  67. f.write(value)
  68. f.flush()
  69. try:
  70. rename(newlinkname, filename)
  71. except BaseException:
  72. os.remove(newvalname)
  73. os.rmdir(newlinkname)
  74. raise
  75. # typing ignored due to:
  76. # https://github.com/python/typeshed/issues/4249
  77. def readlink(filename): # type: ignore[misc]
  78. """
  79. Read the contents of C{filename}. See the above comment block as to why
  80. this is needed.
  81. """
  82. try:
  83. fObj = _open(os.path.join(filename, "symlink"), "r")
  84. except OSError as e:
  85. if e.errno == errno.ENOENT or e.errno == errno.EIO:
  86. raise OSError(e.errno, None)
  87. raise
  88. else:
  89. with fObj:
  90. result = fObj.read()
  91. return result
  92. # typing ignored due to:
  93. # https://github.com/python/typeshed/issues/4249
  94. def rmlink(filename): # type: ignore[misc]
  95. os.remove(os.path.join(filename, "symlink"))
  96. os.rmdir(filename)
  97. class FilesystemLock:
  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 OSError: Any exception L{os.symlink()} may raise,
  121. other than L{errno.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 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 OSError: Any exception L{os.readlink()} may raise.
  178. @raise 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(f"Lock {self.name!r} not owned by this process")
  183. rmlink(self.name)
  184. self.locked = False
  185. def isLocked(name):
  186. """
  187. Determine if the lock of the given name is held or not.
  188. @type name: C{str}
  189. @param name: The filesystem path to the lock to test
  190. @rtype: C{bool}
  191. @return: True if the lock is held, False otherwise.
  192. """
  193. l = FilesystemLock(name)
  194. result = None
  195. try:
  196. result = l.lock()
  197. finally:
  198. if result:
  199. l.unlock()
  200. return not result
  201. __all__ = ["FilesystemLock", "isLocked"]