#!/usr/bin/python

# tz_multi_client_web_server.py
#       --copyright--                   Copyright 2011 (C) Tranzoa, Co. All rights reserved.    Warranty: You're free and on your own here. This code is not necessarily up-to-date or of public quality.
#       --url--                         http://www.tranzoa.net/tzpython/
#       --email--                       pycode is the name to send to. tranzoa.com is the place to send to.
#       --bodstamps--
#       November 16, 2011       bar
#       November 19, 2011       bar     macro_data param
#                                       pass log routine to macro processor
#                                       make the default main page "index.htm"
#                                       update client when whenever anything is sent to it OK
#                                       don't log errors on pull - remote disconnects
#       November 20, 2011       bar     change "restart" to "lobotomize"
#                                       charset, defaults to utf8
#       November 22, 2011       bar     read multiple 'json' args from clients
#       November 29, 2011       bar     pyflake cleanup
#       December 10, 2011       bar     put the client name in messages, too
#       December 14, 2011       bar     ah. module name with the ARG names
#       March 20, 2012          bar     set_macro_data()
#       March 22, 2012          bar     be cleaner about flight's tzjavascript and tzpython
#       March 25, 2012          bar     electro_shock_all_clients()
#       March 27, 2012          bar     forget_old_clients()
#       March 28, 2012          bar     macro expand the stuff that's pushed to the browser
#       May 5, 2012             bar     send_hit_path() subroutine
#                                       magic mime type in file
#                                       byte ranges on macroed files
#       May 7, 2012             bar     get the default language dir from the macro processor
#       May 8, 2012             bar     allow caller to spec the empty file name (index.htm)
#       May 27, 2012            bar     doxygen namespace
#       March 26, 2013          bar     unused MAX_VIDEO_RANGE_LEN
#       August 20, 2013         bar     in case the macros come out with unicode, convert them back to utf8 for shoving to the clients
#       November 7, 2017        bar     maxint->maxsize
#       --eodstamps--
##      \file
#       \namespace              tzpython.tz_multi_client_web_server
#
#
#       This is a web server that services mulitple clients, all of whom can control the main program
#       and all of whom, presumably, want to see the same thing.
#
#       It uses the tz_web_server.py logic's 'ci' (<FORM> in other uses) value inside client javascript to distinguish clients.
#       It uses the json_to_browser.js client logic to get json from the server to the client. (This logic establishes a long-lasting xmlhttp connection through which json is streamed as it is generated on the server.)
#       It uses the json_to_server.js  client logic to get json from the client to the server.
#       It has a facility to completely refresh all the clients and restart the json stream to those clients.
#       It has a facility to clear the json output stream (presumably with the first thing sent after the reset being javascript code to get the client back to the known, reset state).
#       It uses the tz_web_server_macro.py logic to deliver content to the client and to help define where to find the content on the server machine.
#       During normal operation, it gets json to stream to the clients from its input "q" and puts un-json'd data structures out through the output queue (which may be passed in to it from the main program).
#       It has a facilty for setting the language directory so that language-dependent content can be gotten from the right place.
#
#

import  mimetypes
import  os
import  Queue
import  re
import  socket
import  sys
import  threading
import  time
from    types   import  UnicodeType

import  tzlib
import  json_to_from_browser
import  tz_http_server
import  tz_web_server
import  tz_web_server_macro
ld  = os.path.expanduser("~/flight/tzjavascript")
if  os.path.isdir(ld) and ld.startswith('/home/alex') :
    tz_web_server_macro.lib_paths.insert(0, ld)             # bar: make life easy for me
ld  = os.path.expanduser("~/flight/tzpython")
if  os.path.isdir(ld) and ld.startswith('/home/alex') :
    tz_web_server_macro.lib_paths.insert(0, ld)



HTTP_PORT       = 13100                             # default http port number

DATA_DIR        = "./data"                          # where the system stores data (where we can deliver .png files from)
LANG_DIR        = tz_web_server_macro.LANG_DIR      # default language directory

CLIENT_TIMEOUT  = 60 * 60                           # default number of seconds we must hear from a client before whacking him, if asked

EMPTY_NAME      = os.path.splitext(os.path.basename(__file__))[0] + ".htm"
if  not os.path.isfile(EMPTY_NAME) :
    EMPTY_NAME  = "index.htm"                       # the equivalent of index.html / index.htm
PULL_PATH       = "pull"                            # client asks for json
PUSH_PATH       = "push"                            # client sends us json


