|
1 """CVS locking algorithm. |
|
2 |
|
3 CVS locking strategy |
|
4 ==================== |
|
5 |
|
6 As 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 |
|
9 write locks for multiple directories); all lock files are placed in |
|
10 the 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 |
|
14 algorithm can still be fooled (1) if the repository's mode is changed |
|
15 while attempting to lock; (2) if this file exists and is writable but |
|
16 the directory is not.] |
|
17 |
|
18 - While creating the actual read/write lock files (which may exist for |
|
19 a long time), a "meta-lock" is held. The meta-lock is a directory |
|
20 named "#cvs.lock" in the repository. The meta-lock is also held while |
|
21 a 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 |
|
46 Additional 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 |
|
52 deadlocks, I presume it should always lock them top-down in the |
|
53 directory hierarchy). |
|
54 |
|
55 - A process should make sure it removes all its lock files and |
|
56 directories when it crashes. |
|
57 |
|
58 - Limitation: one user id should not be committing files into the same |
|
59 repository at the same time. |
|
60 |
|
61 |
|
62 Turn this into Python code |
|
63 -------------------------- |
|
64 |
|
65 rl = ReadLock(repository, waittime) |
|
66 |
|
67 wl = WriteLock(repository, waittime) |
|
68 |
|
69 list = MultipleWriteLock([repository1, repository2, ...], waittime) |
|
70 |
|
71 """ |
|
72 |
|
73 |
|
74 import os |
|
75 import time |
|
76 import stat |
|
77 import pwd |
|
78 |
|
79 |
|
80 # Default wait time |
|
81 DELAY = 10 |
|
82 |
|
83 |
|
84 # XXX This should be the same on all Unix versions |
|
85 EEXIST = 17 |
|
86 |
|
87 |
|
88 # Files used for locking (must match cvs.h in the CVS sources) |
|
89 CVSLCK = "#cvs.lck" |
|
90 CVSRFL = "#cvs.rfl." |
|
91 CVSWFL = "#cvs.wfl." |
|
92 |
|
93 |
|
94 class 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 |
|
106 class Locked(Error): |
|
107 pass |
|
108 |
|
109 |
|
110 class 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 |
|
173 def 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 |
|
187 class 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 |
|
204 class 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 |
|
231 def 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 |
|
246 def 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 |
|
279 if __name__ == '__main__': |
|
280 test() |