blob: 8b97cc23c126bad11b08e0289e8e2e99bf229c26 [file] [log] [blame]
# Copyright 2017 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Lock files.
Please read:
fcntl(2)
flock(2)
lockf(3)
http://pubs.opengroup.org/onlinepubs/9699919799/functions/lockf.html
http://pubs.opengroup.org/onlinepubs/9699919799/functions/fcntl.html
https://github.com/python/cpython/blob/2.7/Modules/fcntlmodule.c
Informal references:
http://0pointer.de/blog/projects/locking.html
https://www.samba.org/samba/news/articles/low_point/tale_two_stds_os2.html
http://chris.improbable.org/2010/12/16/everything-you-never-wanted-to-know-about-file-locking/
"""
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import errno
import fcntl
import logging
import os
import random
import time
logger = logging.getLogger(__name__)
class FileLock(object):
"""Context manager for an exclusive file lock.
This uses fcntl() locks for portability. Refer to the module
docstring and the links therein for details that informed this
choice.
fcntl() locks have a number of important quirks:
This may or may not work if the lock is on a remote filesystem.
Locks are per-process and therefore not thread-safe.
The lock is bound to the file inode:
>>> tmp = getfixture('tmpdir')
>>> f1 = str(tmp.join('lock'))
>>> f2 = str(tmp.join('lock2'))
>>> with FileLock(f1):
... os.link(f1, f2)
... f = open(f2) # Same inode
... f.close() # This will release the lock
... # Stuff here will run without the lock
"""
def __init__(self, lockfile, timeout=180):
"""Initialize instance.
Args:
lockfile: Path to lockfile
timeout: Timeout for grabbing lock, in seconds
"""
self._lockfile = lockfile
self._timeout = timeout
self._fd = None
def __enter__(self):
timeout = time.time() + self._timeout
# Wait for possible symlink locks and open lock file.
while True:
if not self._symlink_lock_present():
try:
self._fd = os.open(self._lockfile,
os.O_RDWR | os.O_CREAT,
0666)
break
except OSError as e: # pragma: no cover
logger.debug('Error opening lock file: %s', e)
if time.time() > timeout:
raise TimeoutError(self._lockfile, self._timeout)
time.sleep(random.random())
# Grab lock on file.
while True:
try:
fcntl.lockf(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError as e:
logger.debug('Error acquiring lock: %s', e)
else:
return
if time.time() > timeout:
raise TimeoutError(self._lockfile, self._timeout)
time.sleep(random.random())
def __exit__(self, exc_type, exc_value, traceback):
os.close(self._fd)
def _symlink_lock_present(self):
"""Check if an old style symlink lock is in place."""
# TODO(ayatane): 2017-08-21 Old symlink lock file handling can
# be removed after the code has existed for Long Enough that
# there aren't reasonably any instances of it left on developer
# or prod machines.
try:
target = os.readlink(self._lockfile)
except OSError as e:
if e.errno == errno.EINVAL:
# Not a symlink.
return False
elif e.errno == errno.ENOENT:
# Lock is gone.
return False
else: # pragma: no cover
raise
pid = _procdir.getpid(target)
if _is_pid_running(pid):
return True
try:
os.unlink(self._lockfile)
except OSError as e: # pragma: no cover
if e.errno != errno.ENOENT:
raise
return False
class TimeoutError(Exception):
"""Grabbing lock timed out."""
def __init__(self, filename, timeout):
super(TimeoutError, self).__init__(filename, timeout)
self.filename = filename
self.timeout = timeout
def __str__(self):
return ('Grabbing lock %(filename)s'
' timed out after %(timeout)s seconds'
% {
'filename': self.filename,
'timeout': self.timeout,
})
class _ProcDir(object):
"""Class wrapping /proc related functions."""
def __init__(self, procdir):
self._procdir = procdir
def getpath(self, pid):
"""Return /proc dir path for pid."""
return os.path.join(self._procdir, str(pid))
def getpid(self, path):
"""Return int pid for /proc path."""
proc_prefix_len = len(self._procdir) + 1 # Handle extra ending slash
pid_string = path[proc_prefix_len:]
return int(pid_string)
_procdir = _ProcDir('/proc')
def _is_pid_running(pid):
"""Return True if pid is running."""
try:
os.kill(pid, 0)
except OSError as e:
return e.errno != errno.ESRCH
else:
return True