Blame dnf/lock.py

Packit 6f3914
# lock.py
Packit 6f3914
# DNF Locking Subsystem.
Packit 6f3914
#
Packit 6f3914
# Copyright (C) 2013-2016 Red Hat, Inc.
Packit 6f3914
#
Packit 6f3914
# This copyrighted material is made available to anyone wishing to use,
Packit 6f3914
# modify, copy, or redistribute it subject to the terms and conditions of
Packit 6f3914
# the GNU General Public License v.2, or (at your option) any later version.
Packit 6f3914
# This program is distributed in the hope that it will be useful, but WITHOUT
Packit 6f3914
# ANY WARRANTY expressed or implied, including the implied warranties of
Packit 6f3914
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
Packit 6f3914
# Public License for more details.  You should have received a copy of the
Packit 6f3914
# GNU General Public License along with this program; if not, write to the
Packit 6f3914
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
Packit 6f3914
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
Packit 6f3914
# source code or documentation are not subject to the GNU General Public
Packit 6f3914
# License and may only be used or replicated with the express permission of
Packit 6f3914
# Red Hat, Inc.
Packit 6f3914
#
Packit 6f3914
Packit 6f3914
from __future__ import absolute_import
Packit 6f3914
from __future__ import unicode_literals
Packit 6f3914
from dnf.exceptions import ProcessLockError, ThreadLockError, LockError
Packit 6f3914
from dnf.i18n import _
Packit 6f3914
from dnf.yum import misc
Packit 6f3914
import dnf.logging
Packit 6f3914
import dnf.util
Packit 6f3914
import errno
Packit 6f3914
import fcntl
Packit 6f3914
import hashlib
Packit 6f3914
import logging
Packit 6f3914
import os
Packit 6f3914
import threading
Packit 6f3914
import time
Packit 6f3914
Packit 6f3914
logger = logging.getLogger("dnf")
Packit 6f3914
Packit 6f3914
def _fit_lock_dir(dir_):
Packit 6f3914
    if not dnf.util.am_i_root():
Packit 6f3914
        # for regular users the best we currently do is not to clash with
Packit 6f3914
        # another DNF process of the same user. Since dir_ is quite definitely
Packit 6f3914
        # not writable for us, yet significant, use its hash:
Packit 6f3914
        hexdir = hashlib.sha1(dir_.encode('utf-8')).hexdigest()
Packit 6f3914
        dir_ = os.path.join(misc.getCacheDir(), 'locks', hexdir)
Packit 6f3914
    return dir_
Packit 6f3914
Packit 6f3914
def build_download_lock(cachedir, exit_on_lock):
Packit 6f3914
    return ProcessLock(os.path.join(_fit_lock_dir(cachedir), 'download_lock.pid'),
Packit 6f3914
                       'cachedir', not exit_on_lock)
Packit 6f3914
Packit 6f3914
def build_metadata_lock(cachedir, exit_on_lock):
Packit 6f3914
    return ProcessLock(os.path.join(_fit_lock_dir(cachedir), 'metadata_lock.pid'),
Packit 6f3914
                       'metadata', not exit_on_lock)
Packit 6f3914
Packit 6f3914
Packit 6f3914
def build_rpmdb_lock(persistdir, exit_on_lock):
Packit 6f3914
    return ProcessLock(os.path.join(_fit_lock_dir(persistdir), 'rpmdb_lock.pid'),
Packit 6f3914
                       'RPMDB', not exit_on_lock)
Packit 6f3914
Packit 6f3914
Packit 6f3914
def build_log_lock(logdir, exit_on_lock):
Packit 6f3914
    return ProcessLock(os.path.join(_fit_lock_dir(logdir), 'log_lock.pid'),
Packit 6f3914
                       'log', not exit_on_lock)
Packit 6f3914
Packit 6f3914
Packit 6f3914
class ProcessLock(object):
Packit 6f3914
    def __init__(self, target, description, blocking=False):
Packit 6f3914
        self.blocking = blocking
Packit 6f3914
        self.count = 0
Packit 6f3914
        self.description = description
