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 configurationDone_request(self, request): if self.start_request is None or self.has_started: request.cant_handle( '"configurationDone" is only allowed during handling of a "launch" ' 'or an "attach" request') try: self.has_started = True request.respond(self.server.channel.delegate(request)) except messaging.MessageHandlingError as exc: self.start_request.cant_handle(str(exc)) finally: self.start_request.respond({}) self._propagate_deferred_events() # Notify the client of any child processes of the debuggee that aren't already # being debugged. for conn in servers.connections(): if conn.server is None and conn.ppid == self.session.pid: self.notify_of_subprocess(conn)
def configurationDone_request(self, request): if self.start_request is None or self.has_started: request.cant_handle( '"configurationDone" is only allowed during handling of a "launch" ' 'or an "attach" request' ) try: self.has_started = True try: result = self.server.channel.delegate(request) except messaging.NoMoreMessages: # Server closed connection before we could receive the response to # "configurationDone" - 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.start_request.respond({}) self.session.finalize( fmt( "{0} disconnected before responding to {1!j}", self.server, request.command, ) ) return else: request.respond(result) except messaging.MessageHandlingError as exc: self.start_request.cant_handle(str(exc)) finally: if self.start_request.response is None: self.start_request.respond({}) self._propagate_deferred_events() # Notify the client of any child processes of the debuggee that aren't already # being debugged. for conn in servers.connections(): if conn.server is None and conn.ppid == self.session.pid: self.notify_of_subprocess(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 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 _finalize(self, why, terminate_debuggee): # If the client started a session, and then disconnected before issuing "launch" # or "attach", the main thread will be blocked waiting for the first server # connection to come in - unblock it, so that we can exit. servers.dont_wait_for_first_connection() if self.server: if self.server.is_connected: if terminate_debuggee and self.launcher and self.launcher.is_connected: # If we were specifically asked to terminate the debuggee, and we # can ask the launcher to kill it, do so instead of disconnecting # from the server to prevent debuggee from running any more code. self.launcher.terminate_debuggee() else: # Otherwise, let the server handle it the best it can. try: self.server.channel.request( "disconnect", {"terminateDebuggee": terminate_debuggee}) except Exception: pass self.server.detach_from_session() if self.launcher and self.launcher.is_connected: # If there was a server, we just disconnected from it above, which should # cause the debuggee process to exit - so let's wait for that first. if self.server: log.info('{0} waiting for "exited" event...', self) if not self.wait_for( lambda: self.launcher.exit_code is not None, timeout=5): log.warning('{0} timed out waiting for "exited" event.', self) # Terminate the debuggee process if it's still alive for any reason - # whether it's because there was no server to handle graceful shutdown, # or because the server couldn't handle it for some reason. self.launcher.terminate_debuggee() # Wait until the launcher message queue fully drains. There is no timeout # here, because the final "terminated" event will only come after reading # user input in wait-on-exit scenarios. log.info("{0} waiting for {1} to disconnect...", self, self.launcher) self.wait_for(lambda: not self.launcher.is_connected) try: self.launcher.channel.close() except Exception: log.swallow_exception() if self.client: if self.client.is_connected: # Tell the client that debugging is over, but don't close the channel until it # tells us to, via the "disconnect" request. try: self.client.channel.send_event("terminated") except Exception: pass if (self.client.start_request is not None and self.client.start_request.command == "launch"): servers.stop_serving() log.info( '"launch" session ended - killing remaining debuggee processes.' ) pids_killed = set() if self.launcher and self.launcher.pid is not None: # Already killed above. pids_killed.add(self.launcher.pid) while True: conns = [ conn for conn in servers.connections() if conn.pid not in pids_killed ] if not len(conns): break for conn in conns: log.info("Killing {0}", conn) try: os.kill(conn.pid, signal.SIGTERM) except Exception: log.swallow_exception("Failed to kill {0}", conn) pids_killed.add(conn.pid)