#!/usr/bin/env python
#########################################################################
""" wingdb.py    -- Top-level command used internally by Wing IDE to
                    start a debug process.
                    
Copyright (c) 2000-2012, Archaeopteryx Software, Inc.  All rights reserved.

Written by Stephan R.A. Deibel and John P. Ehresman

"""
#########################################################################

# Only import sys at the top-level because sys.path needs to be modified
# in some contexts
import sys

# Start without translation -- this gets changed once netserver is found
_ = lambda x: x

# For trouble-shooting, set environment variable or uncomment line below
def _GetDefaultPrintAllTracebacks():
  import os 
  return os.environ.get('WINGDB_PRINT_ALL_TRACEBACKS', 0)
kPrintAllTracebacks = _GetDefaultPrintAllTracebacks()
if kPrintAllTracebacks:
  import os
  os.environ['WINGDB_PRINT_ALL_TRACEBACKS'] = '1'
  
# Wing version & build numbers
kVersion = "4.1.9"
kBuild = "1"

# Utils for dealing w/ Python 2.x vs. 3.x
if sys.hexversion >= 0x03000000:
  def has_key(o, key):
    return key in o
else:
  def has_key(o, key):
    return o.has_key(key)
  
# Set __file__ if not already set; this is an internal value so is kosher
try:
  __file__
except NameError:
  __file__ = sys.argv[0]
  
def _GetWingDirs():
  """ Gets winghome & usersettings dir if __name__ is __main__.  Returns
  (None, None) if unable to retrieve the dirs for some reason. """

  if __name__ != '__main__':
    return None, None

  import os

  if has_key(os.environ, 'WINGDB_WINGHOME'):
    try:
      winghome = os.environ['WINGDB_WINGHOME']
      del os.environ['WINGDB_WINGHOME']
      user_settings = os.environ.get('WINGDB_USERSETTINGS', None)
      if user_settings is not None:
        del os.environ['WINGDB_USERSETTINGS']
      return winghome, user_settings
    except:
      if kPrintAllTracebacks:
        import traceback
        traceback.print_exc(file=sys.__stderr__)
    
  try:
    winghome = os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0])))
    return winghome, None
  except:
    if kPrintAllTracebacks:
      sys.__stderr__.write('WINGDB ARGS:' + str(sys.argv))
      sys.__stderr__.write('\n')
      import traceback
      traceback.print_exc(file=sys.__stderr__)
  
  return None, None

#########################################################################
# Temporary, in-memory logger; used until we know where to send the messages
#########################################################################
class CTempLog:
  """ Temporarily log messages to a list. """

  def __init__(self):
    self.fEntries = []
    
  def out(self, *args):
    """ Save msg in fEntries """

    args = map(str, args)
    first = 1
    for s in args:
      if first:
        first = 0
      else:
        self.write(' ')
      self.write(s)
    
  def write(self, msg):
    """ Save msg in fEntries """
    
    if msg[-1:] == '\n':
      msg = msg[:-1]
    self.fEntries.append(msg)
    
  def write_entries(self, log):
    """ Write fEntries to out object via it's out method. """
    
    for entry in self.fEntries:
      log.out(entry)

  def clear(self):
    """ Clear all entries. """
    
    self.fEntries = []
  
#########################################################################
# Argument parsing
#########################################################################

def _ParseSingleArg(err, arg_index, func = None, choices = None):
  """ Parse a single arg from argv; print usage if arg is incorrect.  Applies
  func to the value if func is not None and look up value in choices map
  if choices is not None. """

  try:
    value = sys.argv[arg_index]

    # Transform value if function is provided 
    if func != None:
      value = func(value)
    
    # Look up value in choices if choices is not None
    if choices != None:
      value = choices[value]

    return value

  except:
    if kPrintAllTracebacks:
      import traceback
      traceback.print_exc(file=sys.__stderr__)
    sys.exit(2)
    
def _LogfileTransform(value):
  """ Transformation function for logfile arg. """
  
  value = eval(value)
  
  if value == '<none>':
    return None
  else:
    return value
  
