#!/usr/bin/env python
#
# This file is part of Python Download Manager
# Copyright (C) 2007-2009 Instituto Nokia de Tecnologia
# Author: Kenneth Christiansen <kenneth.christiansen@openbossa.org>
#
# 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 3 of the License, or
# (at your option) 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.
#
# Additional permission under GNU GPL version 3 section 7
#
# The copyright holders grant you an additional permission under Section 7
# of the GNU General Public License, version 3, exempting you from the
# requirement in Section 6 of the GNU General Public License, version 3, to
# accompany Corresponding Source with Installation Information for the
# Program or any work based on the Program. You are still required to comply
# with all other Section 6 requirements to provide Corresponding Source.
#

__author__ = \
    'Kenneth Christiansen, ' \
    'Leonardo Sobral Cunha, ' \
    'Artur Souza',
__author_email__ = \
    'kenneth.christiansen@openbossa.com, ' \
    'leonardo.cunha@openbossa.org, ' \
    'artur.souza@openbossa.org',
__version__ = "0.2"

import os
import sys
import logging
import errno
import gobject

gobject.threads_init()

import dbus
import dbus.service
import dbus.glib

dbus.glib.init_threads()

from dbus import DBusException

from downloadmanager.server import DownloadSession
from optparse import OptionParser

# timeout of 5 minutes (300 ms) to die
timeout = 300 * 1000
log_level = logging.ERROR
pidfile = "/tmp/download-manager-%d.pid" % os.getuid()
logfile = os.devnull
log = logging.getLogger("downloadmanager.daemon")


class DownloadManagerException(Exception):
    def formatException(tb_level=5):
        cls, exc, tb = sys.exc_info()
        try:
            execName = cls.__name__
            execArgs = exc.args
            execTb = traceback.format_tb(tb, tb_level)[0]
            return (execName, execTb, execArgs)
        except Exception:
            return ("", "", "")

    def __init__(self, arg):
        Exception.__init__(arg)
        self.execname, self.exectb, self.execargs = self.formatException()

    def __str__(self):
        return "%s @%s: %s" % (self.execname, self.exectb, self.execargs)


class InvalidSessionError(DownloadManagerException, DBusException):
    def __init__(self, session):
        session_error = KeyError("Invalid Session")
        DownloadManagerException.__init__(self, session_error)
        DBusException.__init__(self, session_error)
        self.session = session

    def __str__(self):
        return "invalid session: %s @%s" % (self.session, self.exectb)


class OwnerCheckError(DownloadManagerException, DBusException):
    def __init__(self, owner):
        error = ValueError(owner)
        DownloadManagerException.__init__(self, error)
        DBusException.__init__(self, error)
        self.owner = owner

    def __str_(self):
        return "sender's id is different " \
            "from owner's id (%s) @%s" % (self.owner, self.exectb)


class DaemonStartError(DownloadManagerException):
    """Problems while starting daemon."""
    def __init__(self, pid):
        pid_error = SystemError(pid)
        DownloadManagerException.__init__(self, pid_error)
        self.pid = pid

    def __str__(self):
        return "pid %s already exists. " \
               "Is it still running ?" % (self.pid)


