|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- # -*- test-case-name: twisted.test.test_lockfile -*-
- # Copyright (c) 2005 Divmod, Inc.
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
-
- """
- Filesystem-based interprocess mutex.
- """
-
-
- import errno
- import os
- from time import time as _uniquefloat
-
- from twisted.python.runtime import platform
-
-
- def unique():
- return str(int(_uniquefloat() * 1000))
-
-
- from os import rename
-
- if not platform.isWindows():
- from os import kill, readlink, remove as rmlink, symlink
-
- _windows = False
- else:
- _windows = True
-
- # On UNIX, a symlink can be made to a nonexistent location, and
- # FilesystemLock uses this by making the target of the symlink an
- # imaginary, non-existing file named that of the PID of the process with
- # the lock. This has some benefits on UNIX -- making and removing this
- # symlink is atomic. However, because Windows doesn't support symlinks (at
- # least as how we know them), we have to fake this and actually write a
- # file with the PID of the process holding the lock instead.
- # These functions below perform that unenviable, probably-fraught-with-
- # race-conditions duty. - hawkie
-
- try:
- import pywintypes # type: ignore[import]
- from win32api import OpenProcess # type: ignore[import]
- except ImportError:
- kill = None # type: ignore[assignment]
- else:
- ERROR_ACCESS_DENIED = 5
- ERROR_INVALID_PARAMETER = 87
-
- # typing ignored due to:
- # https://github.com/python/typeshed/issues/4249
- def kill(pid, signal): # type: ignore[misc]
- try:
- OpenProcess(0, 0, pid)
- except pywintypes.error as e:
- if e.args[0] == ERROR_ACCESS_DENIED:
- return
- elif e.args[0] == ERROR_INVALID_PARAMETER:
- raise OSError(errno.ESRCH, None)
- raise
- else:
- raise RuntimeError("OpenProcess is required to fail.")
-
- # For monkeypatching in tests
- _open = open
-
- # typing ignored due to:
- # https://github.com/python/typeshed/issues/4249
- def symlink(value, filename): # type: ignore[misc]
- """
- Write a file at C{filename} with the contents of C{value}. See the
- above comment block as to why this is needed.
- """
- # XXX Implement an atomic thingamajig for win32
- newlinkname = filename + "." + unique() + ".newlink"
- newvalname = os.path.join(newlinkname, "symlink")
- os.mkdir(newlinkname)
-
- # Python 3 does not support the 'commit' flag of fopen in the MSVCRT
- # (http://msdn.microsoft.com/en-us/library/yeby3zcb%28VS.71%29.aspx)
- mode = "w"
-
- with _open(newvalname, mode) as f:
- f.write(value)
- f.flush()
-
- try:
- rename(newlinkname, filename)
- except BaseException:
- os.remove(newvalname)
- os.rmdir(newlinkname)
- raise
-
- # typing ignored due to:
- # https://github.com/python/typeshed/issues/4249
- def readlink(filename): # type: ignore[misc]
- """
- Read the contents of C{filename}. See the above comment block as to why
- this is needed.
- """
- try:
- fObj = _open(os.path.join(filename, "symlink"), "r")
- except OSError as e:
- if e.errno == errno.ENOENT or e.errno == errno.EIO:
- raise OSError(e.errno, None)
- raise
- else:
- with fObj:
- result = fObj.read()
- return result
-
- # typing ignored due to:
- # https://github.com/python/typeshed/issues/4249
- def rmlink(filename): # type: ignore[misc]
- os.remove(os.path.join(filename, "symlink"))
- os.rmdir(filename)
-
-
- class FilesystemLock:
- """
- A mutex.
-
- This relies on the filesystem property that creating
- a symlink is an atomic operation and that it will
- fail if the symlink already exists. Deleting the
- symlink will release the lock.
-
- @ivar name: The name of the file associated with this lock.
-
- @ivar clean: Indicates whether this lock was released cleanly by its
- last owner. Only meaningful after C{lock} has been called and
- returns True.
-
- @ivar locked: Indicates whether the lock is currently held by this
- object.
- """
-
- clean = None
- locked = False
-
- def __init__(self, name):
- self.name = name
-
- def lock(self):
- """
- Acquire this lock.
-
- @rtype: C{bool}
- @return: True if the lock is acquired, false otherwise.
-
- @raise OSError: Any exception L{os.symlink()} may raise,
- other than L{errno.EEXIST}.
- """
- clean = True
- while True:
- try:
- symlink(str(os.getpid()), self.name)
- except OSError as e:
- if _windows and e.errno in (errno.EACCES, errno.EIO):
- # The lock is in the middle of being deleted because we're
- # on Windows where lock removal isn't atomic. Give up, we
- # don't know how long this is going to take.
- return False
- if e.errno == errno.EEXIST:
- try:
- pid = readlink(self.name)
- except OSError as e:
- if e.errno == errno.ENOENT:
- # The lock has vanished, try to claim it in the
- # next iteration through the loop.
- continue
- elif _windows and e.errno == errno.EACCES:
- # The lock is in the middle of being
- # deleted because we're on Windows where
- # lock removal isn't atomic. Give up, we
- # don't know how long this is going to
- # take.
- return False
- raise
- try:
- if kill is not None:
- kill(int(pid), 0)
- except OSError as e:
- if e.errno == errno.ESRCH:
- # The owner has vanished, try to claim it in the
- # next iteration through the loop.
- try:
- rmlink(self.name)
- except OSError as e:
- if e.errno == errno.ENOENT:
- # Another process cleaned up the lock.
- # Race them to acquire it in the next
- # iteration through the loop.
- continue
- raise
- clean = False
- continue
- raise
- return False
- raise
- self.locked = True
- self.clean = clean
- return True
-
- def unlock(self):
- """
- Release this lock.
-
- This deletes the directory with the given name.
-
- @raise OSError: Any exception L{os.readlink()} may raise.
- @raise ValueError: If the lock is not owned by this process.
- """
- pid = readlink(self.name)
- if int(pid) != os.getpid():
- raise ValueError(f"Lock {self.name!r} not owned by this process")
- rmlink(self.name)
- self.locked = False
-
-
- def isLocked(name):
- """
- Determine if the lock of the given name is held or not.
-
- @type name: C{str}
- @param name: The filesystem path to the lock to test
-
- @rtype: C{bool}
- @return: True if the lock is held, False otherwise.
- """
- l = FilesystemLock(name)
- result = None
- try:
- result = l.lock()
- finally:
- if result:
- l.unlock()
- return not result
-
-
- __all__ = ["FilesystemLock", "isLocked"]
|