#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Handles the apt system lock"""
# Copyright (C) 2010 Sebastian Heinlein <devel@glatzor.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

__author__ = "Sebastian Heinlein <devel@glatzor.de>"

__all__ = ("LockFailedError", "system")

import fcntl
import os
import struct

import apt_pkg
from gi.repository import GLib

from aptkit import enums
from aptkit.errors import TransactionCancelled


class LockFailedError(Exception):

    """The locking of file failed."""

    def __init__(self, flock, process=None):
        """Return a new LockFailedError instance.

        Keyword arguments:
        flock -- the path of the file lock
        process -- the process which holds the lock or None
        """
        msg = "Could not acquire lock on %s." % flock
        if process:
            msg += " The lock is held by %s." % process
        Exception.__init__(self, msg)
        self.flock = flock
        self.process = process


class FileLock(object):

    """Represents a file lock."""

    def __init__(self, path):
        self.path = path
        self.fd = None

    @property
    def locked(self):
        return self.fd is not None

    def acquire(self):
        """Return the file descriptor of the lock file or raise
        LockFailedError if the lock cannot be obtained.
        """
        if self.fd:
            return self.fd
        fd_lock = apt_pkg.get_lock(self.path)
        if fd_lock < 0:
            process = get_locking_process_name(self.path)
            raise LockFailedError(self.path, process)
        else:
            self.fd = fd_lock
            return fd_lock

    def release(self):
        """Relase the lock."""
        if self.fd:
            os.close(self.fd)
            self.fd = None


def get_locking_process_name(lock_path):
    """Return the name of a process which holds a lock. It will be None if
    the name cannot be retrivied.
    """
    try:
        fd_lock_read = open(lock_path, "r")
    except IOError:
        return None
    else:
        # Get the pid of the locking application
        flk = struct.pack('hhQQi', fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0)
        flk_ret = fcntl.fcntl(fd_lock_read, fcntl.F_GETLK, flk)
        pid = struct.unpack("hhQQi", flk_ret)[4]
        # Get the command of the pid
        try:
            with open("/proc/%s/status" % pid, "r") as fd_status:
                try:
                    for key, value in (line.split(":") for line in
                                       fd_status.readlines()):
                        if key == "Name":
                            return value.strip()
                except Exception:
                    return None
        except IOError:
            return None
        finally:
            fd_lock_read.close()
    return None

apt_pkg.init()

#: The lock for dpkg status file
_status_dir = os.path.dirname(apt_pkg.config.find_file("Dir::State::status"))
status_lock = FileLock(os.path.join(_status_dir, "lock"))
frontend_lock = FileLock(os.path.join(_status_dir, "lock-frontend"))

#: The lock for the package archive
_archives_dir = apt_pkg.config.find_dir("Dir::Cache::Archives")
archive_lock = FileLock(os.path.join(_archives_dir, "lock"))

#: The lock for the repository indexes
lists_lock = FileLock(os.path.join(
    apt_pkg.config.find_dir("Dir::State::lists"), "lock"))


def acquire():
    """Acquire an exclusive lock for the package management system."""
    try:
        for lock in frontend_lock, status_lock, archive_lock, lists_lock:
            if not lock.locked:
                lock.acquire()
    except:
        release()
        raise

    os.environ['DPKG_FRONTEND_LOCKED'] = '1'

def release():
    """Release an exclusive lock for the package management system."""
    for lock in lists_lock, archive_lock, status_lock, frontend_lock:
        lock.release()

    try:
        del os.environ['DPKG_FRONTEND_LOCKED']
    except KeyError:
        pass


def wait_for_lock(trans, alt_lock=None):
    """Acquire the system lock or the optionally given one. If the lock
    cannot be obtained pause the transaction in the meantime.

    :param trans: the transaction
    :param lock: optional alternative lock
    """
    def watch_lock():
        """Helper to unpause the transaction if the lock can be obtained.

        Keyword arguments:
        trans -- the corresponding transaction
        alt_lock -- alternative lock to the system lock
        """
        try:
            if alt_lock:
                alt_lock.acquire()
            else:
                acquire()
        except LockFailedError:
            return True
        trans.paused = False
        return True

    try:
        if alt_lock:
            alt_lock.acquire()
        else:
            acquire()
    except LockFailedError as error:
        trans.paused = True
        trans.status = enums.STATUS_WAITING_LOCK
        if error.process:
            # TRANSLATORS: %s is the name of a package manager
            msg = trans.gettext("Waiting for %s to exit")
            trans.status_details = msg % error.process
        lock_watch = GLib.timeout_add_seconds(3, watch_lock)
        while trans.paused and not trans.cancelled:
            GLib.main_context_default().iteration()
        GLib.source_remove(lock_watch)
        if trans.cancelled:
            raise TransactionCancelled()

# vim:ts=4:sw=4:et
