#-*- coding: utf-8 -*-

# Copyright 2010-2013 Calculate Ltd. http://www.calculate-linux.org
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

import os
from os import path
import re
import sys
import pwd
import time
import traceback

from calculate.desktop._cl_keys import getKey, clearKey
from datavars import DataVarsDesktop, DataVars, __version__,__app__

from calculate.lib.cl_template import (Template, ProgressTemplate,
    TemplatesError,templateFunction,iniParser)
from calculate.lib.utils.files import (runOsCommand, isMount,process,
                        getRunCommands,STDOUT,childMounts,getLoopFromPath,
                        getMdRaidDevices,listDirectory,removeDir)
from calculate.lib.utils.common import (getpathenv,appendProgramToEnvFile,
                                       removeProgramToEnvFile,mountEcryptfs,
                                       CommonError, isBootstrapDataOnly)
from calculate.core.server.func import safetyWrapper

from calculate.lib.cl_lang import setLocalTranslate,getLazyLocalTranslate
setLocalTranslate('cl_desktop3',sys.modules[__name__])
__ = getLazyLocalTranslate(_)
from itertools import ifilter
import tarfile
import tempfile
import shutil
from itertools import count


class DesktopError(Exception):
    """Desktop Error"""


class Desktop:
    """
    Модуль для настройки пользовательского сеанса и выполнения
    принудительного выхода из X сессии пользователя
    """
    
    def __init__(self):
        self.homeDir = ""
        self.clTempl = None
        self.clVars = None

    def createCryptDir(self,userName,uid,gid,userDir,recreateOnError=False):
        """
        Создать шифрование домашней директории, или подключить существующую

        userName,uid,gid,userDir: параметры пользовательской учётной записи
        recreateOnError: пересоздать профиль при ошбиках (используется при доменной
        ученой записи, так пользователь при этом ничего не теряет - профиль на сервере)
        """
        userPwd = getKey(userName)
        error = ""
        # проверить наличие пароля в ключах ядра
        if not userPwd or userPwd == "XXXXXXXX":
            raise DesktopError(_("User password not found"))
        ecryptfsPath = path.join('/home/.ecryptfs',userName)
        # если шифрование уже используется
        if path.exists(ecryptfsPath):
            for d in (".ecryptfs",".Private"):
                source,target = path.join(ecryptfsPath,d),path.join(userDir,d)
                if not path.lexists(target):
                    os.symlink(source,target)
            # попытаться подключить шифрованные данные
            try:
                if not mountEcryptfs(userName,userPwd,userDir):
                    error = _("Failed to mount ecrypted data")
            except CommonError as e:
                error = _("Failed to mount ecrypted data")+": \"%s\""%str(e)
        # если при подключении произошли ошибки
        if error:
            # заархивировать текущий профиль и удалить его
            if recreateOnError:
                self.printSUCCESS(_("Recreating encrypted data"))
                if self.getMountUserPaths(userDir):
                    raise DesktopError(_("Failed to use directory encryption"))
                for source in (userDir,ecryptfsPath):
                    if path.exists(source):
                        if listDirectory(source):
                            target = source+".bak"
                            newtarget = target
                            if path.exists(target):
                                removeDir(target)
                            os.rename(source,newtarget)
                        else:
                            os.rmdir(source)
                self.createUserDir(userName,uid,gid,userDir)
            # ошибка создания шифрования
            else:
                raise DesktopError(error)
        # если нет шифрованных данных
        if not path.exists(ecryptfsPath):
            tf = None
            try:
                # если профиль содержит только данные от бутстрапа core
                if isBootstrapDataOnly(userDir):
                    if childMounts(userDir):
                        raise DesktopError(
                            _("Failed to create the encrypted user profile")+":"+
                            _("User home directory contains mount points"))
                    # поместить данные во временный tarfile
                    calculateName = ".calculate"
                    calculatePath = path.join(userDir,calculateName)
                    tf = tempfile.TemporaryFile()
                    with tarfile.open(fileobj=tf,mode='w:') as tarf:
                        tarf.add(calculatePath,calculateName)
                    tf.flush()
                    tf.seek(0)
                    # удалить эти данные
                    shutil.rmtree(calculatePath)

                # создать шифрованные данные
                e = process('/usr/bin/ecryptfs-setup-private','-u',userName,
                            '-b','-l',userPwd,stderr=STDOUT)
                if e.failed():
                    raise DesktopError(e.read())
                # если были данные от бутстрапа, то распаковать их
                if tf:
                    with tarfile.open(fileobj=tf,mode='r:') as tarf:
                        tarf.extractall(userDir)
            except Exception as e:
                # в случае ошибки сохраняем архив (с данными bootstrap)
                # из памяти в файловую систему
                if tf:
                    tf.seek(0)
                    bakArchName = path.join(userDir,".calculate.tar.bz2")
                    with open(bakArchName,'w') as f:
                        f.write(tf.read())
                raise DesktopError(str(e)+
                    _("Failed to create the encrypted user profile"))
            finally:
                if tf:
                    tf.close()
        return True

    def createUserDir(self, userName, uid, gid, userDir, mode=0700):
        """
        Create user directory with need uid and gid
        """
        if not path.exists(userDir):
            os.makedirs(userDir)
            if mode:
                os.chmod(userDir,mode)
            os.chown(userDir,uid,gid)
            return True
        else:
            raise DesktopError(_("Path %s exists") %userDir)

    def umountUserRes(self, *umountPaths):
        """
        Отключить пользовательские ресурсы
        """
        if umountPaths and type(umountPaths[0]) == list:
            umountPaths = umountPaths[0]
        for umountPath in umountPaths:
            if not self.umountSleepPath(umountPath):
                return False
        return True

    def getMountUserPaths(self, homeDir=False):
        """
        Found user resources
        """
        if not homeDir:
            userName = self.clVars.Get("ur_login")
            homeDir = self.clVars.Get("ur_home_path")
        if not homeDir:
            raise DesktopError(_("Failed to determine home directory"))
        dirStart, dirEnd = path.split(homeDir)
        mountProfileDir = path.join(dirStart, ".%s" %dirEnd)
        mountRemoteProfileDir = path.join(dirStart, ".%s.remote" %dirEnd)
        return filter(lambda x: x.startswith(homeDir) or\
                      x.startswith(mountProfileDir) or\
                      x.startswith(mountRemoteProfileDir),
                      map(lambda x: x.split(" ")[1],\
                          open("/proc/mounts").readlines()))

    def execProg(self, cmdStrProg, inStr=False, envProg={}):
        """
        Exec external program
        """
        env_path = {"PATH":getpathenv()}
        env = {}
        env.update(os.environ.items() + env_path.items() + envProg.items())
        retCode,programOut = runOsCommand(cmdStrProg,in_str=inStr,env_dict=env)
        if not retCode:
            return programOut
        return False

    def umountSleepPath(self, rpath):
        """
        Отмонтировать указанный путь, а также отключить используемые
        в этом пути loop устройства и raid
        """
        # check for mount
        loops = getLoopFromPath(rpath)
        if loops:
            setLoops = set(map(lambda x:x.partition('/dev/')[2],loops))
            mdInfo = getMdRaidDevices()
            for k,v in mdInfo.items():
                if setLoops & set(v):
                    self.umountSleepPath('/dev/%s'%k)
                    process('/sbin/mdadm','--stop','/dev/%s'%k).success()
            for loop in loops:
                self.umountSleepPath(loop)
                process('/sbin/losetup','-d',loop).success()
        if isMount(rpath):
            for waittime in [0,0.5,1,2]:
                time.sleep(waittime)
                if not self.execProg("umount %s"%rpath) is False \
                    or not isMount(rpath):
                    if not isMount(rpath):
                        return True
            self.execProg("fuser -km %s"%rpath)
            for waittime in [0.5, 0.5, 0.5, 0.5, 0.5, 0.5]:
                time.sleep(waittime)
                if not self.execProg("umount %s"%rpath) is False \
                    or not isMount(rpath):
                    if not isMount(rpath):
                        return True
                self.execProg("umount -l %s"%rpath)
            else:
                if isMount(rpath):
                    self.printERROR(_("Failed to unmount directory %s")%rpath)
                    return False
        return True

    def userLogout(self, urLogin):
        """
        Выполнить logout пользователя через dbus
        """
        display = self.clVars.Select('cl_desktop_online_display',
                    where='cl_desktop_online_user',eq=urLogin,limit=1)
        session = self.clVars.Get('cl_desktop_xsession')
        if session == 'xfce':
            logoutCommand = "/usr/bin/qdbus org.xfce.SessionManager " \
                "/org/xfce/SessionManager Logout False False"
        elif session == 'kde':
            logoutCommand = "/usr/bin/kquitapp ksmserver"
        elif session == 'gnome':
            logoutCommand = "/usr/bin/qdbus org.gnome.SessionManager " \
                "/org/gnome/SessionManager Logout 1"
        else:
            raise DesktopError(_("Unable to detect the X session"))
        if process("su",urLogin,"-c",
            ("DISPLAY=:%s "%display)+logoutCommand).failed():
            raise DesktopError(_("Unable send logout command"))
        return True

    def waitLogout(self,urLogin,waitTime,postWaitTime=5):
        """
        Ожидать завершения пользовательского сеанса

        Args:
        urLogin: логин пользователя
        waitTime: время ожидания завершения сеанса
        """
        if filter(lambda x: "xdm/xdm\x00--logout" in x,
                 getRunCommands()):
            for i in range(0,waitTime):
                if not filter(lambda x: "xdm/xdm\x00--logout" in x,
                    getRunCommands()):
                    return True
                time.sleep(1)
            else:
                raise DesktopError(_("Unable to wait for completion "
                                     "of the user logout"))
        for wait in range(0,5):
            self.clVars.Invalidate('cl_desktop_online_data')
            if not urLogin in self.clVars.Get('cl_desktop_online_user'):
                return True
            time.sleep(1)
        else:
            return False
