# This is a helper utility that launches Pin with "-appdebug", gets the
# TCP port number that it is waiting on, and then launches a debugger so
# that it attaches to that port.
#
# We used to do this with shell commands in the makefile, but there was
# a Cygwin bug that caused this to hang periodically (see Mantis #1839).

import optparse
import os.path
import re
import subprocess
import sys
import time


def Main(argv):
    """
    Main entry point.

    @param argv:    Command line arguments.
    @type argv:     List of String.
    """

    # There appears to be a bug in the Cygwin version that prevents us from
    # reading the output file of a subprocess while that subprocess is running.
    #
    if sys.platform == 'cygwin':
        print "This script must be run with the Windows native Python"
        return 1

    # Parse options.
    #
    parser = optparse.OptionParser()
    parser.add_option("--pin", dest="pin", type="string", default="",
        help="Pathname to the Pin driver script (or executable).  Required.")
    parser.add_option("--pin-exe", dest="pinexe", type="string", default="",
        help="Pathname to the Pin executable.  Required.")
    parser.add_option("--pindb", dest="pindb", type="string", default="",
        help="Pathname to the PinDB executable.  Required.")
    parser.add_option("--pindb-libpath", dest="pindbLibPath", type="string", default="",
        help="Additional path(s) to append to LD_LIBRARY_PATH when launching PinDB.  Separate paths with a ':'.")
    parser.add_option("--tool", dest="tool", type="string", default="",
        help="Pathname to the Pin tool.  Required.")
    parser.add_option("--app", dest="app", type="string", default="",
        help="Pathname to the application.  Required.")
    parser.add_option("--pin-out", dest="pinout", type="string", default="",
        help="Pathname to the output file which receives the Pin output.  Required.")
    parser.add_option("--pindb-in", dest="pindbin", type="string", default="",
        help="Pathname to the input file for PinDB.  Required.")
    parser.add_option("--pindb-out", dest="pindbout", type="string", default="",
        help="Pathname to the output file which receives the PinDB output.  Required.")
    parser.add_option("--cpu", dest="cpu", type="string", default="",
        help="Name of the CPU to test [ia32 | intel64].  Required.")
    parser.add_option("--os", dest="os", type="string", default="",
        help="Name of the OS to test [linux | windows].  Required.")
    parser.add_option("--timeout", dest="timeout", type="int", default=30,
        help="Time limit (seconds) before deciding that a command is hung.")

    (options, args) = parser.parse_args(args=argv)
    if not options.pin:
        print "Must specify --pin=PIN option"
        return 1
    if not options.pinexe:
        print "Must specify --pin-exe=PINEXE option"
        return 1
    if not options.pindb:
        print "Must specify --pindb=PINDB option"
        return 1
    if not options.tool:
        print "Must specify --tool=TOOL option"
        return 1
    if not options.app:
        print "Must specify --app=APP option"
        return 1
    if not options.pinout:
        print "Must specify --pin-out=PINOUT option"
        return 1
    if not options.pindbin:
        print "Must specify --pindb-in=PINDBIN option"
        return 1
    if not options.pindbout:
        print "Must specify --pindb-out=PINDBOUT option"
        return 1
    if not options.cpu:
        print "Must specify --cpu=CPU option"
        return 1
    if options.cpu not in ['ia32', 'intel64']:
        print "Invalid --cpu=CPU option"
        return 1
    if not options.os:
        print "Must specify --os=OS option"
        return 1
    if options.os not in ['linux', 'windows']:
        print "Invalid --os=OS option"
        return 1

    # Launch Pin.
    #
    cmd = [os.path.normcase(options.pin)]
    cmd.append('-slow_asserts')
    if options.os == 'windows':
        cmd.append('-xyzzy')
        cmd.append('-late_injection')
    cmd.append('-appdebug')
    cmd.append('-t')
    cmd.append(os.path.normcase(options.tool))
    cmd.append('--')
    cmd.append(os.path.normcase(options.app))

    outFile = open(os.path.normcase(options.pinout), 'w')

    PrintCommand(cmd)
    try:
        subPin = subprocess.Popen(cmd, stdout=outFile)
    except (OSError):
        print "Unable to launch Pin"
        return 1

    # Pin waits for the debugger to attach.  Wait for it to print the
    # port number, and get that port number from the output file.
    #
    port = None
    startTime = time.time()
    rePort = re.compile('\s*target remote :([0-9]+)$')
    while not port:
        # Check for timeout.
        #
        now = time.time()
        if (now - startTime) > options.timeout:
            print "Timed out waiting for port after " + str(options.timeout) + " seconds"
            KillProcess(subPin)
            return 1
        time.sleep(1)

        f = open(os.path.normcase(options.pinout), 'r')
        lines = f.readlines()
        f.close()
        for line in lines:
            m = rePort.match(line)
            if m:
                port = m.group(1)
                break

    # Launch PinDB.
    #
    cmd = [os.path.normcase(options.pindb)]
    cmd.append('--pin=' + os.path.normcase(options.pinexe))
    cmd.append('--timeout=' + str(options.timeout))
    cmd.append('--gdb-protocol=:' + port)
    cmd.append('--cpu=' + options.cpu)
    cmd.append('--os=' + options.os)
    cmd.append('--noprompt')

    env = None
    if options.pindbLibPath:
        env = os.environ
        if 'LD_LIBRARY_PATH' in env:
            env['LD_LIBRARY_PATH'] += ':' + options.pindbLibPath
        else:
            env['LD_LIBRARY_PATH'] = options.pindbLibPath
        print "PinDB runs with this LD_LIBRARY_PATH: " + env['LD_LIBRARY_PATH']

    inFile = open(os.path.normcase(options.pindbin), 'r')
    outFile = open(os.path.normcase(options.pindbout), 'w')

    PrintCommand(cmd)
    try:
        subPinDB = subprocess.Popen(cmd, env=env, stdin=inFile, stdout=outFile)
    except (OSError):
        print "Unable to launch PinDB"
        KillProcess(subPin)
        return 1

    # If PinDB exits abnormally, it may have left the Pin process hanging, so try to kill it.
    #
    if subPinDB.wait() != 0:
        KillProcess(subPin)
        return subPinDB.returncode
    return subPin.wait()


def KillProcess(sub):
    """
    Kill a subprocess.

    @param sub:     The subprocess to kill.
    @type sub:      Popen object.
    """

    # The functions below are only available on Python 2.6 or later.
    #
    if sys.hexversion < 0x20600f0:
        return

    # First try to kill it gracefully, then terminate forcefully.
    #
    sub.terminate()
    time.sleep(1)
    sub.kill()


def PrintCommand(cmd):
    """
    Print an informational message about a command that is executed.

    @param cmd:     The command.
    @type cmd:      List of String.
    """

    mesg = "Launching: " + " ".join(cmd)
    print mesg


if __name__ == "__main__": sys.exit(Main(sys.argv[1:]))
