def long_tmpdir(request, tmpdir): """Like tmpdir, but ensures that it's a long rather than short filename on Win32. """ path = compat.filename(tmpdir.strpath) buffer = ctypes.create_unicode_buffer(512) if GetLongPathNameW(path, buffer, len(buffer)): path = buffer.value return py.path.local(path)
def configure(self, session): if self._cwd: session.config["cwd"] = self._cwd session.config["program"] = self._get_relative_program() else: session.config["program"] = compat.filename(self.filename.strpath) session.config["args"] = self.args
def __repr__(self): if self._cwd: return fmt( "program (relative) {0!j} / {1!j}", self._cwd, self._get_relative_program(), ) else: return fmt("program {0!j}", compat.filename(self.filename.strpath))
def inject(pid, debugpy_args): host, port = listener.getsockname() cmdline = [ sys.executable, compat.filename(os.path.dirname(debugpy.__file__)), "--connect", host + ":" + str(port), ] if adapter.access_token is not None: cmdline += ["--adapter-access-token", adapter.access_token] cmdline += debugpy_args cmdline += ["--pid", str(pid)] log.info("Spawning attach-to-PID debugger injector: {0!r}", cmdline) try: injector = subprocess.Popen( cmdline, bufsize=0, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) except Exception as exc: log.swallow_exception( "Failed to inject debug server into process with PID={0}", pid ) raise messaging.MessageHandlingError( fmt( "Failed to inject debug server into process with PID={0}: {1}", pid, exc ) ) # We need to capture the output of the injector - otherwise it can get blocked # on a write() syscall when it tries to print something. def capture_output(): while True: line = injector.stdout.readline() if not line: break log.info("Injector[PID={0}] output:\n{1}", pid, line.rstrip()) log.info("Injector[PID={0}] exited.", pid) thread = threading.Thread( target=capture_output, name=fmt("Injector[PID={0}] output", pid) ) thread.daemon = True thread.start()
def parse_argv(): seen = set() it = consume_argv() while True: try: arg = next(it) except StopIteration: raise ValueError("missing target: " + TARGET) switch = compat.filename(arg) if not switch.startswith("-"): switch = "" for pattern, placeholder, action in switches: if re.match("^(" + pattern + ")$", switch): break else: raise ValueError("unrecognized switch " + switch) if switch in seen: raise ValueError("duplicate switch " + switch) else: seen.add(switch) try: action(arg, it) except StopIteration: assert placeholder is not None raise ValueError(fmt("{0}: missing {1}", switch, placeholder)) except Exception as exc: raise ValueError( fmt("invalid {0} {1}: {2}", switch, placeholder, exc)) if options.target is not None: break if options.mode is None: raise ValueError("either --listen or --connect is required") if options.adapter_access_token is not None and options.mode != "connect": raise ValueError("--adapter-access-token requires --connect") if options.target_kind == "pid" and options.wait_for_client: raise ValueError("--pid does not support --wait-for-client") assert options.target is not None assert options.target_kind is not None assert options.address is not None
def factory(source): assert isinstance(source, types.FunctionType) name = source.__name__ source, _ = inspect.getsourcelines(source) # First, find the "def" line. def_lineno = 0 for line in source: line = line.strip() if line.startswith("def") and line.endswith(":"): break def_lineno += 1 else: raise ValueError("Failed to locate function header.") # Remove everything up to and including "def". source = source[def_lineno + 1:] assert source # Now we need to adjust indentation. Compute how much the first line of # the body is indented by, then dedent all lines by that amount. Blank # lines don't matter indentation-wise, and might not be indented to begin # with, so just replace them with a simple newline. line = source[0] indent = len(line) - len(line.lstrip()) source = [l[indent:] if l.strip() else "\n" for l in source] source = "".join(source) # Write it to file. tmpfile = long_tmpdir / (name + ".py") tmpfile.strpath = compat.filename(tmpfile.strpath) assert not tmpfile.check() tmpfile.write(source) tmpfile.lines = code.get_marked_line_numbers(tmpfile) return tmpfile
def launch_request(request): debug_options = set(request("debugOptions", json.array(unicode))) # Handling of properties that can also be specified as legacy "debugOptions" flags. # If property is explicitly set to false, but the flag is in "debugOptions", treat # it as an error. Returns None if the property wasn't explicitly set either way. def property_or_debug_option(prop_name, flag_name): assert prop_name[0].islower() and flag_name[0].isupper() value = request(prop_name, bool, optional=True) if value == (): value = None if flag_name in debug_options: if value is False: raise request.isnt_valid( '{0!j}:false and "debugOptions":[{1!j}] are mutually exclusive', prop_name, flag_name, ) value = True return value python = request("python", json.array(unicode, size=(1,))) cmdline = list(python) if not request("noDebug", json.default(False)): port = request("port", int) cmdline += [ compat.filename(os.path.dirname(debugpy.__file__)), "--connect", launcher.adapter_host + ":" + str(port), ] if not request("subProcess", True): cmdline += ["--configure-subProcess", "False"] qt_mode = request( "qt", json.enum( "none", "auto", "pyside", "pyside2", "pyqt4", "pyqt5", optional=True ), ) cmdline += ["--configure-qt", qt_mode] adapter_access_token = request("adapterAccessToken", unicode, optional=True) if adapter_access_token != (): cmdline += ["--adapter-access-token", compat.filename(adapter_access_token)] debugpy_args = request("debugpyArgs", json.array(unicode)) cmdline += debugpy_args # Further arguments can come via two channels: the launcher's own command line, or # "args" in the request; effective arguments are concatenation of these two in order. # Arguments for debugpy (such as -m) always come via CLI, but those specified by the # user via "args" are passed differently by the adapter depending on "argsExpansion". cmdline += sys.argv[1:] cmdline += request("args", json.array(unicode)) process_name = request("processName", compat.filename(sys.executable)) env = os.environ.copy() env_changes = request("env", json.object((unicode, type(None)))) if sys.platform == "win32": # Environment variables are case-insensitive on Win32, so we need to normalize # both dicts to make sure that env vars specified in the debug configuration # overwrite the global env vars correctly. If debug config has entries that # differ in case only, that's an error. env = {k.upper(): v for k, v in os.environ.items()} n = len(env_changes) env_changes = {k.upper(): v for k, v in env_changes.items()} if len(env_changes) != n: raise request.isnt_valid('Duplicate entries in "env"') if "DEBUGPY_TEST" in env: # If we're running as part of a debugpy test, make sure that codecov is not # applied to the debuggee, since it will conflict with pydevd. env.pop("COV_CORE_SOURCE", None) env.update(env_changes) env = {k: v for k, v in env.items() if v is not None} if request("gevent", False): env["GEVENT_SUPPORT"] = "True" console = request( "console", json.enum( "internalConsole", "integratedTerminal", "externalTerminal", optional=True ), ) redirect_output = property_or_debug_option("redirectOutput", "RedirectOutput") if redirect_output is None: # If neither the property nor the option were specified explicitly, choose # the default depending on console type - "internalConsole" needs it to # provide any output at all, but it's unnecessary for the terminals. redirect_output = console == "internalConsole" if redirect_output: # sys.stdout buffering must be disabled - otherwise we won't see the output # at all until the buffer fills up. env["PYTHONUNBUFFERED"] = "1" # Force UTF-8 output to minimize data loss due to re-encoding. env["PYTHONIOENCODING"] = "utf-8" if property_or_debug_option("waitOnNormalExit", "WaitOnNormalExit"): if console == "internalConsole": raise request.isnt_valid( '"waitOnNormalExit" is not supported for "console":"internalConsole"' ) debuggee.wait_on_exit_predicates.append(lambda code: code == 0) if property_or_debug_option("waitOnAbnormalExit", "WaitOnAbnormalExit"): if console == "internalConsole": raise request.isnt_valid( '"waitOnAbnormalExit" is not supported for "console":"internalConsole"' ) debuggee.wait_on_exit_predicates.append(lambda code: code != 0) if sys.version_info < (3,): # Popen() expects command line and environment to be bytes, not Unicode. # Assume that values are filenames - it's usually either that, or numbers - # but don't allow encoding to fail if we guessed wrong. encode = functools.partial(compat.filename_bytes, errors="replace") cmdline = [encode(s) for s in cmdline] env = {encode(k): encode(v) for k, v in env.items()} debuggee.spawn(process_name, cmdline, env, redirect_output) return {}
def launch_request(self, request): from debugpy.adapter import launchers if self.session.id != 1 or len(servers.connections()): raise request.cant_handle('"attach" expected') debug_options = set(request("debugOptions", json.array(unicode))) # Handling of properties that can also be specified as legacy "debugOptions" flags. # If property is explicitly set to false, but the flag is in "debugOptions", treat # it as an error. Returns None if the property wasn't explicitly set either way. def property_or_debug_option(prop_name, flag_name): assert prop_name[0].islower() and flag_name[0].isupper() value = request(prop_name, bool, optional=True) if value == (): value = None if flag_name in debug_options: if value is False: raise request.isnt_valid( '{0!j}:false and "debugOptions":[{1!j}] are mutually exclusive', prop_name, flag_name, ) value = True return value # "pythonPath" is a deprecated legacy spelling. If "python" is missing, then try # the alternative. But if both are missing, the error message should say "python". python_key = "python" if python_key in request: if "pythonPath" in request: raise request.isnt_valid( '"pythonPath" is not valid if "python" is specified') elif "pythonPath" in request: python_key = "pythonPath" python = request(python_key, json.array(unicode, vectorize=True, size=(0, ))) if not len(python): python = [compat.filename(sys.executable)] python += request("pythonArgs", json.array(unicode, size=(0, ))) request.arguments["pythonArgs"] = python[1:] request.arguments["python"] = python launcher_python = request("debugLauncherPython", unicode, optional=True) if launcher_python == (): launcher_python = python[0] program = module = code = () if "program" in request: program = request("program", unicode) args = [program] request.arguments["processName"] = program if "module" in request: module = request("module", unicode) args = ["-m", module] request.arguments["processName"] = module if "code" in request: code = request("code", json.array(unicode, vectorize=True, size=(1, ))) args = ["-c", "\n".join(code)] request.arguments["processName"] = "-c" num_targets = len([x for x in (program, module, code) if x != ()]) if num_targets == 0: raise request.isnt_valid( 'either "program", "module", or "code" must be specified') elif num_targets != 1: raise request.isnt_valid( '"program", "module", and "code" are mutually exclusive') # Propagate "args" via CLI if and only if shell expansion is requested. args_expansion = request("argsExpansion", json.enum("shell", "none", optional=True)) if args_expansion == "shell": args += request("args", json.array(unicode)) request.arguments.pop("args", None) cwd = request("cwd", unicode, optional=True) if cwd == (): # If it's not specified, but we're launching a file rather than a module, # and the specified path has a directory in it, use that. cwd = None if program == () else (os.path.dirname(program) or None) console = request( "console", json.enum( "internalConsole", "integratedTerminal", "externalTerminal", optional=True, ), ) console_title = request("consoleTitle", json.default("Python Debug Console")) sudo = bool(property_or_debug_option("sudo", "Sudo")) if sudo and sys.platform == "win32": raise request.cant_handle( '"sudo":true is not supported on Windows.') launcher_path = request("debugLauncherPath", os.path.dirname(launcher.__file__)) adapter_host = request("debugAdapterHost", "127.0.0.1") servers.serve(adapter_host) launchers.spawn_debuggee( self.session, request, [launcher_python], launcher_path, adapter_host, args, cwd, console, console_title, sudo, )
def configure(self, session): session.config["program"] = compat.filename(self.filename.strpath) session.config["args"] = self.args
def __repr__(self): return fmt("program {0!j}", compat.filename(self.filename.strpath))
def _get_relative_program(self): assert self._cwd relative_filename = compat.filename( self.filename.strpath)[len(self._cwd):] assert not relative_filename.startswith(("/", "\\")) return relative_filename
def launch_request(request): debug_options = set(request("debugOptions", json.array(unicode))) # Handling of properties that can also be specified as legacy "debugOptions" flags. # If property is explicitly set to false, but the flag is in "debugOptions", treat # it as an error. Returns None if the property wasn't explicitly set either way. def property_or_debug_option(prop_name, flag_name): assert prop_name[0].islower() and flag_name[0].isupper() value = request(prop_name, bool, optional=True) if value == (): value = None if flag_name in debug_options: if value is False: raise request.isnt_valid( '{0!j}:false and "debugOptions":[{1!j}] are mutually exclusive', prop_name, flag_name, ) value = True return value # "pythonPath" is a deprecated legacy spelling. If "python" is missing, then try # the alternative. But if both are missing, the error message should say "python". python_key = "python" if python_key in request: if "pythonPath" in request: raise request.isnt_valid( '"pythonPath" is not valid if "python" is specified') elif "pythonPath" in request: python_key = "pythonPath" cmdline = request(python_key, json.array(unicode, vectorize=True, size=(0, ))) if not len(cmdline): cmdline = [compat.filename(sys.executable)] if not request("noDebug", json.default(False)): port = request("port", int) cmdline += [ compat.filename(os.path.dirname(debugpy.__file__)), "--connect", str(port), ] if not request("subProcess", True): cmdline += ["--configure-subProcess", "False"] adapter_access_token = request("adapterAccessToken", unicode, optional=True) if adapter_access_token != (): cmdline += [ "--adapter-access-token", compat.filename(adapter_access_token) ] debugpy_args = request("debugpyArgs", json.array(unicode)) cmdline += debugpy_args program = module = code = () if "program" in request: program = request("program", unicode) cmdline += [program] process_name = program if "module" in request: module = request("module", unicode) cmdline += ["-m", module] process_name = module if "code" in request: code = request("code", json.array(unicode, vectorize=True, size=(1, ))) cmdline += ["-c", "\n".join(code)] process_name = cmdline[0] num_targets = len([x for x in (program, module, code) if x != ()]) if num_targets == 0: raise request.isnt_valid( 'either "program", "module", or "code" must be specified') elif num_targets != 1: raise request.isnt_valid( '"program", "module", and "code" are mutually exclusive') cmdline += request("args", json.array(unicode)) cwd = request("cwd", unicode, optional=True) if cwd == (): # If it's not specified, but we're launching a file rather than a module, # and the specified path has a directory in it, use that. cwd = None if program == () else (os.path.dirname(program[0]) or None) env = os.environ.copy() env_changes = request("env", json.object(unicode)) if sys.platform == "win32": # Environment variables are case-insensitive on Win32, so we need to normalize # both dicts to make sure that env vars specified in the debug configuration # overwrite the global env vars correctly. If debug config has entries that # differ in case only, that's an error. env = {k.upper(): v for k, v in os.environ.items()} n = len(env_changes) env_changes = {k.upper(): v for k, v in env_changes.items()} if len(env_changes) != n: raise request.isnt_valid('Duplicate entries in "env"') if "DEBUGPY_TEST" in env: # If we're running as part of a debugpy test, make sure that codecov is not # applied to the debuggee, since it will conflict with pydevd. env.pop("COV_CORE_SOURCE", None) env.update(env_changes) if request("gevent", False): env["GEVENT_SUPPORT"] = "True" console = request( "console", json.enum("internalConsole", "integratedTerminal", "externalTerminal", optional=True), ) redirect_output = property_or_debug_option("redirectOutput", "RedirectOutput") if redirect_output is None: # If neither the property nor the option were specified explicitly, choose # the default depending on console type - "internalConsole" needs it to # provide any output at all, but it's unnecessary for the terminals. redirect_output = console == "internalConsole" if redirect_output: # sys.stdout buffering must be disabled - otherwise we won't see the output # at all until the buffer fills up. env["PYTHONUNBUFFERED"] = "1" # Force UTF-8 output to minimize data loss due to re-encoding. env["PYTHONIOENCODING"] = "utf-8" if property_or_debug_option("waitOnNormalExit", "WaitOnNormalExit"): if console == "internalConsole": raise request.isnt_valid( '"waitOnNormalExit" is not supported for "console":"internalConsole"' ) debuggee.wait_on_exit_predicates.append(lambda code: code == 0) if property_or_debug_option("waitOnAbnormalExit", "WaitOnAbnormalExit"): if console == "internalConsole": raise request.isnt_valid( '"waitOnAbnormalExit" is not supported for "console":"internalConsole"' ) debuggee.wait_on_exit_predicates.append(lambda code: code != 0) if sys.version_info < (3, ): # Popen() expects command line and environment to be bytes, not Unicode. # Assume that values are filenames - it's usually either that, or numbers - # but don't allow encoding to fail if we guessed wrong. encode = functools.partial(compat.filename_bytes, errors="replace") cmdline = [encode(s) for s in cmdline] env = {encode(k): encode(v) for k, v in env.items()} debuggee.spawn(process_name, cmdline, cwd, env, redirect_output) return {}