def _HostportTransform(hostport):
  """ Transformation function for host:port arg. """
  
  colonpos = hostport.index(':')
  host = hostport[0:colonpos]
  if host == '':
    host = '127.0.0.1'
  port = int(hostport[colonpos+1:])
  return host, port
 
  
def _ParseArgv(err):
  """ Parses sys.argv, which contains parameters encoded by position, and 
  returns dictionary of values.
  
  The parameters allowed are defined as follows by position in sys.argv:

    0) This script's name
    
    1) host:port indicates where Wing IDE is listening for reverse connection 
    from this debug process.

    2) attachport, where the debug process will listen for attach requests
    when not connected to a debug process.

    3) One of --first-stop to stop on the first line of the debug program, 
    or --no-first-stop to run to first breakpoint or completion.

    4) logfile for debug server internals.  One of <none> for no logging,
    <stderr>, <stdout>, or a file name in which to log extra error output.
    The parameter should be encoded as a Python expression that can
    be parsed by eval().  This avoids problems with special chars
    in file names.  For example, "r'mylog'" is a valid value.

    5) optional --very-verbose-log to turn on core logging support if it
    is present
    
    6) One of --wait-on-exit or --nowait-on-exit.  When set to wait-on-exit, 
    the debugger will wait for user to hit a key before exiting entirely.

    7) Optional --stdout-encoding=<encoding>.  Sets sys.stdout.encoding &
    sys.stderr.encoding to given <encoding> string iff encoding is valid
    
    8) Optional --stdin-encoding=<encoding>.  Sets sys.stdin.encoding
    to given <encoding> string iff encoding is valid
    
    7) filename, which is a Python expression that can be passed to eval()
    that evaluates to the name of the file to debug.

  The dictionary returned from this function contains:
    
    host: host to connect back to
    port: port to connect back to
    attachport: port # to listen far attach requests on
    firststop: whether to stop on first line
    logfile: <none>, <stderr>, <stdout>, or a file name
    veryverboselog: whether very verbose is on
    waitonexit: whether to wait for a keystroke when the program exits
    stdoutencoding: output encoding if specified or None
    stdinencoding: input encoding if specified or None
    filename: name of the python file to debug

  """

  import os

  args = {}

  # Parse args and store value in dictionary to return
  args['host'], args['port'] = _ParseSingleArg(err, 1, _HostportTransform)
  args['attachport'] = _ParseSingleArg(err, 2, int)
  args['firststop'] = _ParseSingleArg(err, 3, choices = {"--no-first-stop": 0,
                                                         "--first-stop": 1})
  args['logfile'] = _ParseSingleArg(err, 4, _LogfileTransform)
  args['veryverboselog'] = (sys.argv[5] == '--very-verbose-log')
  next = 5
  if args['veryverboselog']:
    next = next + 1
  args['waitonexit'] = _ParseSingleArg(err, next, choices = {"--nowait-on-exit": 0,
                                                             "--wait-on-exit": 1})
  next = next + 1

  if sys.argv[next].find('--stdout-encoding=') == 0:
    args['stdoutencoding'] = sys.argv[next][len('--stdout-encoding='):]
    next = next + 1
  else:
    args['stdoutencoding'] = None

  if sys.argv[next].find('--stdin-encoding=') == 0:
    args['stdinencoding'] = sys.argv[next][len('--stdin-encoding='):]
    next = next + 1
  else:
    args['stdinencoding'] = None

  args['filename'] = filename = _ParseSingleArg(err, next, eval)
  first_addtl_arg = next + 1

  if sys.hexversion < 0x03000000:
    args['filename'] = filename = _ParseSingleArg(err, next, eval)
    first_addtl_arg = next + 1

  # Under Python 3 need to convert file name format used for working
  # around limitations on command line args
  else:
    filename_arg = _ParseSingleArg(err, next)
    
    def py2cvt(fn):
        retval = ''
        inquote = False
        i = 0
        while i < len(fn):
            if fn[i] == "'":
                inquote = not inquote
                retval += fn[i]
                i += 1
            # Rewrite r'' strings to b''
            elif fn[i] == 'r' and not inquote:
                retval += 'b'
                i += 1
            # Rewrite chr(x) with byte identifier
            elif not inquote and fn[i:].startswith('chr('):
                num = int(fn[i+len('chr('):i+fn[i:].find(')')])
                num_hex = "b'\\x%x'" % num
                retval += num_hex
                i += fn[i:].find(')') + 1
            else:
                retval += fn[i]
                i += 1
        return retval
    
    # Assumes reported file system encoding in IDE and debug process match
    try:
      kFileSystemEncoding = sys.getfilesystemencoding()
    except:
      kFileSystemEncoding = None
    try:
      if kFileSystemEncoding is None:
        kFileSystemEncoding = sys.getdefaultencoding()
    except:
      kFileSystemEncoding = 'latin_1'
    
    cvt = py2cvt(filename_arg)
    byte_str = eval(cvt)
    
    args['filename'] = filename = byte_str.decode(kFileSystemEncoding)
    first_addtl_arg = next + 1
    
  # Check if debug file exists and is a file
  if not os.path.exists(filename):
    err.write(_('wingdb.py: Error: Debug file does not exist:'))
    err.write(filename)
    sys.exit(1)
  if not os.path.isfile(filename):
    err.write(_('wingdb.py: Error: Debug file is not a file:'))
    err.write(filename)
    sys.exit(1)
    
  # Prune args down to just args for the debugged program
  del sys.argv[:first_addtl_arg]
  return args


