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 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
|
# 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):
if self.locked():
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
stolen = 0
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():
os.unlink(self.tmpfname)
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
# If we've gotten to here, we should be the winner, because
# otherwise, an AlreadyCalledLockError should have been raised
# above, and we should have never gotten into this loop. However,
# the following scenario can occur, and this is what the stolen
# flag takes care of:
#
# Say that processes A and B are already laying claim to the lock
# by creating link files, and say A actually has the lock (i.e., A
# is the winner). We are process C and we lay claim by creating a
# link file. All is cool, and we'll trip the pid <> last_pid
# test, unlink our claim, sleep and try again. Second time
# through our loop, we again determine that A is the winner but
# because it and B are swapped out, we trip our hung_timeout test
# and figure we need to steal the lock. So we piss on the hydrant
# (write our info into the lock file), unlink A's link file and go
# around the loop again. However, because B is still laying
# claim, and we never knew it (since it wasn't the winner), we
# again have 3 links to the lock file the next time through this
# loop, and the assert will trip.
#
# The stolen flag alerts us that this has happened, but I still
# worry that our logic might be flawed here.
assert stolen or 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
stolen = 1
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()
# use with caution!!!
def steal(self):
self.__write()
|