summaryrefslogtreecommitdiff
path: root/Mailman/LockFile.py
blob: d1fb9504ab93203fc6f70a4e70d0331b46630ecb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# Copyright (C) 1998 by the Free Software Foundation, Inc.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software 
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# flock.py: Portable file locking.  John Viega, Jun 13, 1998


"""Portable (?) file locking with timeouts.  
This code should work with all versions of NFS.
The algorithm was suggested by the GNU/Linux open() man page.  Make
sure no malicious people have access to link() to the lock file.
"""

# Potential change: let the locker insert a field saying when he promises
# to be done with the lock, so if he needs more time than the other
# processes think he needs, he can say so.

import socket, os, time
import string
#from stat import ST_NLINK
ST_NLINK = 3                                      # faster

DEFAULT_HUNG_TIMEOUT   = 15
DEFAULT_SLEEP_INTERVAL = .25

AlreadyCalledLockError = "AlreadyCalledLockError"
NotLockedError         = "NotLockedError"
TimeOutError           = "TimeOutError"


class FileLock:
    def __init__(self, lockfile, hung_timeout = DEFAULT_HUNG_TIMEOUT,
                 sleep_interval = DEFAULT_SLEEP_INTERVAL):
        self.lockfile = lockfile
        self.hung_timeout = hung_timeout
        self.sleep_interval = sleep_interval
        self.tmpfname = "%s.%s.%d" % (lockfile, socket.gethostname(),
                                      os.getpid())
        self.__kickstart()

    def __del__(self):
        self.unlock()

    def __kickstart(self, force=0):
        # forcing means to remove the original lockfile, and create a new one.
        # this might be necessary if the file contains bogus locker
        # information such that the owner of the lock can't be determined
        if force:
            try:
                os.unlink(self.lockfile)
            except IOError:
                pass
        if not os.path.exists(self.lockfile):
            try:
                # make sure it's group writable
                oldmask = os.umask(002)
                try:
                    file = open(self.lockfile, 'w+')
                    file.close()
                finally:
                    os.umask(oldmask)
            except IOError:
                pass

    def __write(self):
        # make sure it's group writable
        oldmask = os.umask(002)
        try:
            fp = open(self.tmpfname, 'w')
            fp.write('%d %s\n' % (os.getpid(), self.tmpfname))
            fp.close()
        finally:
            os.umask(oldmask)

    def __read(self):
        # can raise ValueError in two situations:
        #
        # either first element wasn't an integer (a valid pid), or we didn't
        # get a 2-list from the string.split.  Either way, the data in the
        # file is bogus, but this is caught higher up
        fp = open(self.tmpfname, 'r')
        try:
            pid, winner = string.split(string.strip(fp.read()))
        finally:
            fp.close()
        return int(pid), winner

    # Note that no one new can grab the lock once we've opened our tmpfile
    # until we close it, even if we don't have the lock.  So checking the PID
    # and stealing the lock are guaranteed to be atomic.
    def lock(self, timeout = 0):
        """Blocks until the lock can be obtained.

        Raises a TimeOutError exception if a positive timeout value is given
        and that time elapses before the lock is obtained.

        """
        if timeout > 0:
            timeout_time = time.time() + timeout
        last_pid = -1
        if self.locked():
            raise AlreadyCalledLockError
        while 1:
            # create the hard link and test for exactly 2 links to the file
            os.link(self.lockfile, self.tmpfname)
            if os.stat(self.tmpfname)[ST_NLINK] == 2:
                # we have the lock (since there are no other links to the lock
                # file), so we can piss on the hydrant
                self.__write()
                break
            if timeout and timeout_time < time.time():
                raise TimeOutError
            # someone else must have gotten the lock.  let's find out who it
            # is.  if there is some bogosity in the lock file's data then we
            # will steal the lock.
            try:
                pid, winner = self.__read()
            except ValueError:
                os.unlink(self.tmpfname)
                self.__kickstart(force=1)
                continue
            assert winner <> self.tmpfname
            # record the previous winner and the current time
            if pid <> last_pid:
                last_pid = pid
                stime = time.time()
            # here's where we potentially steal the lock.  if the pid in the
            # lockfile hasn't changed in hung_timeout seconds, then we assume
            # that the locker crashed
            elif stime + self.hung_timeout < time.time():
                self.__write()                    # steal
                try:
                    os.unlink(winner)
                except os.error:
                    # winner lockfile could be missing
                    pass
                os.unlink(self.tmpfname)
                continue
            # okay, someone else has the lock, we didn't steal it, and it
            # hasn't timed out yet.  So let's wait for the owner of the lock
            # to give it up.  Unlink our claim to the lock and sleep for a
            # while, then try again
            os.unlink(self.tmpfname)
            time.sleep(self.sleep_interval)

    # This could error if the lock is stolen.  You must catch it.
    def unlock(self):
        if not self.locked():
            raise NotLockedError
        os.unlink(self.tmpfname)

    def locked(self):
        if not os.path.exists(self.tmpfname):
            return 0
        pid, winner = self.__read()
        return pid == os.getpid()