1"""CVS locking algorithm.
2
3CVS locking strategy
4====================
5
6As reverse engineered from the CVS 1.3 sources (file lock.c):
7
8- Locking is done on a per repository basis (but a process can hold
9write locks for multiple directories); all lock files are placed in
10the repository and have names beginning with "#cvs.".
11
12- Before even attempting to lock, a file "#cvs.tfl.<pid>" is created
13(and removed again), to test that we can write the repository.  [The
14algorithm can still be fooled (1) if the repository's mode is changed
15while attempting to lock; (2) if this file exists and is writable but
16the directory is not.]
17
18- While creating the actual read/write lock files (which may exist for
19a long time), a "meta-lock" is held.  The meta-lock is a directory
20named "#cvs.lock" in the repository.  The meta-lock is also held while
21a write lock is held.
22
23- To set a read lock:
24
25        - acquire the meta-lock
26        - create the file "#cvs.rfl.<pid>"
27        - release the meta-lock
28
29- To set a write lock:
30
31        - acquire the meta-lock
32        - check that there are no files called "#cvs.rfl.*"
33                - if there are, release the meta-lock, sleep, try again
34        - create the file "#cvs.wfl.<pid>"
35
36- To release a write lock:
37
38        - remove the file "#cvs.wfl.<pid>"
39        - rmdir the meta-lock
40
41- To release a read lock:
42
43        - remove the file "#cvs.rfl.<pid>"
44
45
46Additional notes
47----------------
48
49- A process should read-lock at most one repository at a time.
50
51- A process may write-lock as many repositories as it wishes (to avoid
52deadlocks, I presume it should always lock them top-down in the
53directory hierarchy).
54
55- A process should make sure it removes all its lock files and
56directories when it crashes.
57
58- Limitation: one user id should not be committing files into the same
59repository at the same time.
60
61
62Turn this into Python code
63--------------------------
64
65rl = ReadLock(repository, waittime)
66
67wl = WriteLock(repository, waittime)
68
69list = MultipleWriteLock([repository1, repository2, ...], waittime)
70
71"""
72
73
74import os
75import time
76import stat
77import pwd
78
79
80# Default wait time
81DELAY = 10
82
83
84# XXX This should be the same on all Unix versions
85EEXIST = 17
86
87
88# Files used for locking (must match cvs.h in the CVS sources)
89CVSLCK = "#cvs.lck"
90CVSRFL = "#cvs.rfl."
91CVSWFL = "#cvs.wfl."
92
93
94class Error:
95
96    def __init__(self, msg):
97        self.msg = msg
98
99    def __repr__(self):
100        return repr(self.msg)
101
102    def __str__(self):
103        return str(self.msg)
104
105
106class Locked(Error):
107    pass
108
109
110class Lock:
111
112    def __init__(self, repository = ".", delay = DELAY):
113        self.repository = repository
114        self.delay = delay
115        self.lockdir = None
116        self.lockfile = None
117        pid = repr(os.getpid())
118        self.cvslck = self.join(CVSLCK)
119        self.cvsrfl = self.join(CVSRFL + pid)
120        self.cvswfl = self.join(CVSWFL + pid)
121
122    def __del__(self):
123        print "__del__"
124        self.unlock()
125
126    def setlockdir(self):
127        while 1:
128            try:
129                self.lockdir = self.cvslck
130                os.mkdir(self.cvslck, 0777)
131                return
132            except os.error, msg:
133                self.lockdir = None
134                if msg[0] == EEXIST:
135                    try:
136                        st = os.stat(self.cvslck)
137                    except os.error:
138                        continue
139                    self.sleep(st)
140                    continue
141                raise Error("failed to lock %s: %s" % (
142                        self.repository, msg))
143
144    def unlock(self):
145        self.unlockfile()
146        self.unlockdir()
147
148    def unlockfile(self):
149        if self.lockfile:
150            print "unlink", self.lockfile
151            try:
152                os.unlink(self.lockfile)
153            except os.error:
154                pass
155            self.lockfile = None
156
157    def unlockdir(self):
158        if self.lockdir:
159            print "rmdir", self.lockdir
160            try:
161                os.rmdir(self.lockdir)
162            except os.error:
163                pass
164            self.lockdir = None
165
166    def sleep(self, st):
167        sleep(st, self.repository, self.delay)
168
169    def join(self, name):
170        return os.path.join(self.repository, name)
171
172
173def sleep(st, repository, delay):
174    if delay <= 0:
175        raise Locked(st)
176    uid = st[stat.ST_UID]
177    try:
178        pwent = pwd.getpwuid(uid)
179        user = pwent[0]
180    except KeyError:
181        user = "uid %d" % uid
182    print "[%s]" % time.ctime(time.time())[11:19],
183    print "Waiting for %s's lock in" % user, repository
184    time.sleep(delay)
185
186
187class ReadLock(Lock):
188
189    def __init__(self, repository, delay = DELAY):
190        Lock.__init__(self, repository, delay)
191        ok = 0
192        try:
193            self.setlockdir()
194            self.lockfile = self.cvsrfl
195            fp = open(self.lockfile, 'w')
196            fp.close()
197            ok = 1
198        finally:
199            if not ok:
200                self.unlockfile()
201            self.unlockdir()
202
203
204class WriteLock(Lock):
205
206    def __init__(self, repository, delay = DELAY):
207        Lock.__init__(self, repository, delay)
208        self.setlockdir()
209        while 1:
210            uid = self.readers_exist()
211            if not uid:
212                break
213            self.unlockdir()
214            self.sleep(uid)
215        self.lockfile = self.cvswfl
216        fp = open(self.lockfile, 'w')
217        fp.close()
218
219    def readers_exist(self):
220        n = len(CVSRFL)
221        for name in os.listdir(self.repository):
222            if name[:n] == CVSRFL:
223                try:
224                    st = os.stat(self.join(name))
225                except os.error:
226                    continue
227                return st
228        return None
229
230
231def MultipleWriteLock(repositories, delay = DELAY):
232    while 1:
233        locks = []
234        for r in repositories:
235            try:
236                locks.append(WriteLock(r, 0))
237            except Locked, instance:
238                del locks
239                break
240        else:
241            break
242        sleep(instance.msg, r, delay)
243    return list
244
245
246def test():
247    import sys
248    if sys.argv[1:]:
249        repository = sys.argv[1]
250    else:
251        repository = "."
252    rl = None
253    wl = None
254    try:
255        print "attempting write lock ..."
256        wl = WriteLock(repository)
257        print "got it."
258        wl.unlock()
259        print "attempting read lock ..."
260        rl = ReadLock(repository)
261        print "got it."
262        rl.unlock()
263    finally:
264        print [1]
265        sys.exc_traceback = None
266        print [2]
267        if rl:
268            rl.unlock()
269        print [3]
270        if wl:
271            wl.unlock()
272        print [4]
273        rl = None
274        print [5]
275        wl = None
276        print [6]
277
278
279if __name__ == '__main__':
280    test()
281