"""Contains all code for remote/out-of-process connections."""

from mpdb import MPdb

class RemoteWrapper(MPdb):
    
    """An abstract wrapper class that provides common functionality
    for an object that wishes to use remote communication. Classes
    should inherit from this class if they wish to build an object
    capable of remote communication.
    """
    def __init__(self, mpdb_object):
        MPdb.__init__(self)
        self.mpdb = mpdb_object
        self.connection = None

    def _disconnect(self):
        """ Disconnect a connection. """
        self.connection.disconnect()
        self.connection = None
        self.target = 'local'
        if hasattr(self, 'local_prompt'):
            import pydb
            self.prompt = self.local_prompt
            self.onecmd = lambda x: pydb.Pdb.onecmd(self, x)

    def do_restart(self, arg):
        """ Extend pydb.do_restart to signal to any clients connected on
        a debugger's connection that this debugger is going to be restarted.
        All state is lost, and a new copy of the debugger is used.
        """
        # We don't proceed with the restart until the action has been
        # ACK'd by any connected clients
        if self.connection != None:
            self.msg('restart_now\n(MPdb)')
            line = ""
            while not 'ACK:restart_now' in line:
                line = self.connection.readline()
            self.do_rquit(None)
        else:
            self.msg("Re exec'ing\n\t%s" % self._sys_argv)
        import os
        os.execvp(self._sys_argv[0], self._sys_argv)

    def do_rquit(self, arg):
        """ Quit a remote debugging session. The program being executed
        is aborted.
        """
        if self.target == 'local':
            self.errmsg('Connected locally; cannot remotely quit')
            return
        self._rebind_output(self.orig_stdout)
        self._rebind_input(self.orig_stdin)
        self._disconnect()
        self.target = 'local'
        import sys
        sys.settrace(None)
        self.do_quit(None)

class RemoteWrapperServer(RemoteWrapper):
    def __init__(self, mpdb_object):
        RemoteWrapper.__init__(self, mpdb_object)

    def do_pdbserver(self, args):
        """ Allow a debugger to connect to this session.
The first argument is the type or protocol that is used for this connection
(which can be the name of a class that is avaible either in the current
working directory or in Python's PYTHONPATH environtment variable).
The next argument is protocol specific arguments (e.g. hostname and
port number for a TCP connection, or a serial device for a serial line
connection). The next argument is the filename of the script that is
being debugged. The rest of the arguments are passed as arguments to the
script file and are optional. For more information on the arguments for a
particular protocol, type `help pdbserver ' followed by the protocol name.
The syntax for this command is,

`pdbserver ConnectionClass comm scriptfile [args ...]'

"""
        try:
            target, comm = args.split(' ')
        except ValueError:
            self.errmsg('Invalid arguments')
            return
        if 'remote' in self.target:
            self.errmsg('Already connected remotely')
            return
        if self.connection: self.connection.disconnect()
        from mconnection import MConnectionServerFactory, ConnectionFailed
        self.connection = MConnectionServerFactory.create(target)
        if self.connection is None:
            self.errmsg('Unknown protocol')
            return
        try:
            self.msg('Listening on: %s' % comm)
            self.connection.connect(comm)
        except ConnectionFailed, err:
            self.errmsg("Failed to connect to %s: (%s)" % (comm, err))
            return
        self.pdbserver_addr = comm
        self.target = 'remote-pdbserver'
        self._rebind_input(self.connection)
        self._rebind_output(self.connection)

    def do_rdetach(self, arg):
        """ The rdetach command is performed on the pdbserver, it cleans
        things up when the client has detached from this process.
        Control returns to the file being debugged and execution of that
        file continues.
        """
        self._rebind_input(self.orig_stdin)
        self._rebind_output(self.orig_stdout)

        self.cmdqueue.append('continue')  # Continue execution