MAX_VIDEO_RANGE_LEN = 0                             # 10000000  # we can't deliver any more than this of a video file that's asked for by range. Note! The FF video player gets confused about how long the video is, if nothing else. And it's not "nothing else".


class   a_client(tz_web_server.a_client) :
    def __init__(me, hit) :
        super(a_client, me).__init__(hit)
        me.sig  = threading.Event()
        me.qi   = 0
    #   a_client



class   a_server(tz_web_server.a_client_server) :

    RELOAD_MAIN_PAGE_MSG        = { 'eval' : 'getter.stop();  putter.stop();  getter.ignore_errs();  putter.ignore_errs();  window.location = "/";', }


    def __init__(me, empty_file_name = None, data_dir = None, lang_dir = None, iq = None, macro_data = None, show_exceptions = True, *args, **kwargs) :
        kwargs['http_port'] = kwargs.get('http_port', HTTP_PORT) or HTTP_PORT
        super(a_server, me).__init__(show_exceptions = show_exceptions, *args, **kwargs)
        me.clients.a_client = a_client                                  # tell the server to use our super class to make new clients

        me.empty_file_name  = empty_file_name   or  EMPTY_NAME
        me.data_dir         = data_dir          or  DATA_DIR
        me.lang_dir         = lang_dir          or  tz_web_server_macro.LANG_DIR
        me.iq               = iq                or  Queue.Queue()
        me.macro_data       = macro_data        or  {}

        me.lock             = threading.RLock()
        me.oq               = []                                    # array of all wrapped json (or None) that must go to clients, starting with when they are new (presumably to take them to current reality from the main page)
        me.cl_hitcnt        = 0


    def language_dir(me, lang_dir = None) :
        """ Set/get the current language directory. Files for the clients/user-agents are delivered from the current language directory, if found, before they are sought in other, more generic, directories. """

        ov  = me.lang_dir
        if  lang_dir   != None :
            me.lang_dir = lang_dir
        return(ov)


    def set_macro_data(me, macro_data = None) :
        ov                  = me.macro_data
        if  macro_data     != None :
            me.macro_data   = macro_data
        return(ov)


    def put_to_clients(me, msg = None) :
        """ Put the given message (dictionary) or javascript string to the clients. """

        me.lock.acquire()
        if  msg :
            if  not isinstance(msg, basestring) :
                msg = json_to_from_browser.make_json(msg)
            me.oq.append(msg)
        for cl in me.clients.values() :
            cl.sig.set()                                    # get the client out of wait state if it is waiting
        me.lock.release()


    def reload_all_clients(me, msg = None) :
        """ Cause all the clients to reload the main page. """

        me.lock.acquire()
        me.oq       = []                                    # start from scratch
        for cl in me.clients.values() :
            cl.qi   = sys.maxsize                           # special thing that will trigger the reload in logic in PULL_PATH handler
        me.put_to_clients(msg or " ")                       # wake up all the clients
        me.lock.release()


    def electro_shock_all_clients(me) :
        """ Wipe the memory of all messages at the price of new clients not being up to date. """

        me.lock.acquire()
        me.oq       = []                                    # all old messages are in-op
        for cl in me.clients.values() :
            cl.qi   = 0                                     # start all the clients at the start of the old messages, of which there are none, now
        me.lock.release()


    def lobotomize_all_clients(me, msg = None) :
        """ Start all the clients' output with the given msg (which presumably tells the client to get back to the main page <div> inside itself) """

        me.lock.acquire()
        me.oq       = []                                    # all old messages are in-op
        for cl in me.clients.values() :
            cl.qi   = 0                                     # start all the clients at the start of the old messages, of which there are none, now
        me.put_to_clients(msg or " ")
        me.lock.release()



    def forget_old_clients(me, timeout = None) :
        timeout = timeout or CLIENT_TIMEOUT
        t       = tzlib.elapsed_time()
        me.lock.acquire()
        for cl in me.clients.values() :
            if  t - cl.get_when() >= timeout :
                me.clients.delete_client(cl)
            pass
        me.lock.release()



    def send_hit_path(me, hit, path, err_ok = False) :
        """
            Send a macro-expanded file, with an option to send it even if there are macro errors.
            Return True if the file is sent, False otherwise.
        """

        macroer     = tz_web_server_macro.a_processor(data_dir = me.data_dir, lang_dir = me.lang_dir, srv = me, hit = hit, log_rtn = me.log, **me.macro_data)       # note: 'srv' and 'hit' are added to the macro data so me.srv and me.hit are available to the exec code
        fd          = macroer.safe_load_file(path)
        if  fd and (err_ok or (not macroer.err)) :                      # we may require that the file macro-expands without error - that we we know that the file is there and that it doesn't need a client ID (.js and .css and .png files, for instance)
            g       = re.search(r"\bMimeType:\s+(\S+)", fd)             # special magic string in an xul file to change it to be firefox chrome - to take a random for-instance.
            if  g   :
                ct  = g.group(1)
            else    :
                ct  = mimetypes.guess_type(tz_http_server.safe_path(path), strict = False)[0]
            if  ct  :
                if  ct.startswith('text') and (ct.find('charset') < 1) :
                    ct += ("; charset=%s" % me.charset)

                hdrs    = { 'content-length' : len(fd), }
                rsp     = 200
                rng     = hit.handler.headers.getheader('range', '')
                g       = re.search("^\s*bytes\s*=\s*(\d+)\s*-\s*(\d+)?", rng)
                if  g   :
                    b   = max(0, int(g.group(1)))
                    e   = len(fd) - 1
                    if  (ct.find('video') >= 0) and MAX_VIDEO_RANGE_LEN :
                        e   = min(e, b + MAX_VIDEO_RANGE_LEN)
                    if  g.lastindex > 1 :
                        e   = min(e, int(g.group(2)))
                    hdrs['content-range']   = "bytes %u-%u/%u" % ( b, e, len(fd) )
                    fd  = fd[b : e + 1]
                    hdrs['content-length']  = len(fd)
                    rsp = 206

                if  hit.handler.send_http_headers(hit.is_head, response = rsp, content_type = ct, hdrs = hdrs) :
                    hit.handler.write_data(fd)

                return(True)

            pass
        pass

        return(False)



    def generic_hit(me, hit) :
        """ Deliver simple files and such-like before caring about the client number. """

        if  hit.handler.file_only_lc and (hit.handler.file_only_lc not in {  PULL_PATH : 1, PUSH_PATH : 1, }) :
            if  me.send_hit_path(hit, hit.handler.path) :
                return(True)

            pass

        return(super(a_server, me).generic_hit(hit))                    # do local-directory favicon



    def write_new_client(me, hit) :
        """ This is a new client's first hit for anything. Give 'em the main page."""
        hit.handler.write_data(tz_web_server_macro.a_processor(data_dir = me.data_dir, lang_dir = me.lang_dir, srv = me, hit = hit, log_rtn = me.log, **me.macro_data).read_and_expand_file(me.empty_file_name))


    def new_client_hit(me, hit) :
        """ Our server thinks this is a new client hitting us for something. """

        if  hit.ci in me.clients :
            return(me.client_hit(hit))

        path    = hit.handler.file_only_lc

        if  path not in { "" : 1,   "/" : 1,    me.empty_file_name : 1, PULL_PATH : 1, PUSH_PATH : 1, } :
            return(False)

        me.learn_new_client(hit)

        if  (path == PULL_PATH) or (path == PUSH_PATH) :
            if  hit.server.send_http_headers(hit.handler, is_head = hit.is_head, content_type = "text/javascript; charset=%s" % me.charset) :
                s       = me.RELOAD_MAIN_PAGE_MSG
                if  path == PULL_PATH :
                    s   = json_to_from_browser.json_to_browser_str(s)
                else    :
                    s   = json_to_from_browser.make_json(s)
                hit.handler.write_data(s)                               # tell a client we don't know about to reload itself so that it can get a new client number, etc.
            return(True)

        if  hit.server.send_http_headers(hit.handler, is_head = hit.is_head, content_type = 'text/html; charset=%s' % me.charset) :
            me.write_new_client(hit)

        return(True)


    def client_hit(me, hit) :
        """ Deliver our EMPTY_NAME and PULL_PATH and PUSH_PATH urls. That is, respond to the client's json communications or deliver macro-expanded files. """

        if  hit.ci not in me.clients :
            if  me.new_client_hit(hit) :
                return(True)
            return(me.final_hit(hit))

        hit.cl.set_when()                                               # note: probably redundant because they are done in pre_process_hit() if the client was known then and it was called
        hit.cl.set_ipadr(hit.handler.client_address[0])

        path        = hit.handler.file_only_lc

        if  path   == PULL_PATH :
            if  hit.handler.send_http_headers(hit.is_head, content_type = 'text/javascript; charset=%s' % me.charset) :
                macroer             = tz_web_server_macro.a_processor(data_dir = me.data_dir, lang_dir = me.lang_dir, srv = me, hit = hit, log_rtn = me.log, **me.macro_data)
                me.lock.acquire()
                me.cl_hitcnt       += 1
                me.lock.release()
                ocnt                = 0
                while True :
                    if  ocnt        > json_to_from_browser.MAX_OUT_IN_ONE_CONNECTION :
                        # print "@@@@", hit.cl.qi, len(me.oq), sum([ len(s) for s in me.oq ]), sum([ len(s) for s in me.oq[hit.cl.qi:] ]), me.cl_hitcnt
                        break

                    me.lock.acquire()
                    if  hit.cl.qi   > len(me.oq) :                      # other logic sets the client's output queue index to a really high number to tell this client to reload the main page
                        me.lock.release()
                        s           = json_to_from_browser.json_to_browser_str(me.RELOAD_MAIN_PAGE_MSG)
                        hit.handler.wfile.write(s)
                        break
                    oq              = me.oq[hit.cl.qi:]
                    hit.cl.qi      += len(oq)
                    me.lock.release()

                    stp             = False
                    so              = ""
                    for s in oq     :
                        if  s is None :
                            stp     = True
                            break                                       # someone wants us to get out of here (we're shutting down so forget everything)
                        s           = macroer.expand_macros(s)
                        if  isinstance(s, UnicodeType) :
                            s       = s.encode('utf8')
                        so         += json_to_from_browser.json_to_browser_wrap(s)
                    ocnt           += len(so)
                    # print "@@@@ json sending", so
                    try :
                        hit.handler.wfile.write(so)
                        hit.handler.wfile.flush()
                    except ( socket.error, socket.herror, socket.gaierror, socket.timeout ) :
                        me.lock.acquire()
                        me.cl_hitcnt   -= 1
                        me.lock.release()
                        return(True)

                    hit.cl.set_when()
                    if  stp         :
                        break

                    hit.cl.sig.wait()
                    hit.cl.sig.clear()

                me.lock.acquire()
                me.cl_hitcnt       -= 1
                me.lock.release()
                hit.handler.wfile.write(' ')                            # note: raise exception if the client is disconnected
            return(True)

        if  path   == PUSH_PATH :
            for qi in xrange(len(hit.args.get('json', []))) :
                json    = hit.args.get('json', [ '' ])[qi]
                dt      = json_to_from_browser.unmake_json(json)
                if  dt  :
                    if  hasattr(dt, 'keys') :
                        dt[tz_web_server.CI_ARG]    = hit.ci            # if the json is a dict, pass the client ID in the message if for no other reasons than it tags the source of the msg
                        dt[tz_web_server.CN_ARG]    = hit.cl.name
                    me.iq.put(dt)                                       # send a message to the UI, which must be robust in the face of bad messages
                pass
            if  hit.handler.send_http_headers(hit.is_head, content_type = 'text/javascript') :
                hit.handler.write_data("")
            return(True)

        if  path in { "" : 1,   "/" : 1,    me.empty_file_name : 1, EMPTY_NAME : 1, } :
            path    = me.empty_file_name                                # he gave no URL, go with the default html file

        if  me.send_hit_path(hit, path, err_ok = True) :
            return(True)

        me.log("Failed to send the browser path:               [%s] args:[%s]!" % ( tz_http_server.safe_path(hit.handler.path), str(hit.args) ) )

        return(super(a_server, me).final_hit(hit))                      # 404 the client


    def final_hit(me, hit) :
        """ The client hasn't been handled. Bail or something. """

        me.log("Failed to send the browser path to new client: [%s] args:[%s]!" % ( tz_http_server.safe_path(hit.handler.path), str(hit.args) ) )
        return(super(a_server, me).final_hit(hit))                      # 404 the client


    #   a_server




if  __name__ == '__main__' :
    class   a_logger(object) :
        def write(me, li = "", flush = None, quiet = None) :
            print li
        log = write
        def flush(me) :
            pass
        #   a_logger

    me  = a_server(verbose = 2, logger = a_logger())
    me.start()
    try :
        while   True :
            time.sleep(0.1)
        pass
    except KeyboardInterrupt :
        pass
    me.stop()

    exit(1)


#
#
# eof