Packit 6f3914
        self.target = target
Packit 6f3914
        self.thread_lock = threading.RLock()
Packit 6f3914
Packit 6f3914
    def _lock_thread(self):
Packit 6f3914
        if not self.thread_lock.acquire(blocking=False):
Packit 6f3914
            msg = '%s already locked by a different thread' % self.description
Packit 6f3914
            raise ThreadLockError(msg)
Packit 6f3914
        self.count += 1
Packit 6f3914
Packit 6f3914
    def _try_lock(self, pid):
Packit 6f3914
        fd = os.open(self.target, os.O_CREAT | os.O_RDWR, 0o644)
Packit 6f3914
Packit 6f3914
        try:
Packit 6f3914
            try:
Packit 6f3914
                fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
Packit 6f3914
            except OSError as e:
Packit 6f3914
                if e.errno == errno.EWOULDBLOCK:
Packit 6f3914
                    return -1
Packit 6f3914
                raise
Packit 6f3914
Packit 6f3914
            old_pid = os.read(fd, 20)
Packit 6f3914
            if len(old_pid) == 0:
Packit 6f3914
                # empty file, write our pid
Packit 6f3914
                os.write(fd, str(pid).encode('utf-8'))
Packit 6f3914
                return pid
Packit 6f3914
Packit 6f3914
            try:
Packit 6f3914
                old_pid = int(old_pid)
Packit 6f3914
            except ValueError:
Packit 6f3914
                msg = _('Malformed lock file found: %s.\n'
Packit 6f3914
                        'Ensure no other dnf/yum process is running and '
Packit 6f3914
                        'remove the lock file manually or run '
Packit 6f3914
                        'systemd-tmpfiles --remove dnf.conf.') % (self.target)
Packit 6f3914
                raise LockError(msg)
Packit 6f3914
Packit 6f3914
            if old_pid == pid:
Packit 6f3914
                # already locked by this process
Packit 6f3914
                return pid
Packit 6f3914
Packit 6f3914
            if not os.access('/proc/%d/stat' % old_pid, os.F_OK):
Packit 6f3914
                # locked by a dead process, write our pid
Packit 6f3914
                os.lseek(fd, 0, os.SEEK_SET)
Packit 6f3914
                os.ftruncate(fd, 0)
Packit 6f3914
                os.write(fd, str(pid).encode('utf-8'))
Packit 6f3914
                return pid
Packit 6f3914
Packit 6f3914
            return old_pid
Packit 6f3914
Packit 6f3914
        finally:
Packit 6f3914
            os.close(fd)
Packit 6f3914
Packit 6f3914
    def _unlock_thread(self):
Packit 6f3914
        self.count -= 1
Packit 6f3914
        self.thread_lock.release()
Packit 6f3914
Packit 6f3914
    def __enter__(self):
Packit 6f3914
        dnf.util.ensure_dir(os.path.dirname(self.target))
Packit 6f3914
        self._lock_thread()
Packit 6f3914
        prev_pid = -1
Packit 6f3914
        my_pid = os.getpid()
Packit 6f3914
        pid = self._try_lock(my_pid)
Packit 6f3914
        while pid != my_pid:
Packit 6f3914
            if pid != -1:
Packit 6f3914
                if not self.blocking:
Packit 6f3914
                    self._unlock_thread()
Packit 6f3914
                    msg = '%s already locked by %d' % (self.description, pid)
Packit 6f3914
                    raise ProcessLockError(msg, pid)
Packit 6f3914
                if prev_pid != pid:
Packit 6f3914
                    msg = _('Waiting for process with pid %d to finish.') % (pid)
Packit 6f3914
                    logger.info(msg)
Packit 6f3914
                    prev_pid = pid
Packit 6f3914
            time.sleep(1)
Packit 6f3914
            pid = self._try_lock(my_pid)
Packit 6f3914
Packit 6f3914
    def __exit__(self, *exc_args):
Packit 6f3914
        if self.count == 1:
Packit 6f3914
            os.unlink(self.target)
Packit 6f3914
        self._unlock_thread()