#!/usr/bin/env python
# -*- coding: utf-8 -*-
# $Id: Interprocess.py 9621 2016-04-06 13:36:10Z Eric $
#
# Copyright (c) 2012 Nuwa Information Co., Ltd, All Rights Reserved.
#
# Licensed under the Proprietary License,
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at our web site.
#
# See the License for the specific language governing permissions and
# limitations under the License.
#
# $Author: Eric $
# $Date: 2016-04-06 21:36:10 +0800 (Wed, 06 Apr 2016) $
# $Revision: 9621 $
"""
Skeleton for Request which is implementated with interprocess communication 
architecture.
"""

import sys
import os
import subprocess
import threading

windows = False

if sys.platform.startswith('win'):
    windows = True

    # Special case for windows platform.
    import win32api
    import win32con
    import win32process

    def _isRunning(filename, all=False):
        #http://mail.python.org/pipermail/python-win32/2005-February/002943.html
        def _g():
            f = os.path.split(filename.lower())[-1]
            processes = win32process.EnumProcesses()    # get PID list
            for pid in processes:
                handle = None
                try:
                    handle = win32api.OpenProcess(
                        win32con.PROCESS_ALL_ACCESS, False, pid)
                    exe = os.path.normpath(
                        win32process.GetModuleFileNameEx(handle, 0))

                    if exe[exe.rfind('\\') + 1:].lower() == f:
                        yield pid
                except Exception:
                    pass
                except GeneratorExit:
                    pass
                finally:
                    if handle:
                        win32api.CloseHandle(handle)
            return

        if all:
            return _g()
        else:
            processes = win32process.EnumProcesses()    # get PID list
            for pid in processes:
                handle = None
                try:
                    handle = win32api.OpenProcess(
                        win32con.PROCESS_ALL_ACCESS, False, pid)
                    exe = os.path.normpath(
                                win32process.GetModuleFileNameEx(handle, 0))

                    if exe.lower() == filename.lower():
                        return pid
                except:
                    pass
                finally:
                    if handle:
                        win32api.CloseHandle(handle)
            return None
else:
    import enumprocess

    def _isRunning(filename, all=False):
        f = os.path.split(filename.lower())[-1]
        names = enumprocess.getPidNames()
        if all:
            return [pid for pid, name in names.items() if name.lower() == f]
        else:
            for pid, name in names.items():
                if name.lower() == f:
                    return pid
        return None

def isRunning(filename, all=False):
    """
    Is process running on system?

    @param filename Full path of process executable file.
    @return If it is running, return PID if all is False, otherwise return
            PID list for all running process for filename.
    """
    return _isRunning(filename, all)

from Iuno.browser import UnknownError, TimeOutError
from Iuno.browser.Request import Request as _Request, Response as _Response

import struct

class Request(_Request):
    """
    Request for interprocess communication implementation.
    """

    def _startServer(self, options={}):
        """
        Start server process.

        @param options Start server options.
        @return PID of server.
        """
        if windows:
            creationflags = options.get('creationflags',
                                        win32process.IDLE_PRIORITY_CLASS)

            p = subprocess.Popen([self.__class__.EXE],
                                 creationflags=creationflags)
        else:
            p = subprocess.Popen([self.__class__.EXE])

        return p.pid

    def _push(self, url, options):
        """
        Push to server.

        @param url URL.
        @param options Options pass to client.
        @return Ticket.
        """
        ticket = None

        with self.__class__.LOCK:
            # We delegate all implementation to other process.
            try:
                pids = isRunning(self.__class__.EXE, all=True)
                for _pid in pids:
                    # print hex(id(threading.currentThread())), 'Try', _pid
                    ticket = self.__class__.CLIENT.Push(url, False, _pid,
                                                        options)
                    if not ticket:
                        # print hex(id(threading.currentThread())), 'dead?'
                        continue # Try next.
                    else:
                        break

                if not ticket: # All server are dead or no any existed.
                    # print hex(id(threading.currentThread())), 'new?/All dead?'
                    # If all server are destroying or dead, we start another
                    # server to connect. Or this is totally new start server.
                    pid = self._startServer(options)
                    # print hex(id(threading.currentThread())), pid
                    ticket = self.__class__.CLIENT.Push(url, True, pid, options)

                    if not ticket: # Unfortunately, we connect failed.
                        # Start another to serve this URL.
                        pid = self._startServer(options)
                        # print hex(id(threading.currentThread())), pid
                        ticket = self.__class__.CLIENT.Push(url, True, pid,
                                                            options)

            except RuntimeError, e:
                if str(e) == 'TIMEOUT':
                    raise TimeOutError(url, -1, str(e))
                else:
                    # print "Push exception."
                    raise

            return ticket

    def _pop(self, url, ticket):
        """
        Pop result from server.

        @param url URL.
        @param ticket Ticket.
        @return Result.
        """
        try:
            return self.__class__.CLIENT.Pop(url, ticket)
        except RuntimeError, e:
            if str(e) == 'TIMEOUT':
                raise TimeOutError(url, -1, str(e))
            else:
                # print "Pop exception."
                raise UnknownError(url, -1, str(e))
    
    def _send(self):
        
        # Build options that will be passed to C extension and server.
        options = {} 
                
        if self.config:
            options.update(self.config)
            
        if self.method:
            if self.method == 'get' and self.params:
                options[self.method] = self.params
            elif self.method == 'post' and self.data:
                options[self.method] = self.data

        if self.cookies:
            options['cookie'] = self.cookies
            
        if self.auth:
            options['auth'] = '%s:%s' % self.auth
        
        if 'disable' in options:
            if isinstance(options['disable'], (list, tuple)):
                options['disable'] = ','.join(options['disable'])

        maxTry = 3
        result = None
        times = 0

        while not result:
            try:
                ticket = self._push(self.url, options)
                if not ticket:
                    raise RuntimeError()
                result = self._pop(self.url, ticket)
            except:
                # Try again.
                times += 1
                if times >= maxTry:
                    raise
                    
        # Clean the temporary file.
        tests = [result.src, result.snapshot]
        for i, v in enumerate(tests):
            if isinstance(v, basestring) and v.startswith('file:///'):
                l = v[len('file:///'):]
                if os.path.exists(l):
                    with open(l, 'rb') as f:
                        tests[i] = f.read()
                    os.remove(l)

        response = _Response(self.url, content=tests[0], 
                             lastModified=result.lastModified,
                             snapshot=tests[1],
                             config=self.config)
        try:
            error = int(result.lastModified)
            s = struct.pack("l", error)
            error = struct.unpack("L", s)[0]
        except ValueError:            
            return response
        else:
            if self.throw:
                return self.handleError(error, self.url, options, response)
            else:
                response.statusCode = error
                return response

    def handleError(self, error, url, options, response):
        """
        Handle error.

        @param error Error code.
        @param url URL.
        @param options Options.
        @param response Response object.
        """
        raise NotImplemented()

_killLock = threading.RLock()

def kill(impl):
    """
    Kill all existed server processes.

    @param impl Impl class.
    """
    with _killLock:
        # This is first time import, we try to clean up dead servers.
        pids = isRunning(impl.EXE, all=True)
        if windows:
            for pid in pids: # It may not only one server running.
                # print 'Terminate ', pid
                handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS,
                                              False, pid)
                if handle:
                    win32process.TerminateProcess(handle, -1)
                    win32api.Sleep(100) # Wait for resources to be released.
                    win32api.CloseHandle(handle)
        else:
            import time
            for pid in pids: # It may not only one server running.
                os.system("kill %ld" % pid)
                time.sleep(0.1)
