Jason R. Coombs avatar Jason R. Coombs committed 06e292d Merge

Merge more robust platform-independent locking technique, referencing #1122.

Comments (0)

Files changed (2)

cherrypy/lib/lockfile.py

+"""
+Platform-independent file locking. Inspired by and modeled after zc.lockfile.
+"""
+
+import os
+
+try:
+    import msvcrt
+except ImportError:
+    pass
+
+try:
+    import fcntl
+except ImportError:
+    pass
+
+
+class LockError(Exception):
+    "Could not obtain a lock"
+
+    msg = "Unable to lock %r"
+
+    def __init__(self, path):
+        super(LockError, self).__init__(self.msg % path)
+
+
+class UnlockError(LockError):
+    "Could not release a lock"
+
+    msg = "Unable to unlock %r"
+
+
+# first, a default, naive locking implementation
+class LockFile(object):
+    """
+    A default, naive locking implementation. Always fails if the file
+    already exists.
+    """
+
+    def __init__(self, path):
+        self.path = path
+        try:
+            fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
+        except OSError:
+            raise LockError(self.path)
+        os.close(fd)
+
+    def release(self):
+        os.remove(self.path)
+
+    def remove(self):
+        pass
+
+
+class SystemLockFile(object):
+    """
+    An abstract base class for platform-specific locking.
+    """
+
+    def __init__(self, path):
+        self.path = path
+
+        try:
+            # Open lockfile for writing without truncation:
+            self.fp = open(path, 'r+')
+        except IOError:
+            # If the file doesn't exist, IOError is raised; Use a+ instead.
+            # Note that there may be a race here. Multiple processes
+            # could fail on the r+ open and open the file a+, but only
+            # one will get the the lock and write a pid.
+            self.fp = open(path, 'a+')
+
+        try:
+            self._lock_file()
+        except:
+            self.fp.seek(1)
+            self.fp.close()
+            del self.fp
+            raise
+
+        self.fp.write(" %s\n" % os.getpid())
+        self.fp.truncate()
+        self.fp.flush()
+
+    def release(self):
+        if not hasattr(self, 'fp'):
+            return
+        self._unlock_file()
+        self.fp.close()
+        del self.fp
+
+    def remove(self):
+        """
+        Attempt to remove the file
+        """
+        try:
+            os.remove(self.path)
+        except:
+            pass
+
+    #@abc.abstract_method
+    #def _lock_file(self):
+    #    """Attempt to obtain the lock on self.fp. Raise LockError if not
+    #    acquired."""
+
+    def _unlock_file(self):
+        """Attempt to obtain the lock on self.fp. Raise UnlockError if not
+        released."""
+
+
+class WindowsLockFile(SystemLockFile):
+    def _lock_file(self):
+        # Lock just the first byte
+        try:
+            msvcrt.locking(self.fp.fileno(), msvcrt.LK_NBLCK, 1)
+        except IOError:
+            raise LockError(self.fp.name)
+
+    def _unlock_file(self):
+        try:
+            self.fp.seek(0)
+            msvcrt.locking(self.fp.fileno(), msvcrt.LK_UNLCK, 1)
+        except IOError:
+            raise UnlockError(self.fp.name)
+
+if 'msvcrt' in globals():
+    LockFile = WindowsLockFile
+
+
+class UnixLockFile(SystemLockFile):
+    def _lock_file(self):
+        flags = fcntl.LOCK_EX | fcntl.LOCK_NB
+        try:
+            fcntl.flock(self.fp.fileno(), flags)
+        except IOError:
+            raise LockError(self.fp.name)
+
+    # no need to implement _unlock_file, it will be unlocked on close()
+
+if 'fcntl' in globals():
+    LockFile = UnixLockFile

cherrypy/lib/sessions.py

 import time
 import threading
 import types
-from warnings import warn
 
 import cherrypy
 from cherrypy._cpcompat import copyitems, pickle, random20, unicodestr
 from cherrypy.lib import httputil
-
+from cherrypy.lib import lockfile
 
 missing = object()
 
     LOCK_SUFFIX = '.lock'
     pickle_protocol = pickle.HIGHEST_PROTOCOL
 
+    # class-level objects - don't rebind these
+    locks = {}
+
     def __init__(self, id=None, **kwargs):
         # The 'storage_path' arg is required for file-based sessions.
         kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
 
         for k, v in kwargs.items():
             setattr(cls, k, v)
-
-        # Warn if any lock files exist at startup.
-        lockfiles = [fname for fname in os.listdir(cls.storage_path)
-                     if (fname.startswith(cls.SESSION_PREFIX)
-                         and fname.endswith(cls.LOCK_SUFFIX))]
-        if lockfiles:
-            plural = ('', 's')[len(lockfiles) > 1]
-            warn("%s session lockfile%s found at startup. If you are "
-                 "only running one process, then you may need to "
-                 "manually delete the lockfiles found at %r."
-                 % (len(lockfiles), plural, cls.storage_path))
     setup = classmethod(setup)
 
     def _get_file_path(self):
         path += self.LOCK_SUFFIX
         while True:
             try:
-                lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL)
-            except OSError:
+                self.locks[path] = lockfile.LockFile(path)
+            except lockfile.LockError:
                 time.sleep(0.1)
             else:
-                os.close(lockfd)
                 break
         self.locked = True
         if self.debug:
         """Release the lock on the currently-loaded session data."""
         if path is None:
             path = self._get_file_path()
-        os.unlink(path + self.LOCK_SUFFIX)
+        path += self.LOCK_SUFFIX
+
+        lock = self.locks.pop(path)
+        lock.release()
+        lock.remove()
+
         self.locked = False
 
     def clean_up(self):
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.