# -*- coding: utf-8 -*-

# Copyright (c) 2002, 2003 Detlev Offenbach <detlev@die-offenbachs.de>
# Copyright (c) 2000, 2001, 2002 Phil Thompson <phil@river-bank.demon.co.uk>
#

"""
Module implementing the debug server.
"""

import sys
import os
import socket
import time
import signal

from qt import SIGNAL, PYSIGNAL, qApp
from qtnetwork import QServerSocket, QSocket

from DebugProtocol import *
import Preferences


class DebugServer(QServerSocket):
    """
    Class implementing the debug server embedded within the IDE.
    
    Signals
    
        clientRawInputSent -- emitted after the data was sent to the debug client
        
        clientLine(filename, lineno) -- emitted after the debug client has executed a
            line of code
        
        clientStack(stack) -- emitted after the debug client has executed a
            line of code
            
        clientVariables(variables) -- emitted after a variables dump has been received
        
        clientVariable(variables) -- emitted after a dump for one class variable has
            been received
        
        clientStatement(boolean) -- emitted after an interactive command has
            been executed. The parameter is 0 to indicate that the command is
            complete and 1 if it needs more input.
            
        clientException(exception) -- emitted after an exception occured on the 
            client side
            
        clientSyntaxError(exception) -- emitted after a syntax error has been detected
            on the client side
            
        clientExit(exitcode) -- emitted after the client has exited
        
        clientClearBreak(filename, lineno) -- emitted after the debug client
            has decided to clear a temporary breakpoint
        
        clientRawInput(prompt) -- emitted after a raw input request was received
        
        clientBanner(banner) -- emitted after the client banner was received
        
        clientCompletionList(completionList, text) -- emitted after the client
            the commandline completion list and the reworked searchstring was
            received from the client
        
        passiveDebugStarted -- emitted after the debug client has connected in
            passive debug mode
            
        clientGone -- emitted if the client went away (planned or unplanned)
        
        utPrepared(nrTests, exc_type, exc_value) -- emitted after the client has
            loaded a unittest suite
        
        utFinished -- emitted after the client signalled the end of the unittest
        
        utStartTest(testname, testdocu) -- emitted after the client has started 
            a test
        
        utStopTest -- emitted after the client has finished a test
        
        utTestFailed(testname, exc_info) -- emitted after the client reported 
            a failed test
        
        utTestErrored(testname, exc_info) -- emitted after the client reported 
            an errored test
    """
    def __init__(self):
        """
        Constructor
        """
        if Preferences.getDebugger("PassiveDbgEnabled"):
            socket = Preferences.getDebugger("PassiveDbgPort") # default: 42424
            QServerSocket.__init__(self,socket)
            self.passive = 1
        else:
            QServerSocket.__init__(self,0)
            self.passive = 0

        self.qsock = None
        self.progLoaded = 0
        self.debugging = 1
        self.queue = []
        self.clientPID = 0

        if not self.passive:
            self.startRemote()

    def startRemote(self):
        """
        Private method to start a remote interpreter.
        """
        if Preferences.getDebugger("CustomPythonInterpreter"):
            interpreter = str(Preferences.getDebugger("PythonInterpreter"))
            if interpreter == "":
                interpreter = sys.executable
        else:
            interpreter = sys.executable
            
        debugClientType = Preferences.getDebugger("DebugClientType")
        if debugClientType == 1:
            debugClient = os.path.join(sys.path[0],'Debugger','DebugClient.py')
        elif debugClientType == 2:
            debugClient = os.path.join(sys.path[0],'Debugger','DebugClientThreads.py')
        elif debugClientType == 3:
            debugClient = os.path.join(sys.path[0],'Debugger','DebugClientNoQt.py')
        else:
            debugClient = str(Preferences.getDebugger("DebugClient"))
            if debugClient == "":
                debugClient = os.path.join(sys.path[0],'Debugger','DebugClient.py')
            
        if self.clientPID:
            try:
                # get rid of died child on Unix systems; if the child is
                # non cooperative, it will be killed
                status = os.waitpid(self.clientPID, os.WNOHANG)
                if status == (0, 0):
                    os.kill(self.clientPID, signal.SIGTERM)
                    time.sleep(1)
                    status = os.waitpid(self.clientPID, os.WNOHANG)
                    if status == (0, 0):
                        os.kill(self.clientPID, signal.SIGKILL)
            except:
                pass
            
        if Preferences.getDebugger("RemoteDbgEnabled"):
            ipaddr = socket.gethostbyname(socket.gethostname())
            rexec = str(Preferences.getDebugger("RemoteExecution"))
            rhost = str(Preferences.getDebugger("RemoteHost"))
            self.clientPID = os.spawnv(os.P_NOWAIT, rexec,
                [rexec, rhost, 
                 interpreter, os.path.abspath(debugClient), `self.port()`, ipaddr])
        else:
            self.clientPID = os.spawnv(os.P_NOWAIT,interpreter,
                [interpreter, debugClient, `self.port()`])

    def newConnection(self,sockfd):
        """
        Reimplemented to handle a new connection.
        
        Arguments
        
            sockfd -- the socket descriptor
        """
        sock = QSocket()
        sock.setSocket(sockfd)

        # If we already have a connection, refuse this one.  It will be closed
        # automatically.
        if self.qsock is not None:
            return

        self.connect(sock,SIGNAL('readyRead()'),self.handleLine)
        self.connect(sock,SIGNAL('connectionClosed()'),self.startClient)

        self.qsock = sock

        # Send commands that were waiting for the connection.
        for cmd in self.queue:
            self.qsock.writeBlock(cmd)

        self.queue = []
        
        if self.passive:
            self.progLoaded = 1

    def shutdownServer(self):
        """
        Public method to cleanly shut down.
        
        It closes our socket and shuts down
        the debug client. (Needed on Win OS)
        """
        if self.qsock is None:
            return
            
        # do not want any slots called during shutdown
        self.disconnect(self.qsock, SIGNAL('connectionClosed()'), self.startClient)
        self.disconnect(self.qsock, SIGNAL('readyRead()'), self.handleLine)
        
        # close down socket, and shut down client as well.
        self.sendCommand('%s\n' % RequestShutdown)
        self.qsock.flush()
        
        self.qsock.close()
        
        # reinitialize
        self.qsock = None
        self.progLoaded = 1  # fake server into starting new client later...
        self.queue = []

    def remoteLoad(self,fn,argv,wd):
        """
        Public method to load a new program to debug.
        
        Arguments
        
            fn -- the filename to debug (string)
            
            argv -- the commandline arguments to pass to the program (list of strings)
            
            wd -- the working directory for the program (string)
        """
        # Restart the client if there is already a program loaded.
        if self.progLoaded:
            self.startClient(0)
        
        self.sendCommand('%s%s|%s|%s\n' % \
            (RequestLoad, str(wd),
             str(os.path.abspath(str(fn))),str(argv)))
        self.progLoaded = 1
        self.debugging = 1
        self.restoreBreakpoints()

    def remoteRun(self,fn,argv,wd):
        """
        Public method to load a new program to run.
        
        Arguments
        
            fn -- the filename to run (string)
            
            argv -- the commandline arguments to pass to the program (list of strings)
            
            wd -- the working directory for the program (string)
        """
        # Restart the client if there is already a program loaded.
        if self.progLoaded:
            self.startClient(0)
        
        self.sendCommand('%s%s|%s|%s\n' % \
            (RequestRun, str(wd),
             str(os.path.abspath(str(fn))),str(argv)))
        self.progLoaded = 1
        self.debugging = 0

    def remoteCoverage(self,fn,argv,wd,erase):
        """
        Public method to load a new program to collect coverage data.
        
        Arguments
        
            fn -- the filename to run (string)
            
            argv -- the commandline arguments to pass to the program (list of strings)
            
            wd -- the working directory for the program (string)
            
            erase -- flag indicating that coverage info should be cleared first (boolean)
        """
        # Restart the client if there is already a program loaded.
        if self.progLoaded:
            self.startClient(0)
        
        self.sendCommand('%s%s|%s|%s|%d\n' % \
            (RequestCoverage, str(wd),
             str(os.path.abspath(str(fn))),str(argv),erase))
        self.progLoaded = 1
        self.debugging = 0

    def remoteProfile(self,fn,argv,wd,erase):
        """
        Public method to load a new program to collect profiling data.
        
        Arguments
        
            fn -- the filename to run (string)
            
            argv -- the commandline arguments to pass to the program (list of strings)
            
            wd -- the working directory for the program (string)
            
            erase -- flag indicating that timing info should be cleared first (boolean)
        """
        # Restart the client if there is already a program loaded.
        if self.progLoaded:
            self.startClient(0)
        
        self.sendCommand('%s%s|%s|%s|%d\n' % \
            (RequestProfile, str(wd),
             str(os.path.abspath(str(fn))),str(argv),erase))
        self.progLoaded = 1
        self.debugging = 0

    def remoteStatement(self,stmt):
        """
        Public method to execute a Python statement.  
        
        Arguments
        
            stmt -- the Python statement to execute (string). It
              should not have a trailing newline.
        """
        self.sendCommand('%s\n' % stmt)
        self.sendCommand(RequestOK + '\n')

    def remoteStep(self):
        """
        Public method to single step the debugged program.
        """
        self.sendCommand(RequestStep + '\n')

    def remoteStepOver(self):
        """
        Public method to step over the debugged program.
        """
        self.sendCommand(RequestStepOver + '\n')

    def remoteStepOut(self):
        """
        Public method to step out the debugged program.
        """
        self.sendCommand(RequestStepOut + '\n')

    def remoteStepQuit(self):
        """
        Public method to stop the debugged program.
        """
        self.sendCommand(RequestStepQuit + '\n')

    def remoteContinue(self):
        """
        Public method to continue the debugged program.
        """
        self.sendCommand(RequestContinue + '\n')

    def remoteBreakpoint(self,fn,line,set,cond=None,temp=0):
        """
        Public method to set or clear a breakpoint.
        
        Arguments
        
            fn -- filename the breakpoint belongs to (string)
            
            line -- linenumber of the breakpoint (int)
            
            set -- flag indicating setting or resetting a breakpoint (boolean)
            
            cond -- condition of the breakpoint (string)
            
            temp -- flag indicating a temporary breakpoint (boolean)
        """
        self.sendCommand('%s%s,%d,%d,%d,%s\n' % (RequestBreak,fn,line,temp,set,cond))
        
    def remoteBreakpointEnable(self,fn,line,enable):
        """
        Public method to enable or disable a breakpoint.
        
        Arguments
        
            fn -- filename the breakpoint belongs to (string)
            
            line -- linenumber of the breakpoint (int)
            
            enable -- flag indicating enabling or disabling a breakpoint (boolean)
        """
        self.sendCommand('%s%s,%d,%d\n' % (RequestBreakEnable,fn,line,enable))
        
    def remoteBreakpointIgnore(self,fn,line,count):
        """
        Public method to ignore a breakpoint the next couple of occurences.
        
        Arguments
        
            fn -- filename the breakpoint belongs to (string)
            
            line -- linenumber of the breakpoint (int)
            
            count -- number of occurences to ignore (int)
        """
        self.sendCommand('%s%s,%d,%d\n' % (RequestBreakIgnore,fn,line,count))
        
    def remoteRawInput(self,s):
        """
        Public method to send the raw input to the debugged program.
        
        Arguments
        
            s -- the raw input (string)
        """
        self.sendCommand(s + '\n')
        self.emit(PYSIGNAL('clientRawInputSent'), ())
        
    def remoteClientVariables(self, scope, filter, framenr=0):
        """
        Public method to request the variables of the debugged program.
        
        Arguments
        
            scope -- the scope of the variables (0 = local, 1 = global)
            
            filter -- list of variable types to filter out (list of int)
            
            framenr -- framenumber of the variables to retrieve (int)
        """
        self.sendCommand('%s%d, %d, %s\n' % (RequestVariables,framenr,scope,str(filter)))
        
    def remoteClientVariable(self, scope, filter, var, framenr=0):
        """
        Public method to request the variables of the debugged program.
        
        Arguments
        
            scope -- the scope of the variables (0 = local, 1 = global)
            
            filter -- list of variable types to filter out (list of int)
            
            var -- list encoded name of variable to retrieve (string)
            
            framenr -- framenumber of the variables to retrieve (int)
        """
        self.sendCommand('%s%s, %d, %d, %s\n' % (RequestVariable,str(var),framenr,scope,str(filter)))
        
    def remoteEval(self, arg):
        """
        Public method to evaluate arg in the current context of the debugged program.
        
        Arguments
        
            arg -- the arguments to evaluate (string)
        """
        self.sendCommand('%s%s\n' % (RequestEval,arg))
        
    def remoteExec(self, stmt):
        """
        Public method to execute stmt in the current context of the debugged program.
        
        Arguments
        
            stmt -- statement to execute (string)
        """
        self.sendCommand('%s%s\n' % (RequestExec,stmt))
        
    def remoteBanner(self):
        """
        Public slot to get the banner info of the remote client.
        """
        self.sendCommand(RequestBanner + '\n')
        
    def remoteCompletion(self, text):
        """
        Public slot to get the a list of possible commandline completions
        from the remote client.
        
        Arguments
        
            text -- the text to be completed (string or QString)
        """
        self.sendCommand("%s%s\n" % (RequestCompletion, text))

    def remoteUTPrepare(self,fn,tn,cov,covname,coverase):
        """
        Public method to prepare a new unittest run.
        
        Arguments
        
            fn -- the filename to load (string)
            
            tn -- the testname to load (string)
            
            cov -- flag indicating collection of coverage data is requested
            
            covname -- filename to be used to assemble the coverage caches
                filename
                
            coverase -- flag indicating erasure of coverage data is requested
        """
        # Restart the client if there is already a program loaded.
        if self.progLoaded:
            self.startClient(0)
        
        self.sendCommand('%s%s|%s|%d|%s|%d\n' % \
            (RequestUTPrepare,
             str(os.path.abspath(str(fn))), str(tn), cov, str(covname), coverase))
        self.progLoaded = 1
        self.debugging = 0
        
    def remoteUTRun(self):
        """
        Public method to start a unittest run.
        """
        self.sendCommand('%s\n' % RequestUTRun)
        
    def remoteUTStop(self):
        """
        public method to stop a unittest run.
        """
        self.sendCommand('%s\n' % RequestUTStop)
        
    def handleLine(self):
        """
        Private method to handle data from the client.
        """
        while self.qsock and self.qsock.canReadLine():
            line = str(self.qsock.readLine())

