Use ebegin/eend to indicate when waiting for a lock and when it's acquired.
[portage.git] / pym / portage / locks.py
1 # portage: Lock management code
2 # Copyright 2004 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4 # $Id$
5
6 __all__ = ["lockdir", "unlockdir", "lockfile", "unlockfile", \
7         "hardlock_name", "hardlink_is_mine", "hardlink_lockfile", \
8         "unhardlink_lockfile", "hardlock_cleanup"]
9
10 import errno, os, stat, time, types
11 from portage.exception import DirectoryNotFound, FileNotFound, \
12         InvalidData, TryAgain
13 from portage.data import portage_gid
14 from portage.output import EOutput
15 from portage.util import writemsg
16 from portage.localization import _
17
18 HARDLINK_FD = -2
19
20 # Used by emerge in order to disable the "waiting for lock" message
21 # so that it doesn't interfere with the status display.
22 _quiet = False
23
24 def lockdir(mydir):
25         return lockfile(mydir,wantnewlockfile=1)
26 def unlockdir(mylock):
27         return unlockfile(mylock)
28
29 def lockfile(mypath, wantnewlockfile=0, unlinkfile=0,
30         waiting_msg=None, flags=0):
31         """Creates all dirs upto, the given dir. Creates a lockfile
32         for the given directory as the file: directoryname+'.portage_lockfile'."""
33         import fcntl
34
35         if not mypath:
36                 raise InvalidData("Empty path given")
37
38         if type(mypath) == types.StringType and mypath[-1] == '/':
39                 mypath = mypath[:-1]
40
41         if type(mypath) == types.FileType:
42                 mypath = mypath.fileno()
43         if type(mypath) == types.IntType:
44                 lockfilename    = mypath
45                 wantnewlockfile = 0
46                 unlinkfile      = 0
47         elif wantnewlockfile:
48                 base, tail = os.path.split(mypath)
49                 lockfilename = os.path.join(base, "." + tail + ".portage_lockfile")
50                 del base, tail
51                 unlinkfile   = 1
52         else:
53                 lockfilename = mypath
54         
55         if type(mypath) == types.StringType:
56                 if not os.path.exists(os.path.dirname(mypath)):
57                         raise DirectoryNotFound(os.path.dirname(mypath))
58                 if not os.path.exists(lockfilename):
59                         old_mask=os.umask(000)
60                         myfd = os.open(lockfilename, os.O_CREAT|os.O_RDWR,0660)
61                         try:
62                                 if os.stat(lockfilename).st_gid != portage_gid:
63                                         os.chown(lockfilename,os.getuid(),portage_gid)
64                         except OSError, e:
65                                 if e[0] == 2: # No such file or directory
66                                         return lockfile(mypath, wantnewlockfile=wantnewlockfile,
67                                                 unlinkfile=unlinkfile, waiting_msg=waiting_msg,
68                                                 flags=flags)
69                                 else:
70                                         writemsg("Cannot chown a lockfile. This could cause inconvenience later.\n");
71                         os.umask(old_mask)
72                 else:
73                         myfd = os.open(lockfilename, os.O_CREAT|os.O_RDWR,0660)
74
75         elif type(mypath) == types.IntType:
76                 myfd = mypath
77
78         else:
79                 raise ValueError("Unknown type passed in '%s': '%s'" % \
80                         (type(mypath), mypath))
81
82         # try for a non-blocking lock, if it's held, throw a message
83         # we're waiting on lockfile and use a blocking attempt.
84         locking_method = fcntl.lockf
85         try:
86                 fcntl.lockf(myfd,fcntl.LOCK_EX|fcntl.LOCK_NB)
87         except IOError, e:
88                 if "errno" not in dir(e):
89                         raise
90                 if e.errno in (errno.EACCES, errno.EAGAIN):
91                         # resource temp unavailable; eg, someone beat us to the lock.
92                         if flags & os.O_NONBLOCK:
93                                 raise TryAgain(mypath)
94
95                         global _quiet
96                         out = EOutput()
97                         out.quiet = _quiet
98                         if waiting_msg is None:
99                                 if isinstance(mypath, int):
100                                         waiting_msg = "waiting for lock on fd %i" % myfd
101                                 else:
102                                         waiting_msg = "waiting for lock on %s\n" % lockfilename
103                         out.ebegin(waiting_msg)
104                         # try for the exclusive lock now.
105                         try:
106                                 fcntl.lockf(myfd, fcntl.LOCK_EX)
107                         except EnvironmentError, e:
108                                 out.eend(1, str(e))
109                                 raise
110                         out.eend(os.EX_OK)
111                 elif e.errno == errno.ENOLCK:
112                         # We're not allowed to lock on this FS.
113                         os.close(myfd)
114                         link_success = False
115                         if lockfilename == str(lockfilename):
116                                 if wantnewlockfile:
117                                         try:
118                                                 if os.stat(lockfilename)[stat.ST_NLINK] == 1:
119                                                         os.unlink(lockfilename)
120                                         except OSError:
121                                                 pass
122                                         link_success = hardlink_lockfile(lockfilename)
123                         if not link_success:
124                                 raise
125                         locking_method = None
126                         myfd = HARDLINK_FD
127                 else:
128                         raise
129
130                 
131         if type(lockfilename) == types.StringType and \
132                 myfd != HARDLINK_FD and _fstat_nlink(myfd) == 0:
133                 # The file was deleted on us... Keep trying to make one...
134                 os.close(myfd)
135                 writemsg("lockfile recurse\n",1)
136                 lockfilename, myfd, unlinkfile, locking_method = lockfile(
137                         mypath, wantnewlockfile=wantnewlockfile, unlinkfile=unlinkfile,
138                         waiting_msg=waiting_msg, flags=flags)
139
140         writemsg(str((lockfilename,myfd,unlinkfile))+"\n",1)
141         return (lockfilename,myfd,unlinkfile,locking_method)
142
143 def _fstat_nlink(fd):
144         """
145         @param fd: an open file descriptor
146         @type fd: Integer
147         @rtype: Integer
148         @return: the current number of hardlinks to the file
149         """
150         try:
151                 return os.fstat(fd).st_nlink
152         except EnvironmentError, e:
153                 if e.errno == errno.ENOENT:
154                         # Some filesystems such as CIFS return
155                         # ENOENT which means st_nlink == 0.
156                         return 0
157                 raise
158
159 def unlockfile(mytuple):
160         import fcntl
161
162         #XXX: Compatability hack.
163         if len(mytuple) == 3:
164                 lockfilename,myfd,unlinkfile = mytuple
165                 locking_method = fcntl.flock
166         elif len(mytuple) == 4:
167                 lockfilename,myfd,unlinkfile,locking_method = mytuple
168         else:
169                 raise InvalidData
170
171         if(myfd == HARDLINK_FD):
172                 unhardlink_lockfile(lockfilename)
173                 return True
174         
175         # myfd may be None here due to myfd = mypath in lockfile()
176         if type(lockfilename) == types.StringType and not os.path.exists(lockfilename):
177                 writemsg("lockfile does not exist '%s'\n" % lockfilename,1)
178                 if myfd is not None:
179                         os.close(myfd)
180                 return False
181
182         try:
183                 if myfd is None:
184                         myfd = os.open(lockfilename, os.O_WRONLY,0660)
185                         unlinkfile = 1
186                 locking_method(myfd,fcntl.LOCK_UN)
187         except OSError:
188                 if type(lockfilename) == types.StringType:
189                         os.close(myfd)
190                 raise IOError("Failed to unlock file '%s'\n" % lockfilename)
191
192         try:
193                 # This sleep call was added to allow other processes that are
194                 # waiting for a lock to be able to grab it before it is deleted.
195                 # lockfile() already accounts for this situation, however, and
196                 # the sleep here adds more time than is saved overall, so am
197                 # commenting until it is proved necessary.
198                 #time.sleep(0.0001)
199                 if unlinkfile:
200                         locking_method(myfd,fcntl.LOCK_EX|fcntl.LOCK_NB)
201                         # We won the lock, so there isn't competition for it.
202                         # We can safely delete the file.
203                         writemsg("Got the lockfile...\n",1)
204                         if _fstat_nlink(myfd) == 1:
205                                 os.unlink(lockfilename)
206                                 writemsg("Unlinked lockfile...\n",1)
207                                 locking_method(myfd,fcntl.LOCK_UN)
208                         else:
209                                 writemsg("lockfile does not exist '%s'\n" % lockfilename,1)
210                                 os.close(myfd)
211                                 return False
212         except Exception, e:
213                 writemsg("Failed to get lock... someone took it.\n",1)
214                 writemsg(str(e)+"\n",1)
215
216         # why test lockfilename?  because we may have been handed an
217         # fd originally, and the caller might not like having their
218         # open fd closed automatically on them.
219         if type(lockfilename) == types.StringType:
220                 os.close(myfd)
221
222         return True
223
224
225
226
227 def hardlock_name(path):
228         return path+".hardlock-"+os.uname()[1]+"-"+str(os.getpid())
229
230 def hardlink_is_mine(link,lock):
231         try:
232                 return os.stat(link).st_nlink == 2
233         except OSError:
234                 return False
235
236 def hardlink_lockfile(lockfilename, max_wait=14400):
237         """Does the NFS, hardlink shuffle to ensure locking on the disk.
238         We create a PRIVATE lockfile, that is just a placeholder on the disk.
239         Then we HARDLINK the real lockfile to that private file.
240         If our file can 2 references, then we have the lock. :)
241         Otherwise we lather, rise, and repeat.
242         We default to a 4 hour timeout.
243         """
244
245         start_time = time.time()
246         myhardlock = hardlock_name(lockfilename)
247         reported_waiting = False
248         
249         while(time.time() < (start_time + max_wait)):
250                 # We only need it to exist.
251                 myfd = os.open(myhardlock, os.O_CREAT|os.O_RDWR,0660)
252                 os.close(myfd)
253         
254                 if not os.path.exists(myhardlock):
255                         raise FileNotFound(
256                                 _("Created lockfile is missing: %(filename)s") % \
257                                 {"filename" : myhardlock})
258
259                 try:
260                         res = os.link(myhardlock, lockfilename)
261                 except OSError:
262                         pass
263
264                 if hardlink_is_mine(myhardlock, lockfilename):
265                         # We have the lock.
266                         if reported_waiting:
267                                 writemsg("\n", noiselevel=-1)
268                         return True
269
270                 if reported_waiting:
271                         writemsg(".", noiselevel=-1)
272                 else:
273                         reported_waiting = True
274                         from portage.const import PORTAGE_BIN_PATH
275                         msg = "\nWaiting on (hardlink) lockfile:" + \
276                                 " (one '.' per 3 seconds)\n" + \
277                                 "%s/clean_locks can fix stuck locks.\n" % PORTAGE_BIN_PATH + \
278                                 "Lockfile: %s\n" % lockfilename
279                         writemsg(msg, noiselevel=-1)
280                 time.sleep(3)
281         
282         os.unlink(myhardlock)
283         return False
284
285 def unhardlink_lockfile(lockfilename):
286         myhardlock = hardlock_name(lockfilename)
287         if hardlink_is_mine(myhardlock, lockfilename):
288                 # Make sure not to touch lockfilename unless we really have a lock.
289                 try:
290                         os.unlink(lockfilename)
291                 except OSError:
292                         pass
293         try:
294                 os.unlink(myhardlock)
295         except OSError:
296                 pass
297
298 def hardlock_cleanup(path, remove_all_locks=False):
299         mypid  = str(os.getpid())
300         myhost = os.uname()[1]
301         mydl = os.listdir(path)
302
303         results = []
304         mycount = 0
305
306         mylist = {}
307         for x in mydl:
308                 if os.path.isfile(path+"/"+x):
309                         parts = x.split(".hardlock-")
310                         if len(parts) == 2:
311                                 filename = parts[0]
312                                 hostpid  = parts[1].split("-")
313                                 host  = "-".join(hostpid[:-1])
314                                 pid   = hostpid[-1]
315                                 
316                                 if filename not in mylist:
317                                         mylist[filename] = {}
318                                 if host not in mylist[filename]:
319                                         mylist[filename][host] = []
320                                 mylist[filename][host].append(pid)
321
322                                 mycount += 1
323
324
325         results.append("Found %(count)s locks" % {"count":mycount})
326         
327         for x in mylist:
328                 if myhost in mylist[x] or remove_all_locks:
329                         mylockname = hardlock_name(path+"/"+x)
330                         if hardlink_is_mine(mylockname, path+"/"+x) or \
331                            not os.path.exists(path+"/"+x) or \
332                                  remove_all_locks:
333                                 for y in mylist[x]:
334                                         for z in mylist[x][y]:
335                                                 filename = path+"/"+x+".hardlock-"+y+"-"+z
336                                                 if filename == mylockname:
337                                                         continue
338                                                 try:
339                                                         # We're sweeping through, unlinking everyone's locks.
340                                                         os.unlink(filename)
341                                                         results.append(_("Unlinked: ") + filename)
342                                                 except OSError:
343                                                         pass
344                                 try:
345                                         os.unlink(path+"/"+x)
346                                         results.append(_("Unlinked: ") + path+"/"+x)
347                                         os.unlink(mylockname)
348                                         results.append(_("Unlinked: ") + mylockname)
349                                 except OSError:
350                                         pass
351                         else:
352                                 try:
353                                         os.unlink(mylockname)
354                                         results.append(_("Unlinked: ") + mylockname)
355                                 except OSError:
356                                         pass
357
358         return results
359