"""CVS locking algorithm. | |
CVS locking strategy | |
==================== | |
As reverse engineered from the CVS 1.3 sources (file lock.c): | |
- Locking is done on a per repository basis (but a process can hold | |
write locks for multiple directories); all lock files are placed in | |
the repository and have names beginning with "#cvs.". | |
- Before even attempting to lock, a file "#cvs.tfl.<pid>" is created | |
(and removed again), to test that we can write the repository. [The | |
algorithm can still be fooled (1) if the repository's mode is changed | |
while attempting to lock; (2) if this file exists and is writable but | |
the directory is not.] | |
- While creating the actual read/write lock files (which may exist for | |
a long time), a "meta-lock" is held. The meta-lock is a directory | |
named "#cvs.lock" in the repository. The meta-lock is also held while | |
a write lock is held. | |
- To set a read lock: | |
- acquire the meta-lock | |
- create the file "#cvs.rfl.<pid>" | |
- release the meta-lock | |
- To set a write lock: | |
- acquire the meta-lock | |
- check that there are no files called "#cvs.rfl.*" | |
- if there are, release the meta-lock, sleep, try again | |
- create the file "#cvs.wfl.<pid>" | |
- To release a write lock: | |
- remove the file "#cvs.wfl.<pid>" | |
- rmdir the meta-lock | |
- To release a read lock: | |
- remove the file "#cvs.rfl.<pid>" | |
Additional notes | |
---------------- | |
- A process should read-lock at most one repository at a time. | |
- A process may write-lock as many repositories as it wishes (to avoid | |
deadlocks, I presume it should always lock them top-down in the | |
directory hierarchy). | |
- A process should make sure it removes all its lock files and | |
directories when it crashes. | |
- Limitation: one user id should not be committing files into the same | |
repository at the same time. | |
Turn this into Python code | |
-------------------------- | |
rl = ReadLock(repository, waittime) | |
wl = WriteLock(repository, waittime) | |
list = MultipleWriteLock([repository1, repository2, ...], waittime) | |
""" | |
import os | |
import time | |
import stat | |
import pwd | |
# Default wait time | |
DELAY = 10 | |
# XXX This should be the same on all Unix versions | |
EEXIST = 17 | |
# Files used for locking (must match cvs.h in the CVS sources) | |
CVSLCK = "#cvs.lck" | |
CVSRFL = "#cvs.rfl." | |
CVSWFL = "#cvs.wfl." | |
class Error: | |
def __init__(self, msg): | |
self.msg = msg | |
def __repr__(self): | |
return repr(self.msg) | |
def __str__(self): | |
return str(self.msg) | |
class Locked(Error): | |
pass | |
class Lock: | |
def __init__(self, repository = ".", delay = DELAY): | |
self.repository = repository | |
self.delay = delay | |
self.lockdir = None | |
self.lockfile = None | |
pid = repr(os.getpid()) | |
self.cvslck = self.join(CVSLCK) | |
self.cvsrfl = self.join(CVSRFL + pid) | |
self.cvswfl = self.join(CVSWFL + pid) | |
def __del__(self): | |
print "__del__" | |
self.unlock() | |
def setlockdir(self): | |
while 1: | |
try: | |
self.lockdir = self.cvslck | |
os.mkdir(self.cvslck, 0777) | |
return | |
except os.error, msg: | |
self.lockdir = None | |
if msg[0] == EEXIST: | |
try: | |
st = os.stat(self.cvslck) | |
except os.error: | |
continue | |
self.sleep(st) | |
continue | |
raise Error("failed to lock %s: %s" % ( | |
self.repository, msg)) | |
def unlock(self): | |
self.unlockfile() | |
self.unlockdir() | |
def unlockfile(self): | |
if self.lockfile: | |
print "unlink", self.lockfile | |
try: | |
os.unlink(self.lockfile) | |
except os.error: | |
pass | |
self.lockfile = None | |
def unlockdir(self): | |
if self.lockdir: | |
print "rmdir", self.lockdir | |
try: | |
os.rmdir(self.lockdir) | |
except os.error: | |
pass | |
self.lockdir = None | |
def sleep(self, st): | |
sleep(st, self.repository, self.delay) | |
def join(self, name): | |
return os.path.join(self.repository, name) | |
def sleep(st, repository, delay): | |
if delay <= 0: | |
raise Locked(st) | |
uid = st[stat.ST_UID] | |
try: | |
pwent = pwd.getpwuid(uid) | |
user = pwent[0] | |
except KeyError: | |
user = "uid %d" % uid | |
print "[%s]" % time.ctime(time.time())[11:19], | |
print "Waiting for %s's lock in" % user, repository | |
time.sleep(delay) | |
class ReadLock(Lock): | |
def __init__(self, repository, delay = DELAY): | |
Lock.__init__(self, repository, delay) | |
ok = 0 | |
try: | |
self.setlockdir() | |
self.lockfile = self.cvsrfl | |
fp = open(self.lockfile, 'w') | |
fp.close() | |
ok = 1 | |
finally: | |
if not ok: | |
self.unlockfile() | |
self.unlockdir() | |
class WriteLock(Lock): | |
def __init__(self, repository, delay = DELAY): | |
Lock.__init__(self, repository, delay) | |
self.setlockdir() | |
while 1: | |
uid = self.readers_exist() | |
if not uid: | |
break | |
self.unlockdir() | |
self.sleep(uid) | |
self.lockfile = self.cvswfl | |
fp = open(self.lockfile, 'w') | |
fp.close() | |
def readers_exist(self): | |
n = len(CVSRFL) | |
for name in os.listdir(self.repository): | |
if name[:n] == CVSRFL: | |
try: | |
st = os.stat(self.join(name)) | |
except os.error: | |
continue | |
return st | |
return None | |
def MultipleWriteLock(repositories, delay = DELAY): | |
while 1: | |
locks = [] | |
for r in repositories: | |
try: | |
locks.append(WriteLock(r, 0)) | |
except Locked, instance: | |
del locks | |
break | |
else: | |
break | |
sleep(instance.msg, r, delay) | |
return list | |
def test(): | |
import sys | |
if sys.argv[1:]: | |
repository = sys.argv[1] | |
else: | |
repository = "." | |
rl = None | |
wl = None | |
try: | |
print "attempting write lock ..." | |
wl = WriteLock(repository) | |
print "got it." | |
wl.unlock() | |
print "attempting read lock ..." | |
rl = ReadLock(repository) | |
print "got it." | |
rl.unlock() | |
finally: | |
print [1] | |
sys.exc_traceback = None | |
print [2] | |
if rl: | |
rl.unlock() | |
print [3] | |
if wl: | |
wl.unlock() | |
print [4] | |
rl = None | |
print [5] | |
wl = None | |
print [6] | |
if __name__ == '__main__': | |
test() |