##~             print line          ##debug
            
            eoc = line.find('<') + 1
            
            # Deal with case where user has written directly to stdout
            # or stderr, but not line terminated and we stepped over the
            # write call, in that case the >line< will not be the first
            # string read from the socket...
            boc = line.find('>')
            if boc > 0 and eoc > boc:
                self.emit(PYSIGNAL('clientOutput'),(line[:boc],))
                
                    
            if boc >= 0 and eoc > boc:
                # Emit the signal corresponding to the response.
                resp = line[boc:eoc]

                if resp == ResponseLine:
                    stack = eval(line[eoc:-1])
                    cf = stack[0]
                    self.emit(PYSIGNAL('clientLine'),(cf[0],int(cf[1])))
                    self.emit(PYSIGNAL('clientStack'),(stack,))
                    continue

                if resp == ResponseVariables:
                    self.emit(PYSIGNAL('clientVariables'),(line[eoc:-1],))
                    continue

                if resp == ResponseVariable:
                    self.emit(PYSIGNAL('clientVariable'),(line[eoc:-1],))
                    continue

                if resp == ResponseOK:
                    self.emit(PYSIGNAL('clientStatement'),(0,))
                    continue

                if resp == ResponseContinue:
                    self.emit(PYSIGNAL('clientStatement'),(1,))
                    continue

                if resp == ResponseException:
                    self.emit(PYSIGNAL('clientException'),(line[eoc:-1],))
                    continue

                if resp == ResponseSyntax:
                    self.emit(PYSIGNAL('clientSyntaxError'),(line[eoc:-1],))
                    continue
                    
                if resp == ResponseExit:
                    self.emit(PYSIGNAL('clientExit'),(line[eoc:-1],))
                    if self.passive:
                        self.passiveShutDown()
                    if Preferences.getDebugger("AutomaticReset"):
                        self.startClient(0)
                    continue
                
                if resp == ResponseClearBreak:
                    fn, lineno = line[eoc:-1].split(',')
                    lineno = int(lineno)
                    self.emit(PYSIGNAL('clientClearBreak'),(fn, lineno))
                    continue
                    
                if resp == ResponseRaw:
                    self.emit(PYSIGNAL('clientRawInput'),(line[eoc:-1],))
                    continue
                    
                if resp == ResponseBanner:
                    bi = eval(line[eoc:-1])
                    self.emit(PYSIGNAL('clientBanner'),bi)
                    continue
                    
                if resp == ResponseCompletion:
                    clstring, text = line[eoc:-1].split('||')
                    cl = eval(clstring)
                    self.emit(PYSIGNAL('clientCompletionList'),(cl,text))
                    continue
                    
                if resp == PassiveStartup:
                    self.passiveStartUp(line[eoc:-1])
                    continue
                    
                if resp == ResponseUTPrepared:
                    res = eval(line[eoc:-1])
                    self.emit(PYSIGNAL('utPrepared'),res)
                    continue
                    
                if resp == ResponseUTStartTest:
                    testinfo = eval(line[eoc:-1])
                    self.emit(PYSIGNAL('utStartTest'),testinfo)
                    continue
                    
                if resp == ResponseUTStopTest:
                    self.emit(PYSIGNAL('utStopTest'),())
                    continue
                    
                if resp == ResponseUTTestFailed:
                    err = eval(line[eoc:-1])
                    self.emit(PYSIGNAL('utTestFailed'),err)
                    continue
                    
                if resp == ResponseUTTestErrored:
                    err = eval(line[eoc:-1])
                    self.emit(PYSIGNAL('utTestErrored'),err)
                    continue
                    
                if resp == ResponseUTFinished:
                    self.emit(PYSIGNAL('utFinished'),())
                    continue

            self.emit(PYSIGNAL('clientOutput'),(line,))

    def passiveStartUp(self, fn):
        """
        Private method to handle a passive debug connection.
        
        Arguments
        
            fn -- filename of the debugged script
        """
        print str(self.trUtf8("Passive debug connection received"))
        self.passiveClientExited = 0
        self.restoreBreakpoints()
        self.emit(PYSIGNAL('passiveDebugStarted'), (fn,))
        
    def passiveShutDown(self):
        """
        Private method to shut down a passive debug connection.
        """
        self.passiveClientExited = 1
        self.shutdownServer()
        print str(self.trUtf8("Passive debug connection closed"))
        
    def startClient(self,unplanned=1):
        """
        Private method to start a debug client.
        
        Arguments
        
            unplanned -- flag indicating that the client has died
        """
        if not self.passive or not self.passiveClientExited: 
            if self.qsock is not None:
                self.shutdownServer()
                self.emit(PYSIGNAL('clientGone'),(unplanned & self.debugging,))
            
        if not self.passive:
            self.startRemote()

    def sendCommand(self,cmd):
        """
        Private method to send a single line command to the client.
        
        Arguments
        
            cmd -- command to send to the debug client (string)
        """
        if self.qsock is not None:
            self.qsock.writeBlock(cmd)
        else:
            self.queue.append(cmd)

    def restoreBreakpoints(self):
        """
        Private method to restore the break points after a restart.
        """
        vm = qApp.mainWidget().getViewManager()
        for fn in vm.getOpenFilenames():
            editor = vm.getOpenEditor(fn)
            breaks = editor.getBreakpoints()
            for line, cond, temp, enabled, ignorecount in breaks:
                self.remoteBreakpoint(fn, line, 1, cond, temp)
                if not enabled:
                    self.remoteBreakpointEnable(fn, line, 0)
                if ignorecount:
                    self.remoteBreakpointIgnore(fn, line, ignorecount)
