Use normpath from support
[catalyst.git] / catalyst / lock.py
1 #!/usr/bin/python
2
3
4 import os
5 import fcntl
6 import errno
7 import sys
8 import time
9
10
11 from catalyst.support import CatalystError, normpath
12
13
14 def writemsg(mystr):
15         sys.stderr.write(mystr)
16         sys.stderr.flush()
17
18
19 class LockInUse(Exception):
20         def __init__(self, message):
21                 if message:
22                         #(type,value)=sys.exc_info()[:2]
23                         #if value!=None:
24                             #print
25                             #kprint traceback.print_exc(file=sys.stdout)
26                         print
27                         print "!!! catalyst lock file in use: "+message
28                         print
29
30
31 class LockDir:
32         locking_method=fcntl.flock
33         lock_dirs_in_use=[]
34         die_on_failed_lock=True
35         def __del__(self):
36                 self.clean_my_hardlocks()
37                 self.delete_lock_from_path_list()
38                 if self.islocked():
39                         self.fcntl_unlock()
40
41         def __init__(self,lockdir):
42                 self.locked=False
43                 self.myfd=None
44                 self.set_gid(250)
45                 self.locking_method=LockDir.locking_method
46                 self.set_lockdir(lockdir)
47                 self.set_lockfilename(".catalyst_lock")
48                 self.set_lockfile()
49
50                 if LockDir.lock_dirs_in_use.count(lockdir)>0:
51                         raise "This directory already associated with a lock object"
52                 else:
53                         LockDir.lock_dirs_in_use.append(lockdir)
54
55                 self.hardlock_paths={}
56
57         def delete_lock_from_path_list(self):
58                 i=0
59                 try:
60                         if LockDir.lock_dirs_in_use:
61                                 for x in LockDir.lock_dirs_in_use:
62                                         if LockDir.lock_dirs_in_use[i] == self.lockdir:
63                                                 del LockDir.lock_dirs_in_use[i]
64                                                 break
65                                                 i=i+1
66                 except AttributeError:
67                         pass
68
69         def islocked(self):
70                 if self.locked:
71                         return True
72                 else:
73                         return False
74
75         def set_gid(self,gid):
76                 if not self.islocked():
77 #                       if "DEBUG" in self.settings:
78 #                               print "setting gid to", gid
79                         self.gid=gid
80
81         def set_lockdir(self,lockdir):
82                 if not os.path.exists(lockdir):
83                         os.makedirs(lockdir)
84                 if os.path.isdir(lockdir):
85                         if not self.islocked():
86                                 if lockdir[-1] == "/":
87                                         lockdir=lockdir[:-1]
88                                 self.lockdir = normpath(lockdir)
89 #                               if "DEBUG" in self.settings:
90 #                                       print "setting lockdir to", self.lockdir
91                 else:
92                         raise "the lock object needs a path to a dir"
93
94         def set_lockfilename(self,lockfilename):
95                 if not self.islocked():
96                         self.lockfilename=lockfilename
97 #                       if "DEBUG" in self.settings:
98 #                               print "setting lockfilename to", self.lockfilename
99
100         def set_lockfile(self):
101                 if not self.islocked():
102                         self.lockfile = normpath(self.lockdir+'/'+self.lockfilename)
103 #                       if "DEBUG" in self.settings:
104 #                               print "setting lockfile to", self.lockfile
105
106         def read_lock(self):
107                 if not self.locking_method == "HARDLOCK":
108                         self.fcntl_lock("read")
109                 else:
110                         print "HARDLOCKING doesnt support shared-read locks"
111                         print "using exclusive write locks"
112                         self.hard_lock()
113
114         def write_lock(self):
115                 if not self.locking_method == "HARDLOCK":
116                         self.fcntl_lock("write")
117                 else:
118                         self.hard_lock()
119
120         def unlock(self):
121                 if not self.locking_method == "HARDLOCK":
122                         self.fcntl_unlock()
123                 else:
124                         self.hard_unlock()
125
126         def fcntl_lock(self,locktype):
127                 if self.myfd==None:
128                         if not os.path.exists(os.path.dirname(self.lockdir)):
129                                 raise CatalystError("DirectoryNotFound: %s"
130                                         % os.path.dirname(self.lockdir), print_traceback=True)
131                         if not os.path.exists(self.lockfile):
132                                 old_mask=os.umask(000)
133                                 self.myfd = os.open(self.lockfile, os.O_CREAT|os.O_RDWR,0660)
134                                 try:
135                                         if os.stat(self.lockfile).st_gid != self.gid:
136                                                 os.chown(self.lockfile,os.getuid(),self.gid)
137                                 except SystemExit, e:
138                                         raise
139                                 except OSError, e:
140                                         if e[0] == 2: #XXX: No such file or directory
141                                                 return self.fcntl_locking(locktype)
142                                         else:
143                                                 writemsg("Cannot chown a lockfile. This could cause inconvenience later.\n")
144
145                                 os.umask(old_mask)
146                         else:
147                                 self.myfd = os.open(self.lockfile, os.O_CREAT|os.O_RDWR,0660)
148
149                 try:
150                         if locktype == "read":
151                                 self.locking_method(self.myfd,fcntl.LOCK_SH|fcntl.LOCK_NB)
152                         else:
153                                 self.locking_method(self.myfd,fcntl.LOCK_EX|fcntl.LOCK_NB)
154                 except IOError, e:
155                         if "errno" not in dir(e):
156                                 raise
157                         if e.errno == errno.EAGAIN:
158                                 if not LockDir.die_on_failed_lock:
159                                         # Resource temp unavailable; eg, someone beat us to the lock.
160                                         writemsg("waiting for lock on %s\n" % self.lockfile)
161
162                                         # Try for the exclusive or shared lock again.
163                                         if locktype == "read":
164                                                 self.locking_method(self.myfd,fcntl.LOCK_SH)
165                                         else:
166                                                 self.locking_method(self.myfd,fcntl.LOCK_EX)
167                                 else:
168                                         raise LockInUse,self.lockfile
169                         elif e.errno == errno.ENOLCK:
170                                 pass
171                         else:
172                                 raise
173                 if not os.path.exists(self.lockfile):
174                         os.close(self.myfd)
175                         self.myfd=None
176                         #writemsg("lockfile recurse\n")
177                         self.fcntl_lock(locktype)
178                 else:
179                         self.locked=True
180                         #writemsg("Lockfile obtained\n")
181
182         def fcntl_unlock(self):
183                 import fcntl
184                 unlinkfile = 1
185                 if not os.path.exists(self.lockfile):
186                         print "lockfile does not exist '%s'" % self.lockfile
187                         if (self.myfd != None):
188                                 try:
189                                         os.close(self.myfd)
190                                         self.myfd=None
191                                 except:
192                                         pass
193                                 return False
194
195                         try:
196                                 if self.myfd == None:
197                                         self.myfd = os.open(self.lockfile, os.O_WRONLY,0660)
198                                         unlinkfile = 1
199                                         self.locking_method(self.myfd,fcntl.LOCK_UN)
200                         except SystemExit, e:
201                                 raise
202                         except Exception, e:
203                                 os.close(self.myfd)
204                                 self.myfd=None
205                                 raise IOError, "Failed to unlock file '%s'\n" % self.lockfile
206                                 try:
207                                         # This sleep call was added to allow other processes that are
208                                         # waiting for a lock to be able to grab it before it is deleted.
209                                         # lockfile() already accounts for this situation, however, and
210                                         # the sleep here adds more time than is saved overall, so am
211                                         # commenting until it is proved necessary.
212                                         #time.sleep(0.0001)
213                                         if unlinkfile:
214                                                 InUse=False
215                                                 try:
216                                                         self.locking_method(self.myfd,fcntl.LOCK_EX|fcntl.LOCK_NB)
217                                                 except:
218                                                         print "Read lock may be in effect. skipping lockfile delete..."
219                                                         InUse=True
220                                                         # We won the lock, so there isn't competition for it.
221                                                         # We can safely delete the file.
222                                                         #writemsg("Got the lockfile...\n")
223                                                         #writemsg("Unlinking...\n")
224                                                         self.locking_method(self.myfd,fcntl.LOCK_UN)
225                                         if not InUse:
226                                                 os.unlink(self.lockfile)
227                                                 os.close(self.myfd)
228                                                 self.myfd=None
229 #                                               if "DEBUG" in self.settings:
230 #                                                       print "Unlinked lockfile..."
231                                 except SystemExit, e:
232                                         raise
233                                 except Exception, e:
234                                         # We really don't care... Someone else has the lock.
235                                         # So it is their problem now.
236                                         print "Failed to get lock... someone took it."
237                                         print str(e)
238
239                                         # Why test lockfilename?  Because we may have been handed an
240                                         # fd originally, and the caller might not like having their
241                                         # open fd closed automatically on them.
242                                         #if type(lockfilename) == types.StringType:
243                                         #        os.close(myfd)
244
245                 if (self.myfd != None):
246                         os.close(self.myfd)
247                         self.myfd=None
248                         self.locked=False
249                         time.sleep(.0001)
250
251         def hard_lock(self,max_wait=14400):
252                 """Does the NFS, hardlink shuffle to ensure locking on the disk.
253                 We create a PRIVATE lockfile, that is just a placeholder on the disk.
254                 Then we HARDLINK the real lockfile to that private file.
255                 If our file can 2 references, then we have the lock. :)
256                 Otherwise we lather, rise, and repeat.
257                 We default to a 4 hour timeout.
258                 """
259
260                 self.myhardlock = self.hardlock_name(self.lockdir)
261
262                 start_time = time.time()
263                 reported_waiting = False
264
265                 while(time.time() < (start_time + max_wait)):
266                         # We only need it to exist.
267                         self.myfd = os.open(self.myhardlock, os.O_CREAT|os.O_RDWR,0660)
268                         os.close(self.myfd)
269
270                         self.add_hardlock_file_to_cleanup()
271                         if not os.path.exists(self.myhardlock):
272                                 raise CatalystError("FileNotFound: Created lockfile is missing: "
273                                         "%(filename)s" % {"filename":self.myhardlock},
274                                         print_traceback=True)
275                         try:
276                                 os.link(self.myhardlock, self.lockfile)
277                         except SystemExit:
278                                 raise
279                         except Exception:
280 #                               if "DEBUG" in self.settings:
281 #                                       print "lockfile(): Hardlink: Link failed."
282 #                                       print "Exception: ",e
283                                 pass
284
285                         if self.hardlink_is_mine(self.myhardlock, self.lockfile):
286                                 # We have the lock.
287                                 if reported_waiting:
288                                         print
289                                 return True
290
291                         if reported_waiting:
292                                 writemsg(".")
293                         else:
294                                 reported_waiting = True
295                                 print
296                                 print "Waiting on (hardlink) lockfile: (one '.' per 3 seconds)"
297                                 print "Lockfile: " + self.lockfile
298                         time.sleep(3)
299
300                 os.unlink(self.myhardlock)
301                 return False
302
303         def hard_unlock(self):
304                 try:
305                         if os.path.exists(self.myhardlock):
306                                 os.unlink(self.myhardlock)
307                         if os.path.exists(self.lockfile):
308                                 os.unlink(self.lockfile)
309                 except SystemExit:
310                         raise
311                 except:
312                         writemsg("Something strange happened to our hardlink locks.\n")
313
314         def add_hardlock_file_to_cleanup(self):
315                 #mypath = self.normpath(path)
316                 if os.path.isdir(self.lockdir) and os.path.isfile(self.myhardlock):
317                         self.hardlock_paths[self.lockdir]=self.myhardlock
318
319         def remove_hardlock_file_from_cleanup(self):
320                 if self.lockdir in self.hardlock_paths:
321                         del self.hardlock_paths[self.lockdir]
322                         print self.hardlock_paths
323
324         def hardlock_name(self, path):
325                 mypath=path+"/.hardlock-"+os.uname()[1]+"-"+str(os.getpid())
326                 newpath = os.path.normpath(mypath)
327                 if len(newpath) > 1:
328                         if newpath[1] == "/":
329                                 newpath = "/"+newpath.lstrip("/")
330                 return newpath
331
332         def hardlink_is_mine(self,link,lock):
333                 import stat
334                 try:
335                         myhls = os.stat(link)
336                         mylfs = os.stat(lock)
337                 except SystemExit:
338                         raise
339                 except:
340                         myhls = None
341                         mylfs = None
342
343                 if myhls:
344                         if myhls[stat.ST_NLINK] == 2:
345                                 return True
346                 if mylfs:
347                         if mylfs[stat.ST_INO] == myhls[stat.ST_INO]:
348                                 return True
349                 return False
350
351         def hardlink_active(lock):
352                 if not os.path.exists(lock):
353                         return False
354
355         def clean_my_hardlocks(self):
356                 try:
357                         for x in self.hardlock_paths.keys():
358                                 self.hardlock_cleanup(x)
359                 except AttributeError:
360                         pass
361
362         def hardlock_cleanup(self,path):
363                 #mypid  = str(os.getpid())
364                 myhost = os.uname()[1]
365                 mydl = os.listdir(path)
366                 results = []
367                 mycount = 0
368
369                 mylist = {}
370                 for x in mydl:
371                         filepath=path+"/"+x
372                         if os.path.isfile(filepath):
373                                 parts = filepath.split(".hardlock-")
374                         if len(parts) == 2:
375                                 filename = parts[0]
376                                 hostpid  = parts[1].split("-")
377                                 host  = "-".join(hostpid[:-1])
378                                 pid   = hostpid[-1]
379                         if filename not in mylist:
380                                 mylist[filename] = {}
381
382                         if host not in mylist[filename]:
383                                 mylist[filename][host] = []
384                                 mylist[filename][host].append(pid)
385                                 mycount += 1
386                         else:
387                                 mylist[filename][host].append(pid)
388                                 mycount += 1
389
390
391                 results.append("Found %(count)s locks" % {"count":mycount})
392                 for x in mylist.keys():
393                         if myhost in mylist[x]:
394                                 mylockname = self.hardlock_name(x)
395                                 if self.hardlink_is_mine(mylockname, self.lockfile) or \
396                                         not os.path.exists(self.lockfile):
397                                         for y in mylist[x].keys():
398                                                 for z in mylist[x][y]:
399                                                         filename = x+".hardlock-"+y+"-"+z
400                                                         if filename == mylockname:
401                                                                 self.hard_unlock()
402                                                                 continue
403                                                         try:
404                                                                 # We're sweeping through, unlinking everyone's locks.
405                                                                 os.unlink(filename)
406                                                                 results.append("Unlinked: " + filename)
407                                                         except SystemExit:
408                                                                 raise
409                                                         except Exception:
410                                                                 pass
411                                         try:
412                                                 os.unlink(x)
413                                                 results.append("Unlinked: " + x)
414                                                 os.unlink(mylockname)
415                                                 results.append("Unlinked: " + mylockname)
416                                         except SystemExit:
417                                                 raise
418                                         except Exception:
419                                                 pass
420                                 else:
421                                         try:
422                                                 os.unlink(mylockname)
423                                                 results.append("Unlinked: " + mylockname)
424                                         except SystemExit:
425                                                 raise
426                                         except Exception:
427                                                 pass
428                 return results
429
430 if __name__ == "__main__":
431
432         def lock_work():
433                 print
434                 for i in range(1,6):
435                         print i,time.time()
436                         time.sleep(1)
437                 print
438         #def normpath(mypath):
439                 #newpath = os.path.normpath(mypath)
440                 #if len(newpath) > 1:
441                         #if newpath[1] == "/":
442                                 #newpath = "/"+newpath.lstrip("/")
443                 #return newpath
444
445         print "Lock 5 starting"
446         Lock1=LockDir("/tmp/lock_path")
447         Lock1.write_lock()
448         print "Lock1 write lock"
449
450         lock_work()
451
452         Lock1.unlock()
453         print "Lock1 unlock"
454
455         Lock1.read_lock()
456         print "Lock1 read lock"
457
458         lock_work()
459
460         Lock1.unlock()
461         print "Lock1 unlock"
462
463         Lock1.read_lock()
464         print "Lock1 read lock"
465
466         Lock1.write_lock()
467         print "Lock1 write lock"
468
469         lock_work()
470
471         Lock1.unlock()
472         print "Lock1 unlock"
473
474         Lock1.read_lock()
475         print "Lock1 read lock"
476
477         lock_work()
478
479         Lock1.unlock()
480         print "Lock1 unlock"
481