#########################################################################
# Debug server access
#########################################################################
def GetVersionTriple():
  """ Return 3 element tuple (major, minor, micro) for the version of
  the python interpreter we're running in. """
  
  if sys.hexversion >= 0x02030000 and sys.hexversion < 0x02040000:
    ff000000_mask = eval('int("4278190080")')
  else:
    ff000000_mask = eval('0xff000000')
  return ((sys.hexversion & ff000000_mask) >> 24,
          (sys.hexversion & 0x00ff0000) >> 16,
          (sys.hexversion & 0x0000ff00) >> 8)

def GetPossibleServerLocations(winghome, user_settings=None):
  """ Return sequence of locations to look for netserver in. Each location is
  either a directory or a zip file. Look in patch dirs first, then in primary
  bin dir, and finally in source dir. """

  import os

  server_pkg = 'tserver'
    
  major, minor, release = GetVersionTriple()
  interp_id = '%d.%d' % (major, minor)

  # Get patch dirs w/ bin/major.minor
  try:
    exec_dict = {}
    f = open(os.path.join(winghome, 'bin', '_patchsupport.py'))
    try:
      exec(f.read(), exec_dict)
    finally:
      f.close()
    find_matching = exec_dict['FindMatching']
    bin_list = find_matching(interp_id, winghome, user_settings)
  except Exception:
    if kPrintAllTracebacks:
      import traceback
      traceback.print_exc(file = sys.__stderr__)
    bin_list = []

  # Find dirs with debug server dir
  dir_list = []
  for dirname in bin_list:
    server_dir = os.path.join(dirname, 'src', 'debug', server_pkg)
    if os.path.isdir(server_dir):
      dir_list.append(server_dir)
  
  # Finally append default bin & src dirs
  zip_name = os.path.join(winghome, 'bin',  interp_id, 'src.zip')
  if os.path.isfile(zip_name):
    dir_list.append(os.path.join(zip_name, 'debug', server_pkg))
  else:
    dir_list.append(os.path.join(winghome, 'bin',  interp_id, 'src', 'debug', server_pkg))
  dir_list.append(os.path.join(winghome, 'src', 'debug', server_pkg))

  return dir_list
  
def FindNetServerModule(winghome, user_settings=None):
  """ Finds wing's netserver module given winghome path name.  Does not write
  to log so it can be called from wingdbstub. """
  
  # Work around win32 path joining problems
  if sys.platform == 'win32' and winghome[-1] == '\\':
    winghome = winghome[:-1]

  if sys.hexversion < 0x03000000:
    import copy_reg # cPickle is screwed up if we don't hold onto this
  try:
    import encodings # encodings needs to be shared if it's available
  except ImportError:
    encodings = None
  
  prev_mods = list(sys.modules.keys())
  orig_path = list(sys.path)
  try:
    for path in GetPossibleServerLocations(winghome, user_settings):
      sys.path = [path] + orig_path
      try:
        import netserver
        global _
        _ = netserver.abstract._
      except ImportError:
        if has_key(sys.modules, 'netserver'):
          del sys.modules['netserver']
        if kPrintAllTracebacks:
          import traceback
          sys.__stderr__.write('Trying to import netserver from %s\n' % path)
          traceback.print_exc(file = sys.__stderr__)
      else:
        netserver.abstract._SetWingHome(winghome)
        return netserver

    # If reach here, all imports have failed
    raise ImportError('Could not import netserver')
    
  # Restore sys.path and unload modules we added to sys.modules
  finally:
    sys.path = orig_path
    for key in list(sys.modules.keys()):
      if not key in prev_mods:
        del sys.modules[key]

