"""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 = `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()
