#!/usr/bin/env python
# ppserver.py
#
# Parallel Python Software: http://www.parallelpython.com
# Copyright (c) 2005-2009, Vitalii Vanovschi
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#    * Redistributions of source code must retain the above copyright notice,
#      this list of conditions and the following disclaimer.
#    * Redistributions in binary form must reproduce the above copyright
#      notice, this list of conditions and the following disclaimer in the
#      documentation and/or other materials provided with the distribution.
#    * Neither the name of the author nor the names of its contributors
#      may be used to endorse or promote products derived from this software
#      without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
# THE POSSIBILITY OF SUCH DAMAGE.
"""
Parallel Python Software, Network Server.

http://www.parallelpython.com - updates, documentation, examples and support
forums
"""

from __future__ import absolute_import
from __future__ import print_function
from six import text_type as str
from six.moves import range
import six.moves._thread

copyright = "Copyright (c) 2005-2009 Vitalii Vanovschi. All rights reserved"
version = "1.5.7"

import logging
import getopt
import sys
import socket
import random
import string
import time
import os

from . import pptransport
from . import ppauto

from pp import Server  # @UnresolvedImport

# compartibility with Python 2.6
try:
    import hashlib
    sha_new = hashlib.sha1
except ImportError:
    import sha
    sha_new = sha.new


class _NetworkServer(Server):
    """
    Network Server Class.
    """

    def __init__(self, ncpus="autodetect", interface="0.0.0.0",
                 broadcast="255.255.255.255", port=None, secret=None,
                 timeout=None, loglevel=logging.WARNING, restart=False,
                 proto=0):
        Server.__init__(self, ncpus, secret=secret, loglevel=loglevel,
                        restart=restart, proto=proto)
        self.host = interface
        self.bcast = broadcast
        if port is not None:
            self.port = port
        else:
            self.port = self.default_port
        self.timeout = timeout
        self.ncon = 0
        self.last_con_time = time.time()
        self.ncon_lock = six.moves._thread.allocate_lock()

        logging.debug("Strarting network server interface=%s port=%i"
                      % (self.host, self.port))
        if self.timeout is not None:
            logging.debug("ppserver will exit in %i seconds if no "
                          "connections with clients exist" % (self.timeout))
            six.moves._thread.start_new_thread(self.check_timeout, ())

    def ncon_add(self, val):
        """
        Keeps track of the number of connections and time of the last one.
        """
        self.ncon_lock.acquire()
        self.ncon += val
        self.last_con_time = time.time()
        self.ncon_lock.release()

    def check_timeout(self):
        """
        Checks if timeout happened and shutdowns server if it did.
        """
        while True:
            if self.ncon == 0:
                idle_time = time.time() - self.last_con_time
                if idle_time < self.timeout:
                    time.sleep(self.timeout - idle_time)
                else:
                    logging.debug("exiting ppserver due to timeout (no client"
                                  " connections in last %i sec)", self.timeout)
                    os._exit(0)
            else:
                time.sleep(self.timeout)

    def listen(self):
        """
        Initiates listenting to incoming connections.
        """
        try:
            ssocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            # following allows ppserver to restart faster on the same port
            ssocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            ssocket.bind((self.host, self.port))
            ssocket.listen(5)
        except socket.error:
            logging.error("Cannot create socket with port " + str(self.port)
                          + " (port is already in use)")

        try:
            while True:
                # accept connections from outside
                (csocket, address) = ssocket.accept()
                # now do something with the clientsocket
                # in this case, we'll pretend this is a threaded server
                six.moves._thread.start_new_thread(self.crun, (csocket, ))
        except:
            logging.debug("Closing server socket")
            ssocket.close()

    def crun(self, csocket):
        """
        Authenticates client and handles its jobs.
        """
        mysocket = pptransport.CSocketTransport(csocket)
        # send PP version
        mysocket.send(version)
        # generate a random string
        srandom = "".join([random.choice(string.ascii_letters)
                           for i in range(16)])
        mysocket.send(srandom)
        answer = sha_new(srandom + self.secret).hexdigest()
        cleintanswer = mysocket.receive()
        if answer != cleintanswer:
            logging.warning("Authentification failed, client host=%s, port=%i"
                            % csocket.getpeername())
            mysocket.send("FAILED")
            csocket.close()
            return
        else:
            mysocket.send("OK")

        ctype = mysocket.receive()
        logging.debug("Control message received: " + ctype)
        self.ncon_add(1)
        try:
            if ctype == "STAT":
                # reset time at each new connection
                self.get_stats()["local"].time = 0.0
                mysocket.send(str(self.get_ncpus()))
                while True:
                    mysocket.receive()
                    mysocket.send(str(self.get_stats()["local"].time))
            elif ctype == "EXEC":
                while True:
                    sfunc = mysocket.receive()
                    sargs = mysocket.receive()
                    fun = self.insert(sfunc, sargs)
                    sresult = fun(True)
                    mysocket.send(sresult)
        except:
            logging.debug("Closing client socket")
            csocket.close()
            self.ncon_add(-1)

    def broadcast(self):
        """
        Initiaates auto-discovery mechanism.
        """
        discover = ppauto.Discover(self)
        six.moves._thread.start_new_thread(discover.run,
                                ((self.host, self.port),
                                 (self.bcast, self.port)),
                                )


