#!~/.wine/drive_c/Python25/python.exe
# -*- coding: utf-8 -*-

# Acknowledgements:
#  Nicolas Economou, for his command line debugger on which this is inspired.
#  http://tinyurl.com/nicolaseconomou

# Command line debugger using WinAppDbg
# Copyright (c) 2009-2014, Mario Vilas
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#     * Redistributions of source code must retain the above copyright notice,
#       this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright
#       notice,this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#     * Neither the name of the copyright holder nor the names of its
#       contributors may be used to endorse or promote products derived from
#       this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

__revision__ = "$Id$"

import winappdbg
from winappdbg import System, Debug, ConsoleDebugger

import sys
import optparse

class PDebug (ConsoleDebugger):
    history_file = '.pdebug'    # backwards compatibility with WinAppDbg 1.4
    confirm_quit = True         # confirm before quitting

    # Override the help message.
    def do_quit(self, arg):
        """
        quit - detach from all processes and shut down the debugger
        q - detach from all processes and shut down the debugger
        """
        ConsoleDebugger.do_quit(self, arg)

    do_q = do_quit

#------------------------------------------------------------------------------
# Run from the command line

    # Run the debugger.
    # This is the first method called.
    def run(self, argv):
        self.argv = list(argv)
        try:
            self.initialize()
            self.loop()
        finally:
            self.finalize()

    # Initialize the debugger.
    def initialize(self):
        self.print_banner()
        self.parse_cmdline()
        self.create_debugger()
        self.queue_initial_commands()
        self.load_history()
##        self.set_control_c_handler()

    # Clean up when closing the debugger.
    def finalize(self):
##        self.remove_control_c_handler()
        if hasattr(self, "options"):
            self.destroy_debugger(self.options.autodetach)
        self.save_history()

    # Instance a Debug object and start using it.
    def create_debugger(self):

        # Instance a debugger
        debug = Debug(self, bHostileCode = self.options.hostile)

        # Make sure the remote symbol store is set
        System.fix_symbol_store_path(remote = True, force = False)

        # Populate the snapshot of processes
        debug.system.scan()

        # Use this debugger
        self.start_using_debugger(debug)

    # Print the welcome banner.
    def print_banner(self):
        print "WinAppDbg %s console debugger" % winappdbg.version
        print "by Mario Vilas (mvilas at gmail.com)"
        print

#------------------------------------------------------------------------------
# Command line parsing

# TODO
# * add an option to show python tracebacks of all errors, disabled by default

    # Define the command line parser and parse the command line.
    def parse_cmdline(self):

        # Define the command line parser
        usage = (
                "\n"
                "\n"
                "  Just show the prompt:\n"
                "    %prog\n"
                "\n"
                "  Create a new process:\n"
                "    %prog [options] -c \"console_target.exe optional parameters...\"\n"
                "    %prog [options] -w \"windowed_target.exe optional parameters...\"\n"
                "\n"
                "  Attach to a running process (by filename):\n"
                "    %prog [options] -a \"executable\"\n"
                "\n"
                "  Attach to a running process (by ID):\n"
                "    %prog [options] -a pid"
                )
        self.parser = optparse.OptionParser(
                                        usage=usage,
                                        version=winappdbg.version,
                                      )
        commands = optparse.OptionGroup(self.parser, "Commands")
        commands.add_option("-a", "--attach", action="append", type="string",
                            metavar="PROCESS",
                            help="Attach to a running process")
        commands.add_option("-w", "--windowed", action="callback", type="string",
                            metavar="CMDLINE", callback=self.callback_execute_target,
                            help="Create a new windowed process")
        commands.add_option("-c", "--console", action="callback", type="string",
                            metavar="CMDLINE", callback=self.callback_execute_target,
                            help="Create a new console process [default]")
        self.parser.add_option_group(commands)
        debugging = optparse.OptionGroup(self.parser, "Debugging options")
        debugging.add_option("--autodetach", action="store_true",
                   help="automatically detach from debugees on exit [default]")
        debugging.add_option("--follow", action="store_true",
                      help="automatically attach to child processes [default]")
        debugging.add_option("--trusted", action="store_false",
                                                                dest="hostile",
                      help="treat debugees as trusted code [default]")
        debugging.add_option("--dont-autodetach", action="store_false",
                                                             dest="autodetach",
                   help="don't automatically detach from debugees on exit")
        debugging.add_option("--dont-follow", action="store_false",
                                                                 dest="follow",
                          help="don't automatically attach to child processes")
        debugging.add_option("--hostile", action="store_true",
                      help="treat debugees as hostile code")
        self.parser.add_option_group(debugging)

        # Set the default values
        self.parser.set_defaults(
            attach      = [],
            console     = [],
            windowed    = [],
            autodetach  = True,
            follow      = True,
            hostile     = False,
        )

        # Parse the command line
        (self.options, args) = self.parser.parse_args(self.argv)
        args = args[1:]
        if not self.options.windowed and not self.options.console and not self.options.attach:
            if args:
                self.options.console = [ args ]
        else:
            if args:
                self.parser.error("don't know what to do with extra parameters: %s" % args)

    # Callback to parse -c and -w command line switches
    @staticmethod
    def callback_execute_target(option, opt_str, value, parser):

        # Get the destination variable name.
        dest_name = option.dest
        if dest_name is None:
            dest_name = option.get_opt_string().replace('-', '')

        # Get the destination list to append.
        # Create a new list if needed.
        destination = getattr(parser.values, dest_name, None)
        if destination is None:
            destination = list()
            setattr(parser.values, dest_name, destination)

        # If a value is received from optparse, put it back in the list of
        # arguments to be consumed.
        #
        # From what I gather by examining the examples in the documentation this
        # wasn't even supposed to happen. (!)
        #
        # I suspect is happening because I had to force the argument type for the
        # command line switch definition as a workaround for another bug (the
        # metavariable wasn't being shown in the help message).
        #
        if value is not None:
            parser.rargs.insert(0, value)

        # Get the value from the command line arguments.
        value = []
        for arg in parser.rargs:

            # Stop on --foo like options but not on -- alone.
            if arg[:2] == "--" and len(arg) > 2:
                break

            # Stop on -a like options but not on - alone.
            if arg[:1] == "-" and len(arg) > 1:
                break

            value.append(arg)

        # Delete the command line arguments we consumed so they're not parsed again.
        del parser.rargs[:len(value)]

        # Append the value to the destination list.
        destination.append(value)

    # Queue the startup commands when running from command line.
    def queue_initial_commands(self):

        # Queue the attach commands, if needed
        if self.options.attach:
            cmd = 'attach %s' % self.join_tokens(self.options.attach)
            self.cmdqueue.append(cmd)

        # Queue the windowed commands, if needed
        for argv in self.options.windowed:
            cmdline = System.argv_to_cmdline(argv)
            self.cmdqueue.append( 'windowed %s' % cmdline )

        # Queue the console commands, if needed
        for argv in self.options.console:
            cmdline = System.argv_to_cmdline(argv)
            self.cmdqueue.append( 'console %s' % cmdline )

        # Queue the continue command, if other commands were queued before
        if len(self.cmdqueue) > 0:
            self.cmdqueue.append('continue')

#==============================================================================

def main(argv):
    return PDebug().run(argv)

if __name__ == '__main__':
    try:
        import psyco
        psyco.bind(main)
    except ImportError:
        pass
    main(sys.argv)
