diff -r 866adc8c315b -r 0acf98cd089a eric6/DebugClients/Python/DebugUtilities.py --- a/eric6/DebugClients/Python/DebugUtilities.py Sun Jan 17 13:53:08 2021 +0100 +++ b/eric6/DebugClients/Python/DebugUtilities.py Mon Feb 01 10:38:16 2021 +0100 @@ -8,6 +8,9 @@ """ import json +import os +import traceback +import sys # # Taken from inspect.py of Python 3.4 @@ -101,13 +104,13 @@ @type str @param localsDict reference to the local variables dictionary @type dict - @keyparam formatarg argument formatting function + @param formatarg argument formatting function @type func - @keyparam formatvarargs variable arguments formatting function + @param formatvarargs variable arguments formatting function @type func - @keyparam formatvarkw keyword arguments formatting function + @param formatvarkw keyword arguments formatting function @type func - @keyparam formatvalue value formating functtion + @param formatvalue value formating functtion @type func @return formatted call signature @rtype str @@ -144,3 +147,349 @@ "params": params, } return json.dumps(commandDict) + '\n' + +########################################################################### +## Things related to monkey patching below +########################################################################### + + +PYTHON_NAMES = ["python", "pypy"] + + +def isWindowsPlatform(): + """ + Function to check, if this is a Windows platform. + + @return flag indicating Windows platform + @rtype bool + """ + return sys.platform.startswith(("win", "cygwin")) + + +def isExecutable(program): + """ + Function to check, if the given program is executable. + + @param program program path to be checked + @type str + @return flag indicating an executable program + @rtype bool + """ + return os.access(os.path.abspath(program), os.X_OK) + + +def startsWithShebang(program): + """ + Function to check, if the given program start with a Shebang line. + + @param program program path to be checked + @type str + @return flag indicating an existing and valid shebang line + @rtype bool + """ + try: + if os.path.exists(program): + with open(program) as f: + for line in f: + line = line.strip() + if line: + for name in PYTHON_NAMES: + if line.startswith( + '#!/usr/bin/env {0}'.format(name) + ): + return True + elif line.startswith('#!') and name in line: + return True + return False + else: + return False + except UnicodeDecodeError: + return False + except Exception: + traceback.print_exc() + return False + + +def isPythonProgram(program): + """ + Function to check, if the given program is a Python interpreter or + program. + + @param program program to be checked + @type str + @return flag indicating a Python interpreter or program + @rtype bool + """ + if not program: + return False + + prog = os.path.basename(program).lower() + for pyname in PYTHON_NAMES: + if pyname in prog: + return True + + return ( + not isWindowsPlatform() and + isExecutable(program) and + startsWithShebang(program) + ) + + +def removeQuotesFromArgs(args): + """ + Function to remove quotes from the arguments list. + + @param args list of arguments + @type list of str + @return list of unquoted strings + @rtype list of str + """ + if isWindowsPlatform(): + newArgs = [] + for x in args: + if len(x) > 1 and x.startswith('"') and x.endswith('"'): + x = x[1:-1] + newArgs.append(x) + return newArgs + else: + return args + + +def quoteArgs(args): + """ + Function to quote the given list of arguments. + + @param args list of arguments to be quoted + @type list of str + @return list of quoted arguments + @rtype list of str + """ + if isWindowsPlatform(): + quotedArgs = [] + for x in args: + if x.startswith('"') and x.endswith('"'): + quotedArgs.append(x) + else: + if ' ' in x: + x = x.replace('"', '\\"') + quotedArgs.append('"{0}"'.format(x)) + else: + quotedArgs.append(x) + return quotedArgs + else: + return args + + +def patchArguments(debugClient, arguments, noRedirect=False): + """ + Function to patch the arguments given to start a program in order to + execute it in our debugger. + + @param debugClient reference to the debug client object + @type DebugClient + @param arguments list of program arguments + @type list of str + @param noRedirect flag indicating to not redirect stdin and stdout + @type bool + @return modified argument list + @rtype list of str + """ + debugClientScript = os.path.join( + os.path.dirname(__file__), "DebugClient.py") + if debugClientScript in arguments: + # it is already patched + return arguments + + args = list(arguments[:]) # create a copy of the arguments list + args = removeQuotesFromArgs(args) + + # support for shebang line + program = os.path.basename(args[0]).lower() + for pyname in PYTHON_NAMES: + if pyname in program: + break + else: + if not isWindowsPlatform() and startsWithShebang(args[0]): + # insert our interpreter as first argument + args.insert(0, sys.executable) + elif isWindowsPlatform() and args[0].lower().endswith(".py"): + # it is a Python script; insert our interpreter as first argument + args.insert(0, sys.executable) + + # extract list of interpreter arguments, i.e. all arguments before the + # first one not starting with '-'. + interpreter = args.pop(0) + interpreterArgs = [] + hasCode = False + hasScriptModule = False + while args: + if args[0].startswith("-"): + if args[0] in ("-W", "-X"): + # take two elements off the list + interpreterArgs.append(args.pop(0)) + interpreterArgs.append(args.pop(0)) + elif args[0] == "-c": + # -c indicates code to be executed and ends the + # arguments list + args.pop(0) + hasCode = True + break + elif args[0] == "-m": + # -m indicates a module to be executed as a script + # and ends the arguments list + args.pop(0) + hasScriptModule = True + break + else: + interpreterArgs.append(args.pop(0)) + else: + break + + (wd, host, port, exceptions, tracePython, redirect, noencoding + ) = debugClient.startOptions[:7] + + modifiedArguments = [interpreter] + modifiedArguments.extend(interpreterArgs) + modifiedArguments.extend([ + debugClientScript, + "-h", host, + "-p", str(port), + "--no-passive", + ]) + + if wd: + modifiedArguments.extend(["-w", wd]) + if not exceptions: + modifiedArguments.append("-e") + if tracePython: + modifiedArguments.append("-t") + if noRedirect or not redirect: + modifiedArguments.append("-n") + if noencoding: + modifiedArguments.append("--no-encoding") + if debugClient.multiprocessSupport: + modifiedArguments.append("--multiprocess") + if hasCode: + modifiedArguments.append("--code") + modifiedArguments.append(args.pop(0)) + if hasScriptModule: + modifiedArguments.append("--module") + modifiedArguments.append(args.pop(0)) + modifiedArguments.append("--") + # end the arguments for DebugClient + + # append the arguments for the program to be debugged + modifiedArguments.extend(args) + modifiedArguments = quoteArgs(modifiedArguments) + + return modifiedArguments + + +def stringToArgumentsWindows(args): + """ + Function to prepare a string of arguments for Windows platform. + + @param args list of command arguments + @type str + @return list of command arguments + @rtype list of str + @exception RuntimeError raised to indicate an illegal arguments parsing + condition + """ + # see http:#msdn.microsoft.com/en-us/library/a1y7w461.aspx + result = [] + + DEFAULT = 0 + ARG = 1 + IN_DOUBLE_QUOTE = 2 + + state = DEFAULT + backslashes = 0 + buf = '' + + argsLen = len(args) + for i in range(argsLen): + ch = args[i] + if ch == '\\': + backslashes += 1 + continue + elif backslashes != 0: + if ch == '"': + while backslashes >= 2: + backslashes -= 2 + buf += '\\' + if backslashes == 1: + if state == DEFAULT: + state = ARG + + buf += '"' + backslashes = 0 + continue + else: + # false alarm, treat passed backslashes literally... + if state == DEFAULT: + state = ARG + + while backslashes > 0: + backslashes -= 1 + buf += '\\' + + if ch in (' ', '\t'): + if state == DEFAULT: + # skip + continue + elif state == ARG: + state = DEFAULT + result.append(buf) + buf = '' + continue + + if state in (DEFAULT, ARG): + if ch == '"': + state = IN_DOUBLE_QUOTE + else: + state = ARG + buf += ch + + elif state == IN_DOUBLE_QUOTE: + if ch == '"': + if i + 1 < argsLen and args[i + 1] == '"': + # Undocumented feature in Windows: + # Two consecutive double quotes inside a double-quoted + # argument are interpreted as a single double quote. + buf += '"' + i += 1 + elif len(buf) == 0: + result.append("\"\"") + state = DEFAULT + else: + state = ARG + else: + buf += ch + + else: + raise RuntimeError('Illegal condition') + + if len(buf) > 0 or state != DEFAULT: + result.append(buf) + + return result + + +def patchArgumentStringWindows(debugClient, argStr): + """ + Function to patch an argument string for Windows. + + @param debugClient reference to the debug client object + @type DebugClient + @param argStr argument string + @type str + @return patched argument string + @rtype str + """ + args = stringToArgumentsWindows(argStr) + if not args or not isPythonProgram(args[0]): + return argStr + + argStr = ' '.join(patchArguments(debugClient, args)) + return argStr