1d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen"""CVS locking algorithm.
2d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
3d9c76c765496aab9b4495083b9c5c8a254eeb754David ZeuthenCVS locking strategy
4d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen====================
5d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
6d9c76c765496aab9b4495083b9c5c8a254eeb754David ZeuthenAs reverse engineered from the CVS 1.3 sources (file lock.c):
7d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
8d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen- Locking is done on a per repository basis (but a process can hold
9d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthenwrite locks for multiple directories); all lock files are placed in
10d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthenthe repository and have names beginning with "#cvs.".
11d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
12d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen- Before even attempting to lock, a file "#cvs.tfl.<pid>" is created
13d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen(and removed again), to test that we can write the repository.  [The
14d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthenalgorithm can still be fooled (1) if the repository's mode is changed
15d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthenwhile attempting to lock; (2) if this file exists and is writable but
16d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthenthe directory is not.]
17d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
18d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen- While creating the actual read/write lock files (which may exist for
19d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthena long time), a "meta-lock" is held.  The meta-lock is a directory
20d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthennamed "#cvs.lock" in the repository.  The meta-lock is also held while
21d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthena write lock is held.
22d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
23d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen- To set a read lock:
24d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
25d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen        - acquire the meta-lock
26d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen        - create the file "#cvs.rfl.<pid>"
27d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen        - release the meta-lock
28d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
29d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen- To set a write lock:
30d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
31d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen        - acquire the meta-lock
32d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen        - check that there are no files called "#cvs.rfl.*"
33d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen                - if there are, release the meta-lock, sleep, try again
34d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen        - create the file "#cvs.wfl.<pid>"
35d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
36d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen- To release a write lock:
37d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
38d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen        - remove the file "#cvs.wfl.<pid>"
39d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen        - rmdir the meta-lock
40d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
41d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen- To release a read lock:
42d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
43d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen        - remove the file "#cvs.rfl.<pid>"
44d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
45d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
46d9c76c765496aab9b4495083b9c5c8a254eeb754David ZeuthenAdditional notes
47d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen----------------
48d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
49d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen- A process should read-lock at most one repository at a time.
50d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
51d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen- A process may write-lock as many repositories as it wishes (to avoid
52d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthendeadlocks, I presume it should always lock them top-down in the
53d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthendirectory hierarchy).
54d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
55d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen- A process should make sure it removes all its lock files and
56d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthendirectories when it crashes.
57d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
58d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen- Limitation: one user id should not be committing files into the same
59d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthenrepository at the same time.
60d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
61d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
62d9c76c765496aab9b4495083b9c5c8a254eeb754David ZeuthenTurn this into Python code
63d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen--------------------------
64d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
65d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthenrl = ReadLock(repository, waittime)
66d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
67d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthenwl = WriteLock(repository, waittime)
68d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
69d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthenlist = MultipleWriteLock([repository1, repository2, ...], waittime)
70d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
71d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen"""
72d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
73d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
74d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthenimport os
75d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthenimport time
76d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthenimport stat
77d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthenimport pwd
78d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
79d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
80d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen# Default wait time
81d9c76c765496aab9b4495083b9c5c8a254eeb754David ZeuthenDELAY = 10
82d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
83d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
84d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen# XXX This should be the same on all Unix versions
85d9c76c765496aab9b4495083b9c5c8a254eeb754David ZeuthenEEXIST = 17
86d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
87d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
88d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen# Files used for locking (must match cvs.h in the CVS sources)
89d9c76c765496aab9b4495083b9c5c8a254eeb754David ZeuthenCVSLCK = "#cvs.lck"
90d9c76c765496aab9b4495083b9c5c8a254eeb754David ZeuthenCVSRFL = "#cvs.rfl."
91d9c76c765496aab9b4495083b9c5c8a254eeb754David ZeuthenCVSWFL = "#cvs.wfl."
92d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
93d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
94d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthenclass Error:
95d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
96d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen    def __init__(self, msg):
97d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen        self.msg = msg
98d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
99d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen    def __repr__(self):
100d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen        return repr(self.msg)
101d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
102d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen    def __str__(self):
103d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen        return str(self.msg)
104d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
105d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
106d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthenclass Locked(Error):
107d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen    pass
108d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
109d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
110d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthenclass Lock:
111d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen
112d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen    def __init__(self, repository = ".", delay = DELAY):
113d9c76c765496aab9b4495083b9c5c8a254eeb754David Zeuthen        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