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