def parse_config(file_loc):
    """
    Parses a config file in a very forgiving way.
    """
    # If we don't have configobj installed then let the user know and exit
    try:
        from configobj import ConfigObj
    except ImportError as ie:
        print("ERROR: You must have configobj installed to use \
configuration files. You can still use command line switches.", file=sys.stderr)
        sys.exit(1)

    if not os.access(file_loc, os.F_OK):
        print("ERROR: Can not access %s." % arg, file=sys.stderr)
        sys.exit(1)

    # Load the configuration file
    config = ConfigObj(file_loc)
    # try each config item and use the result if it exists. If it doesn't
    # then simply pass and move along
    try:
        args['secret'] = config['general'].get('secret')
    except:
        pass

    try:
        autodiscovery = config['network'].as_bool('autodiscovery')
    except:
        pass

    try:
        args['interface'] = config['network'].get('interface',
                                                  default="0.0.0.0")
    except:
        pass

    try:
        args['broadcast'] = config['network'].get('broadcast')
    except:
        pass

    try:
        args['port'] = config['network'].as_int('port')
    except:
        pass

    try:
        args['loglevel'] = config['general'].as_bool('debug')
    except:
        pass

    try:
        args['ncpus'] = config['general'].as_int('workers')
    except:
        pass

    try:
        args['proto'] = config['general'].as_int('proto')
    except:
        pass

    try:
        args['restart'] = config['general'].as_bool('restart')
    except:
        pass

    try:
        args['timeout'] = config['network'].as_int('timeout')
    except:
        pass
    # Return a tuple of the args dict and autodiscovery variable
    return args, autodiscovery


def print_usage():
    """
    Prints help.
    """
    print("Parallel Python Network Server (pp-" + version + ")")
    print("Usage: ppserver.py [-hdar] [-n proto] [-c config_path]"\
        " [-i interface] [-b broadcast] [-p port] [-w nworkers]"\
        " [-s secret] [-t seconds]")
    print()
    print("Options: ")
    print("-h                 : this help message")
    print("-d                 : debug")
    print("-a                 : enable auto-discovery service")
    print("-r                 : restart worker process after each"\
        " task completion")
    print("-n proto           : protocol number for pickle module")
    print("-c path            : path to config file")
    print("-i interface       : interface to listen")
    print("-b broadcast       : broadcast address for auto-discovery service")
    print("-p port            : port to listen")
    print("-w nworkers        : number of workers to start")
    print("-s secret          : secret for authentication")
    print("-t seconds         : timeout to exit if no connections with "\
        "clients exist")
    print()
    print("Due to the security concerns always use a non-trivial secret key.")
    print("Secret key set by -s switch will override secret key assigned by")
    print("pp_secret variable in .pythonrc.py")
    print()
    print("Please visit http://www.parallelpython.com for extended up-to-date")
    print("documentation, examples and support forums")


if __name__ == "__main__":
    try:
        opts, args = getopt.getopt(sys.argv[1:],
                                   "hdarn:c:b:i:p:w:s:t:", ["help"])
    except getopt.GetoptError:
        print_usage()
        sys.exit(1)

    args = {}
    autodiscovery = False

    for opt, arg in opts:
        if opt in ("-h", "--help"):
            print_usage()
            sys.exit()
        elif opt == "-c":
            args, autodiscovery = parse_config(arg)
        elif opt == "-d":
            args["loglevel"] = logging.DEBUG
        elif opt == "-i":
            args["interface"] = arg
        elif opt == "-s":
            args["secret"] = arg
        elif opt == "-p":
            args["port"] = int(arg)
        elif opt == "-w":
            args["ncpus"] = int(arg)
        elif opt == "-a":
            autodiscovery = True
        elif opt == "-r":
            args["restart"] = True
        elif opt == "-b":
            args["broadcast"] = arg
        elif opt == "-n":
            args["proto"] = int(arg)
        elif opt == "-t":
            args["timeout"] = int(arg)

    server = _NetworkServer(**args)
    if autodiscovery:
        server.broadcast()
    server.listen()
    # have to destroy it here explicitelly otherwise an exception
    # comes out in Python 2.4
    del server

# Parallel Python Software: http://www.parallelpython.com