def CreateServer(host, port, attachport, firststop, err, netserver, 
                 pwfile_path):
  """ Creates server. Writes traceback to err and returns None if creation
  fails. """

  # Create the server
  try:  
    err.out(_("Network peer is "), host, "port", port)
    err.out(_("Attach port = "), attachport)
    err.out(_("Initial stop = "), firststop)

    # Only listen locally if attachport is an int or doesn't contain ':'
    if type(attachport) == type(1) or attachport.find(':') == -1:
      attachport = '127.0.0.1:' + str(attachport)
    internal_modules = []
    mod = sys.modules.get(__name__)
    if mod is not None and mod.__dict__ is globals():
      internal_modules.append(mod)
    return netserver.CNetworkServer(host, port, attachport, err, firststop, 
                                    pwfile_path, internal_modules=tuple(internal_modules))

  # Cook exceptions for better display
  except:
    err.out(_("wingdb.py: Could not create debug server"))
    raise
  
def DebugFile(server, filename, err, fs_encoding, sys_path=None):
  """ Debug the given file. Writes any exception to err. """
  
  import os
  
  # Run the session
  try:
    filename = os.path.abspath(filename)
    try:
      pfilename = unicode(filename, fs_encoding)
    except:
      pfilename = filename
    err.out(_("wingdb.py: Running %s") % pfilename)
    os.environ['WINGDB_ACTIVE'] = "1"
    if sys_path is not None:
      sys.path = sys_path
    server.Run(filename, sys.argv)

  # Cook exceptions for better display
  except:
    err.out(_("wingdb.py: Server exiting abnormally on exception"))
    raise

def CreateErrStream(netserver, logfile, very_verbose=0):
  """ Creates error stream for debugger. """
  
  file_list = []
  if logfile != None:
    file_list.append(logfile)
  err = netserver.abstract.CErrStream(file_list, very_verbose=very_verbose)

  return err

def ModifySysPath():
  """ Insert std lib directory at the start of sys.path.  This is usually
  sys.prefix/[Ll]ib but it may not be.  Need w/ Python 2.3
  and 2.4 on win32 which put current dir in the path before the
  std lib -- the bug was fixed in 2.5 """
 
  if sys.platform == 'win32':
    libdir = '%s\\Lib' % sys.prefix
  else:
    libdir = '%s/lib' % sys.prefix
  sys.path.insert(0, libdir)

  import os

  # Adjust sys.path[0] if os is not in sys.prefix\Lib
  if os.path.dirname(os.__file__) != libdir:
    sys.path[0] =  os.path.dirname(os.__file__)
  if sys.platform != 'win32':
    sys.path.insert(1, '%s/lib-dynload' % os.__file__)

def SetEncodingPython2(encoding, file_list, netserver):
  """ Set encoding of all file objects in file_list (Python 2.x only)"""
  
  if sys.hexversion >= 0x03000000:
    raise NotImplementedError
        
  if encoding is None or netserver is None:
    return
  try:
    import codecs
    codec_info = codecs.lookup(encoding)
  except:
    return
  
  try:
    set_file_encoding = netserver.dbgserver.dbgtracer.PyFile_SetEncoding
  except AttributeError:
    return

  for single_file in file_list:
    set_file_encoding(single_file, encoding)
    
