# Copyright (c) 2000 Phil Thompson <phil@river-bank.demon.co.uk>


import sys
import socket
import select
import codeop
import traceback
import bdb
import os
from qt import PYSIGNAL

from DebugProtocol import *
from AsyncIO import *


DebugClientInstance = None

def DebugClientQAppHook():
    """Called by PyQt when the QApplication instance has been created.

    """
    if DebugClientInstance is not None:
        AsyncIO.setNotifiers(DebugClientInstance)
 
# Make the hook available to PyQt.
__builtins__.__dict__['__pyQtQAppHook__'] = DebugClientQAppHook
 

class DebugClient(AsyncIO,bdb.Bdb):
    """DebugClient(self)

    Provide access to the Python interpeter from a debugger running in another
    process whether or not the Qt event loop is running.

    The protocol between the debugger and the client assumes that there will be
    a single source of debugger commands and a single source of Python
    statements.  Commands and statement are always exactly one line and may be
    interspersed.

    The protocol is as follows.  First the client opens a connection to the
    debugger and then sends a series of one line commands.  A command is either
    >Load<, >Step<, >StepInto< or a Python statement.

    The response to >Load< is...

    The response to >Step< and >StepInto< is...

    A Python statement consists of the statement to execute, followed (in a
    separate line) by >OK?<.  If the statement was incomplete then the response
    is >Continue<.  If there was an exception then the response is >Exception<.
    Otherwise the response is >OK<.  The reason for the >OK?< part is to
    provide a sentinal (ie. the responding >OK<) after any possible output as a
    result of executing the command.

    The client may send any other lines at any other time which should be
    interpreted as program output.

    If the debugger closes the session there is no response from the client.
    The client may close the session at any time as a result of the script
    being debugged closing or crashing.

    """
    def __init__(self):
        AsyncIO.__init__(self)
        bdb.Bdb.__init__(self)

        # The context to run the debugged program in.
        self.context = {'__name__' : '__main__'}

        # The list of complete lines to execute.
        self.buffer = ''

        self.connect(self,PYSIGNAL('lineReady'),self.handleLine)
        self.connect(self,PYSIGNAL('gotEOF'),self.sessionClose)

        self.pendingResponse = ResponseOK
	self.fncache = {}

        # So much for portability.
        if sys.platform == 'win32':
            self.skipdir = sys.prefix
        else:
	    self.skipdir = os.path.join(sys.prefix,'lib/python' + sys.version[0:3])

    def handleException(self):
        """
        Private method to handle the response to the debugger in the event of
        an exception.
        """
        self.pendingResponse = ResponseException

    def sessionClose(self):
        """
        Private method to close the session with the debugger and terminate.
        """
        try:
            self.set_quit()
        except:
            pass

        self.disconnect()
        sys.exit()

    def handleLine(self,line):
        """
        Private method to handle a complete line by trying to execute the lines
        accumulated so far.
        """
        eoc = line.find('<')

        if line[0] == '>' and eoc >= 0:
            # Get the command part and any argument.
            cmd = line[:eoc + 1]
            arg = line[eoc + 1:-1]

            if cmd == RequestOK:
                self.write(self.pendingResponse + '\n')
                self.pendingResponse = ResponseOK
                return

            if cmd == RequestLoad:
                self.fncache = {}
                sys.argv = arg.split()
        	sys.path[0] = os.path.dirname(sys.argv[0])
                self.running = sys.argv[0]
                self.firstFrame = None

                # This will eventually enter a local event loop.
                self.run('execfile("%s")' % (self.running),self.context)
                return

            if cmd == RequestStep:
                self.set_step()
                self.eventExit = 1
                return

            if cmd == RequestContinue:
                self.set_continue()
                self.eventExit = 1
                return

            if cmd == RequestBreak:
                fn, line, set = arg.split(',')
                line = int(line)
                set = int(set)

                if set:
                    self.set_break(fn,line)
                else:
                    self.clear_break(fn,line)

                return

        #XXX - how do we handle a program reading from stdin?

        # Remove any newline.
        if line[-1] == '\n':
            line = line[:-1]

        if self.buffer:
            self.buffer = self.buffer + '\n' + line
        else:
            self.buffer = line

        try:
            code = codeop.compile_command(self.buffer,sys.stdin.name)
        except (OverflowError, SyntaxError):
            #XXX - need to decide what to do here. Will probably break the
            #exception down and pass each value back separately (eg. message,
            #line number, position in line) for the debugger to use - eg. to
            #highlight the bad text in the source file, or to load an editor at
            #the right point.
            sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
            map(self.write,traceback.format_exception_only(sys.last_type,sys.last_value))
            self.buffer = ''

            self.handleException()
        else:
            if code is None:
                self.pendingResponse = ResponseContinue
            else: 
                self.buffer = ''

                try:
                    exec code in self.context
                except SystemExit:
                    self.sessionClose()
                except:
                    #XXX - similar comments to above.
                    try:
                        type, value, tb = sys.exc_info()
                        sys.last_type = type
                        sys.last_value = value
                        sys.last_traceback = tb
                        tblist = traceback.extract_tb(tb)
                        del tblist[:1]
                        list = traceback.format_list(tblist)
                        if list:
                            list.insert(0, "Traceback (innermost last):\n")
                            list[len(list):] = traceback.format_exception_only(type, value)
                    finally:
                        tblist = tb = None

                    map(self.write,list)

                    self.handleException()


    def write(self,s):
        sys.stdout.write(s)
        sys.stdout.flush()

    def interact(self):
        """
        Interact with whatever is on stdin and stdout - usually the debugger.
        This method will never terminate.
        """
        # Set the descriptors now and the notifiers when the QApplication
        # instance is created.
        global DebugClientInstance

        self.setDescriptors(sys.stdin,sys.stdout)
        DebugClientInstance = self

        # At this point the Qt event loop isn't running, so simulate it.
        self.eventLoop()

    def eventLoop(self):
        self.eventExit = None

        while self.eventExit is None:
            wrdy = []

            if AsyncPendingWrite(sys.stdout):
                wrdy.append(sys.stdout)

            #if AsyncPendingWrite(sys.stderr):
                #wrdy.append(sys.stderr)

            rrdy, wrdy, xrdy = select.select([sys.stdin],wrdy,[])

            if sys.stdin in rrdy:
                self.readReady(sys.stdin.fileno())

            if sys.stdin in wrdy:
                self.writeReady(sys.stdout.fileno())

            if sys.stderr in wrdy:
                self.writeReady(sys.stderr.fileno())

        self.eventExit = None

    def connectDebugger(self,port):
        """
        Establish a session with the debugger and connect it to stdin, stdout
        and stderr.
        """
        sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        sock.connect((DebugAddress,port))
        sock.setblocking(0)

        sys.stdin = AsyncFile(sock,sys.stdin.mode,sys.stdin.name)
        sys.stdout = AsyncFile(sock,sys.stdout.mode,sys.stdout.name)
        #sys.stderr = AsyncFile(sock,sys.stderr.mode,sys.stderr.name)

    def user_line(self,frame):
        """
        Re-implemented to handle the program about to execute a particular
        line.
        """
        line = frame.f_lineno

	# We never stop an line 0.
        if line == 0:
            return

        fn = self.absPath(frame.f_code.co_filename)

        # See if we are skipping at the start of a newly loaded program.
        if self.firstFrame is None:
            if fn != self.running:
                return

            self.firstFrame = frame

        self.write('%s%s,%d\n' % (ResponseLine,fn,line))
        self.eventLoop()

    def user_exception(self,frame,(exctype,excval,exctb)):
        if exctype == SystemExit:
            self.progTerminated(excval)

    def user_return(self,frame,retval):
        # The program has finished if we have just left the first frame.
        if frame == self.firstFrame:
            self.progTerminated(retval)

    def stop_here(self,frame):
        """
        Re-implemented to turn off tracing for files that are part of the
        debugger that are called from the application being debugged.
        """
        fn = frame.f_code.co_filename

        # Eliminate things like <string> and <stdin>.
        if fn[0] == '<':
            return 0

        #XXX - think of a better way to do this.  It's only a convience for
	#debugging the debugger - when the debugger code is in the current
	#directory.
        if os.path.basename(fn) in ['AsyncIO.py', 'DebugClient.py']:
            return 0

        # Eliminate anything that is part of the Python installation.
        #XXX - this should be a user option, or an extension of the meaning of
        # 'step into', or a separate type of step altogether, or have a
	#configurable set of ignored directories.
        afn = self.absPath(fn)

        if afn.find(self.skipdir) == 0:
                return 0

        return bdb.Bdb.stop_here(self,frame)

    def absPath(self,fn):
        """
        Private method to convert a filename to an absolute name using sys.path
        as a set of possible prefixes. The name stays relative if a file could
        not be found.
        """
        if os.path.isabs(fn):
            return fn

        # Check the cache.
        if self.fncache.has_key(fn):
            return self.fncache[fn]

        # Search sys.path.
        for p in sys.path:
            afn = os.path.normpath(os.path.join(p,fn))

            if os.path.exists(afn):
                self.fncache[fn] = afn
                return afn

        # Nothing found.
        return fn

    def progTerminated(self,status):
        """
        Private method to tell the debugger that the program has terminated.
        """
        if status is None:
            status = 0
        else:
            try:
                int(status)
            except:
                status = 1

        self.set_quit()
        self.write('%s%d\n' % (ResponseExit,status))


# We are normally called by the debugger to execute directly.

if __name__ == '__main__':
    port = int(sys.argv[1])
    sys.argv = ['']
    sys.path[0] = ''
    debugClient = DebugClient()
    debugClient.connectDebugger(port)
    debugClient.interact()
