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') # Launcher doesn't use the command line at all, but we pass the arguments so # that they show up in the terminal if we're using "runInTerminal". if "program" in request: args = request("program", json.array(unicode, vectorize=True, size=(1,))) elif "module" in request: args = ["-m"] + request( "module", json.array(unicode, vectorize=True, size=(1,)) ) elif "code" in request: args = ["-c"] + request( "code", json.array(unicode, vectorize=True, size=(1,)) ) else: args = [] args += request("args", json.array(unicode)) console = request( "console", json.enum( "internalConsole", "integratedTerminal", "externalTerminal", optional=True, ), ) console_title = request("consoleTitle", json.default("Python Debug Console")) servers.serve() launchers.spawn_debuggee(self.session, request, args, console, console_title)
def get_variables(self, *varnames, **kwargs): """Fetches the specified variables from the frame specified by frame_id, or from the topmost frame in the last "stackTrace" response if frame_id is not specified. If varnames is empty, then all variables in the frame are returned. The result is an OrderedDict, in which every entry has variable name as the key, and a DAP Variable object as the value. The original order of variables as reported by the debugger is preserved. If varnames is not empty, then only the specified variables are returned. The result is a tuple, in which every entry is a DAP Variable object; those entries are in the same order as varnames. """ assert self.timeline.is_frozen frame_id = kwargs.pop("frame_id", None) if frame_id is None: stackTrace_responses = self.all_occurrences_of( timeline.Response(timeline.Request("stackTrace"))) assert stackTrace_responses, ( "get_variables() without frame_id requires at least one response " 'to a "stackTrace" request in the timeline.') stack_trace = stackTrace_responses[-1] frame_id = stack_trace.body.get("stackFrames", json.array())[0]("id", int) scopes = self.request("scopes", {"frameId": frame_id})("scopes", json.array()) assert len(scopes) > 0 variables = self.request( "variables", {"variablesReference": scopes[0]("variablesReference", int)})( "variables", json.array()) variables = collections.OrderedDict( ((v("name", unicode), v) for v in variables)) if varnames: assert set(varnames) <= set(variables.keys()) return tuple((variables[name] for name in varnames)) else: return variables
def _process_request(self, request): self.timeline.record_request(request, block=False) if request.command == "runInTerminal": args = request("args", json.array(unicode)) cwd = request("cwd", ".") env = request("env", json.object(unicode)) try: return self.run_in_terminal(args, cwd, env) except Exception as exc: log.swallow_exception('"runInTerminal" failed:') raise request.cant_handle(str(exc)) else: raise request.isnt_valid("not supported")
def _process_request(self, request): self.timeline.record_request(request, block=False) if request.command == "runInTerminal": args = request("args", json.array(unicode)) cwd = request("cwd", ".") env = request("env", json.object(unicode)) try: exe = args.pop(0) assert not len(self.spawn_debuggee.env) self.spawn_debuggee.env = env self.spawn_debuggee(args, cwd, exe=exe) return {} except OSError as exc: log.swallow_exception('"runInTerminal" failed:') raise request.cant_handle(str(exc)) else: raise request.isnt_valid("not supported")
def set_breakpoints(self, path, lines): """Sets breakpoints in the specified file, and returns the list of all the corresponding DAP Breakpoint objects in the same order. If lines are specified, it should be an iterable in which every element is either a line number or a string. If it is a string, then it is translated to the corresponding line number via get_marked_line_numbers(path). If lines=all, breakpoints will be set on all the marked lines in the file. """ # Don't fetch line markers unless needed - in some cases, the breakpoints # might be set in a file that does not exist on disk (e.g. remote attach). get_marked_line_numbers = lambda: code.get_marked_line_numbers(path) if lines is all: lines = get_marked_line_numbers().keys() def make_breakpoint(line): if isinstance(line, int): descr = str(line) else: marker = line line = get_marked_line_numbers()[marker] descr = fmt("{0} (@{1})", line, marker) bp_log.append((line, descr)) return {"line": line} bp_log = [] breakpoints = self.request( "setBreakpoints", { "source": { "path": path }, "breakpoints": [make_breakpoint(line) for line in lines], }, )("breakpoints", json.array()) bp_log = sorted(bp_log, key=lambda pair: pair[0]) bp_log = ", ".join((descr for _, descr in bp_log)) log.info("Breakpoints set in {0}: {1}", path, bp_log) return breakpoints
def wait_for_stop( self, reason=some.str, expected_frames=None, expected_text=None, expected_description=None, ): stopped = self.wait_for_next_event("stopped") expected_stopped = { "reason": reason, "threadId": some.int, "allThreadsStopped": True, } if expected_text is not None: expected_stopped["text"] = expected_text if expected_description is not None: expected_stopped["description"] = expected_description if stopped("reason", unicode) not in [ "step", "exception", "breakpoint", "entry", "goto", ]: expected_stopped["preserveFocusHint"] = True assert stopped == some.dict.containing(expected_stopped) tid = stopped("threadId", int) stack_trace = self.request("stackTrace", {"threadId": tid}) frames = stack_trace("stackFrames", json.array()) or [] assert len(frames) == stack_trace("totalFrames", int) if expected_frames: assert len(expected_frames) <= len(frames) assert expected_frames == frames[0:len(expected_frames)] fid = frames[0]("id", int) return StopInfo(stopped, frames, tid, fid)
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 attach_request(self, request): if self.session.no_debug: raise request.isnt_valid('"noDebug" is not supported for "attach"') host = request("host", unicode, optional=True) port = request("port", int, optional=True) listen = request("listen", dict, optional=True) connect = request("connect", dict, optional=True) pid = request("processId", (int, unicode), optional=True) sub_pid = request("subProcessId", int, optional=True) if host != () or port != (): if listen != (): raise request.isnt_valid( '"listen" and "host"/"port" are mutually exclusive') if connect != (): raise request.isnt_valid( '"connect" and "host"/"port" are mutually exclusive') if listen != (): if connect != (): raise request.isnt_valid( '"listen" and "connect" are mutually exclusive') if pid != (): raise request.isnt_valid( '"listen" and "processId" are mutually exclusive') if sub_pid != (): raise request.isnt_valid( '"listen" and "subProcessId" are mutually exclusive') if pid != () and sub_pid != (): raise request.isnt_valid( '"processId" and "subProcessId" are mutually exclusive') if listen != (): host = listen("host", "127.0.0.1") port = listen("port", int) adapter.access_token = None host, port = servers.serve(host, port) else: host, port = servers.serve() # There are four distinct possibilities here. # # If "processId" is specified, this is attach-by-PID. We need to inject the # debug server into the designated process, and then wait until it connects # back to us. Since the injected server can crash, there must be a timeout. # # If "subProcessId" is specified, this is attach to a known subprocess, likely # in response to a "debugpyAttach" event. If so, the debug server should be # connected already, and thus the wait timeout is zero. # # If "listen" is specified, this is attach-by-socket with the server expected # to connect to the adapter via debugpy.connect(). There is no PID known in # advance, so just wait until the first server connection indefinitely, with # no timeout. # # If "connect" is specified, this is attach-by-socket in which the server has # spawned the adapter via debugpy.listen(). There is no PID known to the client # in advance, but the server connection should be either be there already, or # the server should be connecting shortly, so there must be a timeout. # # In the last two cases, if there's more than one server connection already, # this is a multiprocess re-attach. The client doesn't know the PID, so we just # connect it to the oldest server connection that we have - in most cases, it # will be the one for the root debuggee process, but if it has exited already, # it will be some subprocess. if pid != (): if not isinstance(pid, int): try: pid = int(pid) except Exception: raise request.isnt_valid( '"processId" must be parseable as int') debugpy_args = request("debugpyArgs", json.array(unicode)) servers.inject(pid, debugpy_args) timeout = common.PROCESS_SPAWN_TIMEOUT pred = lambda conn: conn.pid == pid else: if sub_pid == (): pred = lambda conn: True timeout = common.PROCESS_SPAWN_TIMEOUT if listen == ( ) else None else: pred = lambda conn: conn.pid == sub_pid timeout = 0 self.channel.send_event("debugpyWaitingForServer", { "host": host, "port": port }) conn = servers.wait_for_connection(self.session, pred, timeout) if conn is None: if sub_pid != (): # If we can't find a matching subprocess, it's not always an error - # it might have already exited, or didn't even get a chance to connect. # To prevent the client from complaining, pretend that the "attach" # request was successful, but that the session terminated immediately. request.respond({}) self.session.finalize( fmt('No known subprocess with "subProcessId":{0}', sub_pid)) return raise request.cant_handle( ("Timed out waiting for debug server to connect." if timeout else "There is no debug server connected to this adapter."), sub_pid, ) try: conn.attach_to_session(self.session) except ValueError: request.cant_handle("{0} is already being debugged.", conn)
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 handle(self, request): assert request.is_request("launch", "attach") if self._initialize_request is None: raise request.isnt_valid("Session is not initialized yet") if self.launcher or self.server: raise request.isnt_valid("Session is already started") self.session.no_debug = request("noDebug", json.default(False)) if self.session.no_debug: servers.dont_wait_for_first_connection() self.session.debug_options = debug_options = set( request("debugOptions", json.array(unicode))) f(self, request) if request.response is not None: return if self.server: self.server.initialize(self._initialize_request) self._initialize_request = None arguments = request.arguments if self.launcher: if "RedirectOutput" in debug_options: # The launcher is doing output redirection, so we don't need the # server to do it, as well. arguments = dict(arguments) arguments["debugOptions"] = list(debug_options - {"RedirectOutput"}) if arguments.get("redirectOutput"): arguments = dict(arguments) del arguments["redirectOutput"] # pydevd doesn't send "initialized", and responds to the start request # immediately, without waiting for "configurationDone". If it changes # to conform to the DAP spec, we'll need to defer waiting for response. try: self.server.channel.request(request.command, arguments) except messaging.NoMoreMessages: # Server closed connection before we could receive the response to # "attach" or "launch" - this can happen when debuggee exits shortly # after starting. It's not an error, but we can't do anything useful # here at this point, either, so just bail out. request.respond({}) self.session.finalize( fmt( "{0} disconnected before responding to {1!j}", self.server, request.command, )) return except messaging.MessageHandlingError as exc: exc.propagate(request) if self.session.no_debug: self.start_request = request self.has_started = True request.respond({}) self._propagate_deferred_events() return if {"WindowsClient", "Windows"} & debug_options: client_os_type = "WINDOWS" elif {"UnixClient", "UNIX"} & debug_options: client_os_type = "UNIX" else: client_os_type = "WINDOWS" if sys.platform == "win32" else "UNIX" self.server.channel.request( "setDebuggerProperty", { "skipSuspendOnBreakpointException": ("BaseException", ), "skipPrintBreakpointException": ("NameError", ), "multiThreadsSingleNotification": True, "ideOS": client_os_type, }, ) # Let the client know that it can begin configuring the adapter. self.channel.send_event("initialized") self.start_request = request return messaging.NO_RESPONSE # will respond on "configurationDone"
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 # Launcher doesn't use the command line at all, but we pass the arguments so # that they show up in the terminal if we're using "runInTerminal". if "program" in request: args = request("program", json.array(unicode, vectorize=True, size=(1, ))) elif "module" in request: args = ["-m"] + request( "module", json.array(unicode, vectorize=True, size=(1, ))) elif "code" in request: args = ["-c"] + request( "code", json.array(unicode, vectorize=True, size=(1, ))) else: args = [] args += request("args", json.array(unicode)) 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.') servers.serve() launchers.spawn_debuggee(self.session, request, args, console, console_title, sudo)
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 {}