class RemoteWrapperClient(RemoteWrapper):
    
    """This is a wrapper class that provides remote capability to an
    instance of mpdb.MPdb.
    """
    def __init__(self, mpdb_object):
        RemoteWrapper.__init__(self, mpdb_object)
        self.setcmds.add('target-address', self.set_target_address)
        self.showcmds.add('target-address', self.show_target_address)
        self.infocmds.add('target', self.info_target)
        self.target_addr = ''

    def remote_onecmd(self, line):
        """ All commands in 'line' are sent across this object's connection
        instance variable.
        """
        if not line:
            # Execute the previous command
            line = self.lastcmd
        # This is the simplest way I could think of to do this without
        # breaking any of the inherited code from pydb/pdb. If we're a
        # remote client, always call 'rquit' (remote quit) when connected to
        # a pdbserver. This executes extra code to allow the client and server
        # to quit cleanly.
        if 'quit'.startswith(line):
            line = 'rquit'
            self.connection.write(line)
            # Reset the onecmd method
            self.onecmd = lambda x: pydb.Pdb.onecmd(self, x)
            self.do_rquit(None)
            return
        if 'detach'.startswith(line):
            self.connection.write('rdetach')
            self.do_detach(None)
        self.connection.write(line)
        ret = self.connection.readline()
        if ret == '':
            self.errmsg('Connection closed unexpectedly')
            self.onecmd = lambda x: pydb.Pdb.onecmd(self, x)
            self.do_rquit(None)
        # The output from the command that we've just sent to the server
        # is returned along with the prompt of that server. So we keep reading
        # until we find our prompt.
        i = 1
        while self.local_prompt not in ret:
            if i == 100:
                # We're probably _never_ going to get that data and that
                # connection is probably dead.
                self.errmsg('Connection died unexpectedly')
                self.onecmd = lambda x: pydb.Pdb.onecmd(self, x)
                self.do_rquit(None)
            else:
                ret += self.connection.readline()
                i += 1

        # Some 'special' actions must be taken depending on the data returned
        if 'restart_now' in ret:
            self.connection.write('ACK:restart_now')
            self.errmsg('Pdbserver restarting..')
            # We've acknowledged a restart, which means that a new pdbserver
            # process is started, so we have to connect all over again.
            self._disconnect()
            time.sleep(3.0)
            if not self.do_target(self.target_addr):
                # We cannot trust these variables below to be in a
                # stable state. i.e. if the pdbserver doesn't come back up.
                self.onecmd = lambda x: pydb.Pdb.onecmd(self, x)
                return
        self.msg_nocr(ret)
        self.lastcmd = line
        return

    def do_target(self, args):
        """ Connect to a target machine or process.
The first argument is the type or protocol of the target machine
(which can be the name of a class that is available either in the current
working directory or in Python's PYTHONPATH environment variable).
Remaining arguments are interpreted by the target protocol.  For more
information on the arguments for a particular protocol, type
`help target ' followed by the protocol name.

List of target subcommands:

target serial device-name -- Use a remote computer via a serial line
target tcp hostname:port -- Use a remote computer via a socket connection
"""
        if not args:
            args = self.target_addr
        try:
            target, addr = args.split(' ')
        except ValueError:
            self.errmsg('Invalid arguments')
            return False
        # If addr is ':PORT' fill in 'localhost' as the hostname
        if addr[0] == ':':
            addr = 'localhost'+addr[:]
        if 'remote' in self.target:
            self.errmsg('Already connected to a remote machine.')
            return False
        if self.connection: self.connection.disconnect()
        from mconnection import MConnectionClientFactory, ConnectionFailed
        self.connection = MConnectionClientFactory.create(target)
        try:
            self.connection.connect(addr)
        except ConnectionFailed, err:
            self.errmsg("Failed to connect to %s: (%s)" % (addr, err))
            return False
        # This interpreter no longer interprets commands but sends
        # them straight across this object's connection to a server.
        # XXX: In the remote_onecmd method we use the local_prompt string
        # to find where the end of the message from the server is. We
        # really need a way to get the prompt from the server for checking
        # in remote_onecmd, because it may be different to this client's.
        self.local_prompt = self.prompt
        self.prompt = ""
        self.target_addr = target + " " + addr
        line = self.connection.readline()
        if line == '':
            self.errmsg('Connection closed unexpectedly')
            self.do_quit(None)
        while '(MPdb)' not in line:
            line = self.connection.readline()
        self.msg_nocr(line)
        self.onecmd = self.remote_onecmd
        self.target = 'remote-client'
        return True

    def set_target_address(self, args):
        """Set the address of a target."""
        self.target_addr = "".join(["%s " % a for a in args[1:]])
        self.target_addr = self.target_addr.strip()
        self.msg('target address set to %s' % self.target_addr)

    def show_target_address(self, arg):
        """Show the address of the current target."""
        self.msg('target-address is %s.' % self.target_addr.__repr__())

    def info_target(self, args):
        """Display information about the current target."""
        self.msg('target is %s' % self.target)




