#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# «recovery_backend» - Backend Manager.  Handles backend service calls
#
# Copyright (C) 2009, Dell Inc.
#           (C) 2008 Canonical Ltd.
#
# Author:
#  - Mario Limonciello <Mario_Limonciello@Dell.com>
#
# This 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 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.
#
# You should have received a copy of the GNU General Public License along
# with this application; if not, write to the Free Software Foundation, Inc., 51
# Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
##################################################################################
import logging
import os
import os.path
import signal
import sys
from gi.repository import GObject
import dbus
import dbus.service
import dbus.mainloop.glib
import atexit
import tempfile
import subprocess

from ubunturecovery.recovery_common import DBUS_BUS_NAME, DBUS_INTERFACE_NAME,PermissionDeniedByPolicy
from ubunturecovery.recovery_threading import ProgressByPulse, ProgressBySize

class Backend(dbus.service.Object):
    '''Backend manager.

    This encapsulates all services calls of the backend. It
    is implemented as a dbus.service.Object, so that it can be called through
    D-BUS as well (on the /RecoveryMedia object path).
    '''

    #
    # D-BUS control API
    #

    def __init__(self):
        dbus.service.Object.__init__(self)

        #initialize variables that will be used during create and run
        self.bus = None
        self.main_loop = None
        self._timeout = False
        self.dbus_name = None

        # cached D-BUS interfaces for _check_polkit_privilege()
        self.dbus_info = None
        self.polkit = None
        self.progress_thread = None
        self.enforce_polkit = True

    def run_dbus_service(self, timeout=None, send_usr1=False):
        '''Run D-BUS server.

        If no timeout is given, the server will run forever, otherwise it will
        return after the specified number of seconds.

        If send_usr1 is True, this will send a SIGUSR1 to the parent process
        once the server is ready to take requests.
        '''
        dbus.service.Object.__init__(self, self.bus, '/RecoveryMedia')
        self.main_loop = GObject.MainLoop()
        self._timeout = False
        if timeout:
            def _quit():
                """This function is ran at the end of timeout"""
                self.main_loop.quit()
                return True
            GObject.timeout_add(timeout * 1000, _quit)

        # send parent process a signal that we are ready now
        if send_usr1:
            os.kill(os.getppid(), signal.SIGUSR1)

        # run until we time out
        while not self._timeout:
            if timeout:
                self._timeout = True
            self.main_loop.run()

    @classmethod
    def create_dbus_server(cls, session_bus=False):
        '''Return a D-BUS server backend instance.

        Normally this connects to the system bus. Set session_bus to True to
        connect to the session bus (for testing).

        '''
        backend = Backend()
        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
        if session_bus:
            backend.bus = dbus.SessionBus()
            backend.enforce_polkit = False
        else:
            backend.bus = dbus.SystemBus()
        try:
            backend.dbus_name = dbus.service.BusName(DBUS_BUS_NAME, backend.bus)
        except dbus.exceptions.DBusException as msg:
            logging.error("Exception when spawning dbus service")
            logging.error(msg)
            return None
        return backend

    #
    # Internal methods
    #

    def _reset_timeout(self):
        '''Reset the D-BUS server timeout.'''

        self._timeout = False

    def _check_polkit_privilege(self, sender, conn, privilege):
        '''Verify that sender has a given PolicyKit privilege.

        sender is the sender's (private) D-BUS name, such as ":1:42"
        (sender_keyword in @dbus.service.methods). conn is
        the dbus.Connection object (connection_keyword in
        @dbus.service.methods). privilege is the PolicyKit privilege string.

        This method returns if the caller is privileged, and otherwise throws a
        PermissionDeniedByPolicy exception.
        '''
        if sender is None and conn is None:
            # called locally, not through D-BUS
            return
        if not self.enforce_polkit:
            # that happens for testing purposes when running on the session
            # bus, and it does not make sense to restrict operations here
            return

        # get peer PID
        if self.dbus_info is None:
            self.dbus_info = dbus.Interface(conn.get_object('org.freedesktop.DBus',
                '/org/freedesktop/DBus/Bus', False), 'org.freedesktop.DBus')
        pid = self.dbus_info.GetConnectionUnixProcessID(sender)

        # query PolicyKit
        if self.polkit is None:
            self.polkit = dbus.Interface(dbus.SystemBus().get_object(
                'org.freedesktop.PolicyKit1', '/org/freedesktop/PolicyKit1/Authority', False),
                'org.freedesktop.PolicyKit1.Authority')
        try:
            # we don't need is_challenge return here, since we call with AllowUserInteraction
            (is_auth, unused, details) = self.polkit.CheckAuthorization(
                    ('unix-process', {'pid': dbus.UInt32(pid, variant_level=1),
                        'start-time': dbus.UInt64(0, variant_level=1)}),
                    privilege, {'': ''}, dbus.UInt32(1), '', timeout=600)
        except dbus.DBusException as msg:
            if msg.get_dbus_name() == \
                                    'org.freedesktop.DBus.Error.ServiceUnknown':
                # polkitd timed out, connect again
                self.polkit = None
                return self._check_polkit_privilege(sender, conn, privilege)
            else:
                raise

        if not is_auth:
            logging.debug('_check_polkit_privilege: sender %s on connection %s pid %i is not authorized for %s: %s',
                    sender, conn, pid, privilege, str(details))
            raise PermissionDeniedByPolicy(privilege)

    #
    # Internal API for calling from Handlers (not exported through D-BUS)
    #
    def request_mount(self, recovery, sender=None, conn=None):
        '''Attempts to mount the recovery partition

           If successful, return mntdir.
           If we find that it's already mounted elsewhere, return that mount
           If unsuccessful, return an empty string
        '''

        #In this is just a directory
        if os.path.isdir(recovery):
            return recovery

        #check for an existing mount
        command = subprocess.Popen(['mount'], stdout=subprocess.PIPE)
        output = command.communicate()[0].decode('utf-8').split('\n')
        for line in output:
            processed_line = line.split()
            if len(processed_line) > 0 and processed_line[0] == recovery:
                return processed_line[2]

        #if not already, mounted, produce a mount point
        mntdir = tempfile.mkdtemp()
        mnt_args = ['mount', '-r', recovery, mntdir]
        if ".iso" in recovery:
            mnt_args.insert(1, 'loop')
            mnt_args.insert(1, '-o')
        else:
            self._check_polkit_privilege(sender, conn,
                                                'com.ubuntu.recoverymedia.create')
        command = subprocess.Popen(mnt_args,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE)
        output = command.communicate()
        ret = command.wait()
        if ret is not 0:
            os.rmdir(mntdir)
            if ret == 32:
                try:
                    mntdir = output[1].decode('utf-8').strip('\n').split('on')[1].strip(' ')
                except IndexError:
                    mntdir = ''
                    logging.warning("IndexError when operating on output string")
            else:
                mntdir = ''
                logging.warning("Unable to mount recovery partition")
                logging.warning(output)
        else:
            atexit.register(self._unmount_drive, mntdir)
        return mntdir

    def _unmount_drive(self, mnt):
        """Unmounts something mounted at a particular mount point"""
        if os.path.exists(mnt):
            ret = subprocess.call(['umount', mnt])
            if ret is not 0:
                print("Error unmounting %s" % mnt, file=sys.stderr)
            try:
                os.rmdir(mnt)
            except OSError as msg:
                print("Error cleaning up: %s" % str(msg), file=sys.stderr)

    def start_sizable_progress_thread(self, input_str, mnt, w_size):
        """Initializes the extra progress thread, or resets it
           if it already exists'"""
        self.progress_thread = ProgressBySize(input_str, mnt, w_size)
        self.progress_thread.progress = self.report_progress
        self.progress_thread.start()

    def stop_progress_thread(self):
        """Stops the extra thread for reporting progress"""
        self.progress_thread.join()

    def start_pulsable_progress_thread(self, input_str):
        """Starts the extra thread for pulsing progress in the UI"""
        self.progress_thread = ProgressByPulse(input_str)
        self.progress_thread.progress = self.report_progress
        self.progress_thread.start()

    def creating_iso(self):
        if not os.access("/usr/share/ubuntu/recovery.iso", os.R_OK):
            subprocess.Popen(['/usr/share/ubuntu/bin/generate-recovery-iso'])
        return False

    #
    # Client API (through D-BUS)
    #
    @dbus.service.method(DBUS_INTERFACE_NAME,
        in_signature = 'b', out_signature = 'b', sender_keyword = 'sender',
        connection_keyword = 'conn')
    def force_network(self, enable, sender=None, conn=None):
        """Forces a network manager disable request as root"""
        self._check_polkit_privilege(sender, conn, 'com.ubuntu.recoverymedia.force_network')
        bus = dbus.SystemBus()
        obj = bus.get_object('org.freedesktop.NetworkManager', '/org/freedesktop/NetworkManager')
        int = dbus.Interface(obj, 'org.freedesktop.NetworkManager')
        return int.Sleep(not enable)

    @dbus.service.method(DBUS_INTERFACE_NAME,
        in_signature = '', out_signature = '', sender_keyword = 'sender',
        connection_keyword = 'conn')
    def create(self, sender=None, conn=None):
        """Create recovery ISO"""
        self._check_polkit_privilege(sender, conn, 'com.ubuntu.recoverymedia.create')
        GObject.timeout_add(500, self.creating_iso)

    @dbus.service.method(DBUS_INTERFACE_NAME,
        in_signature = '', out_signature = '', sender_keyword = 'sender',
        connection_keyword = 'conn')
    def request_exit(self, sender=None, conn=None):
        """Closes the backend and cleans up"""
        self._check_polkit_privilege(sender, conn, 'com.ubuntu.recoverymedia.request_exit')
        self._timeout = True
        self.main_loop.quit()