class Daemon(object):
    """Class to daemonize the download manager

    This class uses the double-fork technique in order to daemonize
    our download manager. It also has basic functions to be executed by
    the class that wants to be daemonized.

    """
    def __init__(self, pidfile):
        self.pidfile = pidfile

    def _daemonize(self):
        """Daemonize the subclass using 'double-fork' trick"""
        # do first fork
        try:
            pid = os.fork()

            if pid > 0:
                # must exit first parent
                os._exit(0)
        except OSError, e:
            log.critical("Fork #1 failed %d (%s)", e.errno, e.strerror)
            os._exit(1)

        os.setsid() # run a program in a new session
        os.umask(0)

        # do second fork
        try:
            pid = os.fork()

            if pid > 0:
                # must exit first parent
                os._exit(0)
        except OSError, e:
            log.critical("Fork #2 failed %d (%s)", e.errno, e.strerror)
            os._exit(1)

        # write pid to file
        pid = str(os.getpid())
        pidfile = open(self.pidfile, "w+")
        pidfile.write("%s\n" % pid)
        pidfile.close()

    def _getpid(self):
        """Get the pid inside pidfile"""
        try:
            pidfile = open(self.pidfile, "r")
            pid = int(pidfile.readline())
            pidfile.close()
        except IOError:
            pid = None

        if pid and not self._pid_exists(pid):
            self._delpid()
            pid = None

        return pid

    def _pid_exists(self, pid):
        """Check whether certain pid is valid (has a process with it)"""
        try:
            os.kill(pid, 0)
            return True
        except OSError, err:
            return err.errno == errno.EPERM

    def _delpid(self):
        """Delete the pidfile"""
        os.remove(self.pidfile)
        return True

    def start(self):
        """Start the daemon"""
        pid = self._getpid()

        if pid:
            # the daemon is running or we have a problem
            raise DaemonStartError(pid)

        os.nice(20)
        # Start the daemon
        self._daemonize()
        self.run()
        return True

    def stop(self):
        """Stop the daemon, killing the process"""
        # Try to get pid from file
        pid = self._getpid()

        if pid:

            if pid == os.getpid():
                self.loop.quit()
                self._delpid()
                log.info("Process %s@%s stopped by dbus", pid, self.pidfile)
                return True
            else:
                log.warning("You should not try to stop this daemon"
                            " through another process: %s",
                            self.__class__.__name__)

        else:
            # When we are restarting the daemon this is not an error
            log.info("Pidfile %s does not exist. Are you sure the "
                     "daemon is running ?", self.pidfile)
            return True

    def run(self):
        """This will be called when the daemon starts, so you should override
        this when subclassing Daemon. If you want to make the daemon run
        continuously, you'll need a mainloop also.
        """
        raise NotImplementedError("You must implement run() "
                                  "to daemonize something")

    def __str__(self):
        return "%s(pidfile=%s)" % (self.__class__.__name__, self.pidfile)


class DownloadManagerDaemon(Daemon, dbus.service.Object):
    DBUS_SERVICE_NAME = "br.org.indt.DownloadManager"
    DBUS_OBJ_PATH     = "/br/org/indt/DownloadManager"
    DBUS_IFACE        = "br.org.indt.DownloadManager"

    def __init__(self, pidfile):
        Daemon.__init__(self, pidfile)
        self.session_bus = dbus.SessionBus()
        self.bn = dbus.service.BusName(self.DBUS_SERVICE_NAME,
                                       bus=self.session_bus)

        dbus.service.Object.__init__(self, self.bn, self.DBUS_OBJ_PATH)

        self.sessions = []

    def _new_session(self, http_uri, local_path, sender):
        try:
            session = DownloadSession(self.session_bus, sender,
                                      http_uri, local_path)
        except Exception, e:
            log.error("Error while creating session: %s", e)
            raise

        session.dispose_cb = self._delete_session
        self.sessions.append(session)
        return session

    def _delete_session_int(self, session):
        session.Pause()
        try:
            session.remove_from_connection()
        except AttributeError:
            log.warning("old dbus version, cannot unexport object: %s", session)

    def _delete_session(self, obj_path=None):
        if not obj_path:
            raise ValueError("Can't delete a session without "
                             "the object's path")
        for session in self.sessions:
            if session.__dbus_object_path__ == obj_path:
                self._delete_session_int(session)
                self.sessions.remove(session)
                log.debug("Remaining sessions: %s", self.sessions)
                return

        log.error("Could not find session: %s", obj_path)

    def _delete_all_sessions(self):
        for session in self.sessions:
            self._delete_session_int(session)
        del self.sessions[:]

    @dbus.service.method(DBUS_IFACE)
    def DeleteSessions(self):
        """Delete all sessions of a given application"""
        self._delete_all_sessions()

    @dbus.service.method(DBUS_IFACE)
    def DeleteSession(self, obj_path):
        self._delete_session(obj_path)

    @dbus.service.method(DBUS_IFACE, sender_keyword='sender')
    def CreateSession(self, http_uri, local_path, sender=None):
        """Create a session on the bus queue and return it's id"""
        session = self._new_session(str(http_uri), str(local_path), sender)
        log.info("Created session: %s %s", sender, session)
        return session.__dbus_object_path__

    @dbus.service.method(DBUS_IFACE)
    def Start(self):
        """Start our Download Manager daemon"""
        Daemon.start(self)

    @dbus.service.method(DBUS_IFACE)
    def Stop(self):
        """Stop our DownloadManager daemon and it's sessions"""
        self._delete_all_sessions()
        log.info("Stopped DownloadManager daemon")
        Daemon.stop(self)

    def check_sessions(self):
        """Check for DownloadManager's clients inactivity"""
        if not self.sessions:
            log.debug("DownloadManager quit due to inactivity")
            self.Stop()
        return False

    def name_owner_changed(self, obj, old, new):
        """Function to handle when a client disconnect from the engine"""
        if new or not old or old != obj:
            return

        for session in self.sessions:
            if session.dbus_id == obj:
                self._delete_session_int(session)
                self.sessions.remove(session)
                log.debug("removed dead client %s %s", obj, session)
                return

    def run(self):
        """Implementation of method run for class Daemon"""
        # dbus stuff
        try:
            self.session_bus.add_signal_receiver(self.name_owner_changed,
                                                 "NameOwnerChanged")
            log.info("Starting DownloadManager")
            self.loop = gobject.MainLoop()
            self.loop.run()
        except Exception, e:
            log.error(e, exc_info=True)
            raise