def main():
  """ Parse args and then run program. """

  # Make sure we always give opportunity to see output when wait-on-exit is true!
  waitonexit = 0
  server = None
  err = None
  tmp_log = CTempLog()
  try:

    # Pick up exceptions
    args = {}
    try:
      
      # Delete sys.path[0] because it refers to this file's directory then save sys.path
      # for future use
      del sys.path[0]
      orig_sys_path = sys.path[:]
      
      # Modify sys.path on win32 python 2.0, 2.1, 2.3 & 2.4 versions to put stdlib before cwd
      interp_version = GetVersionTriple()
      if (sys.platform == 'win32' and interp_version[0] == 2 
          and interp_version[1] in [0, 1, 3, 4]):
        ModifySysPath()

      # Find directories
      winghome, user_settings = _GetWingDirs()
  
      # Parse args
      args = _ParseArgv(tmp_log)
      waitonexit = args.get('waitonexit', waitonexit)
      
      # Find netserver
      try:
        netserver = FindNetServerModule(winghome, user_settings)
      except:
        import traceback
        if kPrintAllTracebacks:
          traceback.print_exc(file=sys.__stderr__)

        traceback.print_exc(file=tmp_log)
        tmp_log.out(_("wingdb.py: Error: Failed to start the debug server"))
        tmp_log.out(_("wingdb.py: Error: You may be running an unsupported version of Python"))
        tmp_log.out(_("wingdb.py: Python version = %s") % sys.version)
        tmp_log.out("wingdb.py: WINGHOME=%s" % repr(winghome))
        sys.exit(-1)
      
      # Sanity check:  Debugging in optimized mode makes no sense
      if __debug__ == 0:
        tmp_log.out(_("wingdb.py: Error: Cannot run a debug process with optimized python"))
        tmp_log.out(_("wingdb.py: Error: You must omit the -O or -OO command line option, or"))
        tmp_log.out(_("wingdb.py: Error: undefine environment variable PYTHONOPTIMIZE"))
        sys.exit(-1)

      out_encoding = args.get('stdoutencoding')
      in_encoding = args.get('stdinencoding')
      tmp_log.out("Setting stdoutencoding=%s" % str(out_encoding))
      tmp_log.out("Setting stdinencoding=%s" % str(in_encoding))
      if sys.hexversion < 0x02060000:
        SetEncodingPython2(out_encoding, [sys.stdout, sys.stderr], netserver)
        SetEncodingPython2(in_encoding, [sys.stdin], netserver)

      # Create error log and write tmp entries to it
      err = CreateErrStream(netserver, args['logfile'], args['veryverboselog'])
      tmp_log.write_entries(err)
      tmp_log.clear()
      
      # Create the server and run
      err.out("sys.path=%s" % repr(sys.path))
      err.out("sys.argv=%s" % repr(sys.argv))
      pwfile_path = [netserver.abstract.kPWFilePathUserProfileDir]
      server = CreateServer(args['host'], args['port'], args['attachport'], 
                            args['firststop'], err, netserver, pwfile_path)
      DebugFile(server, args['filename'], err, netserver.abstract.kFileSystemEncoding,
                orig_sys_path)

    # Handle any exception that caused debug to fail to start up
    except:
      if kPrintAllTracebacks:
        import traceback
        traceback.print_exc(file=sys.__stderr__)

      # Find log file, if any was specified
      logfile = args.get('logfile')
      opened_file = 0
      if logfile is None or logfile == '<none>' or logfile == '<stderr>':
        file = sys.stderr
      elif logfile == '<stdout>':
        file = sys.stdout
      else:
        try:
          file = open(logfile, "a")
          opened_file = 1
        except (IOError, OSError):
          file = sys.stderr
          
      # Write log/exception info if we have a log file
      if file != None:
        
        # Write any stored up log entries
        for entry in tmp_log.fEntries:
          file.write(entry + '\n')

        # Also print traceback to log file, which can raise exceptions
        import traceback
        try:
          traceback.print_exc(file=file)
        except:
          file.write('Exception raised while printing exc')
          
        # Close file if we opened it
        if opened_file:
          file.close()

  # Always stop server and wait for user to exit if requested
  finally:
    if server != None:
      try: server.Stop()
      except: pass

    if waitonexit:
      line = raw_input(_("-- Type return or enter to exit --\n"))
    
  

#########################################################################
# Execution starts here
#########################################################################

if __name__ == '__main__':
  main()
