1 # portage: Lock management code
2 # Copyright 2004-2011 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
5 __all__ = ["lockdir", "unlockdir", "lockfile", "unlockfile", \
6 "hardlock_name", "hardlink_is_mine", "hardlink_lockfile", \
7 "unhardlink_lockfile", "hardlock_cleanup"]
17 from portage import os, _encodings, _unicode_decode
18 from portage.exception import DirectoryNotFound, FileNotFound, \
19 InvalidData, TryAgain, OperationNotPermitted, PermissionDenied
20 from portage.data import portage_gid
21 from portage.util import writemsg
22 from portage.localization import _
24 if sys.hexversion >= 0x3000000:
28 _HARDLINK_POLL_LATENCY = 3 # seconds
29 _default_lock_fn = fcntl.lockf
31 if platform.python_implementation() == 'PyPy':
32 # workaround for https://bugs.pypy.org/issue747
33 _default_lock_fn = fcntl.flock
35 # Used by emerge in order to disable the "waiting for lock" message
36 # so that it doesn't interfere with the status display.
39 def lockdir(mydir, flags=0):
40 return lockfile(mydir, wantnewlockfile=1, flags=flags)
41 def unlockdir(mylock):
42 return unlockfile(mylock)
44 def lockfile(mypath, wantnewlockfile=0, unlinkfile=0,
45 waiting_msg=None, flags=0):
47 If wantnewlockfile is True then this creates a lockfile in the parent
48 directory as the file: '.' + basename + '.portage_lockfile'.
52 raise InvalidData(_("Empty path given"))
54 # Support for file object or integer file descriptor parameters is
55 # deprecated due to ambiguity in whether or not it's safe to close
56 # the file descriptor, making it prone to "Bad file descriptor" errors
57 # or file descriptor leaks.
58 if isinstance(mypath, basestring) and mypath[-1] == '/':
61 lockfilename_path = mypath
62 if hasattr(mypath, 'fileno'):
63 warnings.warn("portage.locks.lockfile() support for "
64 "file object parameters is deprecated. Use a file path instead.",
65 DeprecationWarning, stacklevel=2)
66 lockfilename_path = getattr(mypath, 'name', None)
67 mypath = mypath.fileno()
68 if isinstance(mypath, int):
69 warnings.warn("portage.locks.lockfile() support for integer file "
70 "descriptor parameters is deprecated. Use a file path instead.",
71 DeprecationWarning, stacklevel=2)
76 base, tail = os.path.split(mypath)
77 lockfilename = os.path.join(base, "." + tail + ".portage_lockfile")
78 lockfilename_path = lockfilename
83 if isinstance(mypath, basestring):
84 if not os.path.exists(os.path.dirname(mypath)):
85 raise DirectoryNotFound(os.path.dirname(mypath))
86 preexisting = os.path.exists(lockfilename)
87 old_mask = os.umask(000)
90 myfd = os.open(lockfilename, os.O_CREAT|os.O_RDWR, 0o660)
92 func_call = "open('%s')" % lockfilename
93 if e.errno == OperationNotPermitted.errno:
94 raise OperationNotPermitted(func_call)
95 elif e.errno == PermissionDenied.errno:
96 raise PermissionDenied(func_call)
102 if os.stat(lockfilename).st_gid != portage_gid:
103 os.chown(lockfilename, -1, portage_gid)
105 if e.errno in (errno.ENOENT, errno.ESTALE):
106 return lockfile(mypath,
107 wantnewlockfile=wantnewlockfile,
108 unlinkfile=unlinkfile, waiting_msg=waiting_msg,
111 writemsg("%s: chown('%s', -1, %d)\n" % \
112 (e, lockfilename, portage_gid), noiselevel=-1)
113 writemsg(_("Cannot chown a lockfile: '%s'\n") % \
114 lockfilename, noiselevel=-1)
115 writemsg(_("Group IDs of current user: %s\n") % \
116 " ".join(str(n) for n in os.getgroups()),
121 elif isinstance(mypath, int):
125 raise ValueError(_("Unknown type passed in '%s': '%s'") % \
126 (type(mypath), mypath))
128 # try for a non-blocking lock, if it's held, throw a message
129 # we're waiting on lockfile and use a blocking attempt.
130 locking_method = _default_lock_fn
132 if "__PORTAGE_TEST_HARDLINK_LOCKS" in os.environ:
133 raise IOError(errno.ENOSYS, "Function not implemented")
134 locking_method(myfd, fcntl.LOCK_EX|fcntl.LOCK_NB)
136 if not hasattr(e, "errno"):
138 if e.errno in (errno.EACCES, errno.EAGAIN):
139 # resource temp unavailable; eg, someone beat us to the lock.
140 if flags & os.O_NONBLOCK:
142 raise TryAgain(mypath)
148 out = portage.output.EOutput()
149 if waiting_msg is None:
150 if isinstance(mypath, int):
151 waiting_msg = _("waiting for lock on fd %i") % myfd
153 waiting_msg = _("waiting for lock on %s\n") % lockfilename
155 out.ebegin(waiting_msg)
156 # try for the exclusive lock now.
158 locking_method(myfd, fcntl.LOCK_EX)
159 except EnvironmentError as e:
165 elif e.errno in (errno.ENOSYS, errno.ENOLCK):
166 # We're not allowed to lock on this FS.
167 if not isinstance(lockfilename, int):
168 # If a file object was passed in, it's not safe
169 # to close the file descriptor because it may
172 lockfilename_path = _unicode_decode(lockfilename_path,
173 encoding=_encodings['fs'], errors='strict')
174 if not isinstance(lockfilename_path, basestring):
176 link_success = hardlink_lockfile(lockfilename_path,
177 waiting_msg=waiting_msg, flags=flags)
180 lockfilename = lockfilename_path
181 locking_method = None
187 if isinstance(lockfilename, basestring) and \
188 myfd != HARDLINK_FD and _fstat_nlink(myfd) == 0:
189 # The file was deleted on us... Keep trying to make one...
191 writemsg(_("lockfile recurse\n"), 1)
192 lockfilename, myfd, unlinkfile, locking_method = lockfile(
193 mypath, wantnewlockfile=wantnewlockfile, unlinkfile=unlinkfile,
194 waiting_msg=waiting_msg, flags=flags)
196 writemsg(str((lockfilename,myfd,unlinkfile))+"\n",1)
197 return (lockfilename,myfd,unlinkfile,locking_method)
199 def _fstat_nlink(fd):
201 @param fd: an open file descriptor
204 @return: the current number of hardlinks to the file
207 return os.fstat(fd).st_nlink
208 except EnvironmentError as e:
209 if e.errno in (errno.ENOENT, errno.ESTALE):
210 # Some filesystems such as CIFS return
211 # ENOENT which means st_nlink == 0.
215 def unlockfile(mytuple):
217 #XXX: Compatability hack.
218 if len(mytuple) == 3:
219 lockfilename,myfd,unlinkfile = mytuple
220 locking_method = fcntl.flock
221 elif len(mytuple) == 4:
222 lockfilename,myfd,unlinkfile,locking_method = mytuple
226 if(myfd == HARDLINK_FD):
227 unhardlink_lockfile(lockfilename, unlinkfile=unlinkfile)
230 # myfd may be None here due to myfd = mypath in lockfile()
231 if isinstance(lockfilename, basestring) and \
232 not os.path.exists(lockfilename):
233 writemsg(_("lockfile does not exist '%s'\n") % lockfilename,1)
240 myfd = os.open(lockfilename, os.O_WRONLY,0o660)
242 locking_method(myfd,fcntl.LOCK_UN)
244 if isinstance(lockfilename, basestring):
246 raise IOError(_("Failed to unlock file '%s'\n") % lockfilename)
249 # This sleep call was added to allow other processes that are
250 # waiting for a lock to be able to grab it before it is deleted.
251 # lockfile() already accounts for this situation, however, and
252 # the sleep here adds more time than is saved overall, so am
253 # commenting until it is proved necessary.
256 locking_method(myfd,fcntl.LOCK_EX|fcntl.LOCK_NB)
257 # We won the lock, so there isn't competition for it.
258 # We can safely delete the file.
259 writemsg(_("Got the lockfile...\n"), 1)
260 if _fstat_nlink(myfd) == 1:
261 os.unlink(lockfilename)
262 writemsg(_("Unlinked lockfile...\n"), 1)
263 locking_method(myfd,fcntl.LOCK_UN)
265 writemsg(_("lockfile does not exist '%s'\n") % lockfilename, 1)
270 except Exception as e:
271 writemsg(_("Failed to get lock... someone took it.\n"), 1)
272 writemsg(str(e)+"\n",1)
274 # why test lockfilename? because we may have been handed an
275 # fd originally, and the caller might not like having their
276 # open fd closed automatically on them.
277 if isinstance(lockfilename, basestring):
285 def hardlock_name(path):
286 base, tail = os.path.split(path)
287 return os.path.join(base, ".%s.hardlock-%s-%s" %
288 (tail, os.uname()[1], os.getpid()))
290 def hardlink_is_mine(link,lock):
292 lock_st = os.stat(lock)
293 if lock_st.st_nlink == 2:
294 link_st = os.stat(link)
295 return lock_st.st_ino == link_st.st_ino and \
296 lock_st.st_dev == link_st.st_dev
301 def hardlink_lockfile(lockfilename, max_wait=DeprecationWarning,
302 waiting_msg=None, flags=0):
303 """Does the NFS, hardlink shuffle to ensure locking on the disk.
304 We create a PRIVATE hardlink to the real lockfile, that is just a
305 placeholder on the disk.
306 If our file can 2 references, then we have the lock. :)
307 Otherwise we lather, rise, and repeat.
310 if max_wait is not DeprecationWarning:
311 warnings.warn("The 'max_wait' parameter of "
312 "portage.locks.hardlink_lockfile() is now unused. Use "
313 "flags=os.O_NONBLOCK instead.",
314 DeprecationWarning, stacklevel=2)
318 displayed_waiting_msg = False
319 preexisting = os.path.exists(lockfilename)
320 myhardlock = hardlock_name(lockfilename)
322 # myhardlock must not exist prior to our link() call, and we can
323 # safely unlink it since its file name is unique to our PID
325 os.unlink(myhardlock)
327 if e.errno in (errno.ENOENT, errno.ESTALE):
330 func_call = "unlink('%s')" % myhardlock
331 if e.errno == OperationNotPermitted.errno:
332 raise OperationNotPermitted(func_call)
333 elif e.errno == PermissionDenied.errno:
334 raise PermissionDenied(func_call)
339 # create lockfilename if it doesn't exist yet
341 myfd = os.open(lockfilename, os.O_CREAT|os.O_RDWR, 0o660)
343 func_call = "open('%s')" % lockfilename
344 if e.errno == OperationNotPermitted.errno:
345 raise OperationNotPermitted(func_call)
346 elif e.errno == PermissionDenied.errno:
347 raise PermissionDenied(func_call)
353 myfd_st = os.fstat(myfd)
355 # Don't chown the file if it is preexisting, since we
356 # want to preserve existing permissions in that case.
357 if myfd_st.st_gid != portage_gid:
358 os.fchown(myfd, -1, portage_gid)
360 if e.errno not in (errno.ENOENT, errno.ESTALE):
361 writemsg("%s: fchown('%s', -1, %d)\n" % \
362 (e, lockfilename, portage_gid), noiselevel=-1)
363 writemsg(_("Cannot chown a lockfile: '%s'\n") % \
364 lockfilename, noiselevel=-1)
365 writemsg(_("Group IDs of current user: %s\n") % \
366 " ".join(str(n) for n in os.getgroups()),
369 # another process has removed the file, so we'll have
375 # If fstat shows more than one hardlink, then it's extremely
376 # unlikely that the following link call will result in a lock,
377 # so optimize away the wasteful link call and sleep or raise
379 if myfd_st is not None and myfd_st.st_nlink < 2:
381 os.link(lockfilename, myhardlock)
383 func_call = "link('%s', '%s')" % (lockfilename, myhardlock)
384 if e.errno == OperationNotPermitted.errno:
385 raise OperationNotPermitted(func_call)
386 elif e.errno == PermissionDenied.errno:
387 raise PermissionDenied(func_call)
388 elif e.errno in (errno.ESTALE, errno.ENOENT):
389 # another process has removed the file, so we'll have
395 if hardlink_is_mine(myhardlock, lockfilename):
401 os.unlink(myhardlock)
403 # This should not happen, since the file name of
404 # myhardlock is unique to our host and PID,
405 # and the above link() call succeeded.
406 if e.errno not in (errno.ENOENT, errno.ESTALE):
408 raise FileNotFound(myhardlock)
410 if flags & os.O_NONBLOCK:
411 raise TryAgain(lockfilename)
413 if out is None and not _quiet:
414 out = portage.output.EOutput()
415 if out is not None and not displayed_waiting_msg:
416 displayed_waiting_msg = True
417 if waiting_msg is None:
418 waiting_msg = _("waiting for lock on %s\n") % lockfilename
419 out.ebegin(waiting_msg)
421 time.sleep(_HARDLINK_POLL_LATENCY)
425 def unhardlink_lockfile(lockfilename, unlinkfile=True):
426 myhardlock = hardlock_name(lockfilename)
427 if unlinkfile and hardlink_is_mine(myhardlock, lockfilename):
428 # Make sure not to touch lockfilename unless we really have a lock.
430 os.unlink(lockfilename)
434 os.unlink(myhardlock)
438 def hardlock_cleanup(path, remove_all_locks=False):
439 mypid = str(os.getpid())
440 myhost = os.uname()[1]
441 mydl = os.listdir(path)
448 if os.path.isfile(path+"/"+x):
449 parts = x.split(".hardlock-")
451 filename = parts[0][1:]
452 hostpid = parts[1].split("-")
453 host = "-".join(hostpid[:-1])
456 if filename not in mylist:
457 mylist[filename] = {}
458 if host not in mylist[filename]:
459 mylist[filename][host] = []
460 mylist[filename][host].append(pid)
465 results.append(_("Found %(count)s locks") % {"count":mycount})
468 if myhost in mylist[x] or remove_all_locks:
469 mylockname = hardlock_name(path+"/"+x)
470 if hardlink_is_mine(mylockname, path+"/"+x) or \
471 not os.path.exists(path+"/"+x) or \
474 for z in mylist[x][y]:
475 filename = path+"/."+x+".hardlock-"+y+"-"+z
476 if filename == mylockname:
479 # We're sweeping through, unlinking everyone's locks.
481 results.append(_("Unlinked: ") + filename)
485 os.unlink(path+"/"+x)
486 results.append(_("Unlinked: ") + path+"/"+x)
487 os.unlink(mylockname)
488 results.append(_("Unlinked: ") + mylockname)
493 os.unlink(mylockname)
494 results.append(_("Unlinked: ") + mylockname)