#       m   
#      u    _backend.py - Sun Aug 12 17:46 CEST 2012
#  SQLite   back-end of network connection
#    d      part of sqmediumlite
#   e       copyright (C): nobody
#  m        

"""
Back-end process for the network interface. The main thread of
this process waits for connections over a TCP socket interface.
Connections are further handled in seperate threads. The process 
is started through the front-end module and may run either as a
service in Windows or as s detached process in Unix.
"""

from os import walk
from os.path import isabs
from threading import Thread, ThreadError, Lock, enumerate
from sqmedium._common import Error, ServerNotUp, PicklingError, \
        Socket, e_str, print23, version
try: # try use APSW
    from sqmedium.apswdbapi2 import __name__ as wrapper_name, \
        Connection, Cursor, NotSupportedError, Error as sqliteError, \
        complete_statement, enable_shared_cache, sqlite_version, \
        version as apsw_version
except ImportError: # use standard Python sqlite3 (Pysqlite)
    from sqlite3 import __name__ as wrapper_name, \
        Connection, Cursor, NotSupportedError, Error as sqliteError, \
        complete_statement, enable_shared_cache, sqlite_version, \
        version as apsw_version

# configurable attributes
try: from sqmediumconf import port
except ImportError: from sqmedium._conf import port
try: from sqmediumconf import initsql
except ImportError: from sqmedium._conf import initsql
try: from sqmediumconf import cachesharing
except ImportError: from sqmedium._conf import cachesharing
try: from sqmediumconf import allowedhosts
except ImportError: from sqmedium._conf import allowedhosts
else: import re

# Connection thread
class Bconnection (Connection, Thread):
    _unrecyclable = None
    filename = None
    def __init__ (self, fname, kkvv):
        self.filename = fname # not exposed by Pysqlite
        self._kkvv = kkvv
        checkrelative (fname)
        if "check_same_thread" in kkvv and kkvv.pop ("check_same_thread"):
            raise NotSupportedError ("check_same_thread can not be True")
        Connection.__init__ (self, fname, check_same_thread=False, **kkvv)
        Thread.__init__ (self)
        self.set_authorizer (self._authorizer) # sql check
        self._pause = Lock ();
        self._pause.acquire ()
        self._cursor = Cursor (self) # single cursor for all work
        if initsql:
            self.executescript (initsql)
    def __enter__ (self): # does not return self (=>PicklingError)
        Connection.__enter__ (self)
    def execute (self, *args):
        return self._cursor.execute (*args).description, \
                self._cursor.fetchall()
    def executemany (self, *args):
        return self._cursor.executemany (*args).description, \
                self._cursor.fetchall()
    def executescript (self, *args):
        if wrapper_name == "sqlite3": # Pysqlite issues
            if args [1:] and not args [1]: # parameters=None
                args = args [:1]
            if not args [0].endswith (';'):
                args = (args [0] + ';',) + args [1:]
        return self._cursor.executescript (*args).description, \
                self._cursor.fetchall()
    def setbusytimeout (self, busytimeout): # APSW only
        Connection.setbusytimeout (self, busytimeout)
        self._unrecyclable = True
    def last_insert_rowid (self):
        return self._cursor.lastrowid
    def changes (self):
        return self._cursor.rowcount
    def modget (self, k):
        " get module-level attribute "
        return globals() [k]
    def moddo (self, meth, *args):
        " do module-level request "
        return globals() [meth] (*args)
    def modstatus (self, pooled=None):
        " return list of connections "
        return dict ((i.name, (i.filename, i._kkvv, i._socket and i._socket.sgetpeername ()[0]))
                for i in connections (pooled))
    def modstop (self): # from front-end
        if self._socket.sgetpeername ()[0] != "127.0.0.1":
            raise Error ("Should only stop from local host")
        self._socket.close () # stop at next cycle 
        mainsocket.close () # stop listener at next cycle
        Socket ().sconnect (("127.0.0.1", port)).close() # wake up lsnr
    def _authorizer(self, op, p1, p2, p3, p4):
        " only called when SQL statement is compiled (OK for me) "
        if op == 21: # SQLITE_SELECT
            pass
        elif op in (2, 8, 29): # SQLITE_CREATE_TABLE / VIEW / VTABLE
            if self.filename in ("", ":memory:"):
                self._unrecyclable = True
        elif op == 19: # SQLITE_PRAGMA:
            if p2 is not None: # if not querying
                if self.is_alive (): # and not initsql
                    self._unrecyclable = True
        elif op == 24: # SQLITE_ATTACH:
            checkrelative (p1)
            self._unrecyclable = True
        return 0 # SQLITE_OK
    def run (self): # override Thread.run
        while True: # while recyclable
            try:
                while True: # until socket closed
                    req = self._socket.srecv ()
                    try:
                        ret = getattr (self, req [0]) (*req [1:])
                        self._socket.ssend ((ret, None))
                    except Exception as e:
                        self._socket.ssend ((None, dumperr (e)))
            except ServerNotUp: # closed at front-end
                pass
            if self._unrecyclable:
                break
            self._socket = None # become pooled 
            self.rollback ()
            self._pause.acquire ()
            if not self._socket: # purged
                break
        self._cursor.close ()
        self.close ()