def start(log_file=None):
    try:
        daemon = DownloadManagerDaemon(pidfile)
        if not daemon.Start():
            sys.exit(1)
    except DaemonStartError, e:
        log.error(e)
        sys.exit(1)

    except Exception, e:
        if os.path.exists(pidfile):
            os.remove(pidfile)
        log.error(e, exc_info=True)
        sys.exit(1)

def stop():
    if not stop_daemon():
        log.error("DownloadManager is not running")
        sys.exit(1)

def restart(log_file=None):
    stop()
    start()

def stop_daemon():
    # We are not the daemon and because of this we need to stop it
    # using dbus, just like a client would do
    if os.path.exists(pidfile):
        bus = dbus.SessionBus()
        try:
            me_obj = bus.get_object(DownloadManagerDaemon.DBUS_SERVICE_NAME,
                                    DownloadManagerDaemon.DBUS_OBJ_PATH,
                                    introspect=False)
        except Exception, e:
            return False
        me_iface = dbus.Interface(me_obj, DownloadManagerDaemon.DBUS_IFACE)
        me_iface.Stop()
        return True

    return False

usage = "Usage: %prog [options] start | stop | restart"
parser = OptionParser(usage=usage)
parser.add_option("-v", "--verbose", dest="verbosity", action="count",
                  help="Use this option to increase verbosity")
parser.add_option("-p", "--pidfile", action="store",
                  dest="pidfile", metavar="FILE",
                  help="Location of file to store "
                  "Download Manager PID file e.g.:/tmp/pidfile")
parser.add_option("-l", "--logfile", action="store",
                  dest="logfile", metavar="FILE",
                  help="Location of file to store "
                  "Download Manager logs e.g.:/tmp/download-manager-log-file")


(options, args) = parser.parse_args()


if len(args) != 1:
    parser.error("You need to specify at least one action")
    sys.exit(2)

if options.verbosity:
    log_level -= 10 * options.verbosity

if options.pidfile:
    pidfile = options.pidfile

if options.logfile:
    logfile = options.logfile

logging.basicConfig(filename=logfile, level=log_level,
                    format=("### %(asctime)s %(name)-18s \t%(levelname)-8s "
                            "\t%(message)s"),
                    datefmt="%Y-%m-%d %H:%M:%S")

commands = {
    "start": start,
    "stop": stop,
    "restart": restart
    }

engine_cmd = commands[args[0]]
engine_cmd()
