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