# module-level methods called from front-end via moddo
def interrupt (name):
    for i in connections (False):
        if i.name == name:
            i.interrupt ()
            break
    else:
        raise Error ("no such connection: " + str (name))
def dir (path="."):
    checkrelative (path)
    for i in walk (path):
        return i [1] + i [2] # list of direcories and files

# internal module-level methods
def connections (pooled=None):
    return [i for i in enumerate ()
            if type (i) is Bconnection and \
                bool (i._socket or i._unrecyclable) is not pooled]
def dumperr (e):
    return e.__class__.__name__, e.args
def checkrelative (s):
    if ".." in s or isabs (s):
        raise Error ("db name not relative: %s" % (s,))

# main loop
def main ():
    global mainsocket
    mainsocket = Socket ().sbindlisten (("0.0.0.0", port))
    enable_shared_cache (cachesharing)
    try:
        while True: # until listener socket closed
            socket = mainsocket.saccept ()
            try:
                if allowedhosts and not re.match (
                        "127.0.0.1|" + allowedhosts, socket.sgetpeername ()[0]):
                    socket.close () # not allowed
                    continue
                # do first request (=connect) in main thread
                fname, kkvv = socket.srecv ()
                # try to get a connection from pool
                pool = connections (True)
                for i in pool:
                    if i.filename == fname and i._kkvv == kkvv:
                        final_target = i._pause.release
                        break
                else: # create new connection
                    i = Bconnection (fname, kkvv)
                    final_target = i.start
                    if len (pool) > 2: # if the pool grows to large
                        pool [0]._unrecyclable = True
                        pool [0]._pause.release ()
                socket.ssend ((i.name, None))
            except Exception as e: # connect failed or socker closed
                try: socket.ssend ((None, dumperr (e)))
                except ServerNotUp: pass
            else:
                i._socket = socket
                final_target ()
    except ServerNotUp: # listener closed
        pass
    except Exception as e:
        print23 ("main loop error " + e_str (e))
        ### from traceback import print_exc; print_exc () 
        mainsocket.close ()
    # close all connections
    for i in connections (): # pooled or not
        i._unrecyclable = True
        try:
            i._pause.release () # may be pooled
            i.interrupt () # may be executing
            i._socket.sshutdown () # interrupts recv (on unix)
            i._socket.close ()
        except (ThreadError, sqliteError, AttributeError, ServerNotUp):
            pass # likely closed in thread
    for i in connections ():
        i.join (timeout = 7.5) # wait socket timeout on windows
        if i.isAlive (): print23 ("failed to join %s" % i)

if __name__ == "__main__":
    main ()