Spaces:
				
			
			
	
			
			
		Paused
		
	
	
	
			
			
	
	
	
	
		
		
		Paused
		
	| # Copyright (c) Microsoft Corporation. All rights reserved. | |
| # Licensed under the MIT License. See LICENSE in the project root | |
| # for license information. | |
| from __future__ import annotations | |
| import atexit | |
| import os | |
| import sys | |
| import debugpy | |
| from debugpy import adapter, common, launcher | |
| from debugpy.common import json, log, messaging, sockets | |
| from debugpy.adapter import clients, components, launchers, servers, sessions | |
| class Client(components.Component): | |
| """Handles the client side of a debug session.""" | |
| message_handler = components.Component.message_handler | |
| known_subprocesses: set[servers.Connection] | |
| """Server connections to subprocesses that this client has been made aware of. | |
| """ | |
| class Capabilities(components.Capabilities): | |
| PROPERTIES = { | |
| "supportsVariableType": False, | |
| "supportsVariablePaging": False, | |
| "supportsRunInTerminalRequest": False, | |
| "supportsMemoryReferences": False, | |
| "supportsArgsCanBeInterpretedByShell": False, | |
| "supportsStartDebuggingRequest": False, | |
| } | |
| class Expectations(components.Capabilities): | |
| PROPERTIES = { | |
| "locale": "en-US", | |
| "linesStartAt1": True, | |
| "columnsStartAt1": True, | |
| "pathFormat": json.enum("path", optional=True), # we don't support "uri" | |
| } | |
| def __init__(self, sock): | |
| if sock == "stdio": | |
| log.info("Connecting to client over stdio...", self) | |
| self.using_stdio = True | |
| stream = messaging.JsonIOStream.from_stdio() | |
| # Make sure that nothing else tries to interfere with the stdio streams | |
| # that are going to be used for DAP communication from now on. | |
| sys.stdin = stdin = open(os.devnull, "r") | |
| atexit.register(stdin.close) | |
| sys.stdout = stdout = open(os.devnull, "w") | |
| atexit.register(stdout.close) | |
| else: | |
| self.using_stdio = False | |
| stream = messaging.JsonIOStream.from_socket(sock) | |
| with sessions.Session() as session: | |
| super().__init__(session, stream) | |
| self.client_id = None | |
| """ID of the connecting client. This can be 'test' while running tests.""" | |
| self.has_started = False | |
| """Whether the "launch" or "attach" request was received from the client, and | |
| fully handled. | |
| """ | |
| self.start_request = None | |
| """The "launch" or "attach" request as received from the client. | |
| """ | |
| self.restart_requested = False | |
| """Whether the client requested the debug adapter to be automatically | |
| restarted via "restart":true in the start request. | |
| """ | |
| self._initialize_request = None | |
| """The "initialize" request as received from the client, to propagate to the | |
| server later.""" | |
| self._deferred_events = [] | |
| """Deferred events from the launcher and the server that must be propagated | |
| only if and when the "launch" or "attach" response is sent. | |
| """ | |
| self._forward_terminate_request = False | |
| self.known_subprocesses = set() | |
| session.client = self | |
| session.register() | |
| # For the transition period, send the telemetry events with both old and new | |
| # name. The old one should be removed once the new one lights up. | |
| self.channel.send_event( | |
| "output", | |
| { | |
| "category": "telemetry", | |
| "output": "ptvsd", | |
| "data": {"packageVersion": debugpy.__version__}, | |
| }, | |
| ) | |
| self.channel.send_event( | |
| "output", | |
| { | |
| "category": "telemetry", | |
| "output": "debugpy", | |
| "data": {"packageVersion": debugpy.__version__}, | |
| }, | |
| ) | |
| sessions.report_sockets() | |
| def propagate_after_start(self, event): | |
| # pydevd starts sending events as soon as we connect, but the client doesn't | |
| # expect to see any until it receives the response to "launch" or "attach" | |
| # request. If client is not ready yet, save the event instead of propagating | |
| # it immediately. | |
| if self._deferred_events is not None: | |
| self._deferred_events.append(event) | |
| log.debug("Propagation deferred.") | |
| else: | |
| self.client.channel.propagate(event) | |
| def _propagate_deferred_events(self): | |
| log.debug("Propagating deferred events to {0}...", self.client) | |
| for event in self._deferred_events: | |
| log.debug("Propagating deferred {0}", event.describe()) | |
| self.client.channel.propagate(event) | |
| log.info("All deferred events propagated to {0}.", self.client) | |
| self._deferred_events = None | |
| # Generic event handler. There are no specific handlers for client events, because | |
| # there are no events from the client in DAP - but we propagate them if we can, in | |
| # case some events appear in future protocol versions. | |
| def event(self, event): | |
| if self.server: | |
| self.server.channel.propagate(event) | |
| # Generic request handler, used if there's no specific handler below. | |
| def request(self, request): | |
| return self.server.channel.delegate(request) | |
| def initialize_request(self, request): | |
| if self._initialize_request is not None: | |
| raise request.isnt_valid("Session is already initialized") | |
| self.client_id = request("clientID", "") | |
| self.capabilities = self.Capabilities(self, request) | |
| self.expectations = self.Expectations(self, request) | |
| self._initialize_request = request | |
| exception_breakpoint_filters = [ | |
| { | |
| "filter": "raised", | |
| "label": "Raised Exceptions", | |
| "default": False, | |
| "description": "Break whenever any exception is raised.", | |
| }, | |
| { | |
| "filter": "uncaught", | |
| "label": "Uncaught Exceptions", | |
| "default": True, | |
| "description": "Break when the process is exiting due to unhandled exception.", | |
| }, | |
| { | |
| "filter": "userUnhandled", | |
| "label": "User Uncaught Exceptions", | |
| "default": False, | |
| "description": "Break when exception escapes into library code.", | |
| }, | |
| ] | |
| return { | |
| "supportsCompletionsRequest": True, | |
| "supportsConditionalBreakpoints": True, | |
| "supportsConfigurationDoneRequest": True, | |
| "supportsDebuggerProperties": True, | |
| "supportsDelayedStackTraceLoading": True, | |
| "supportsEvaluateForHovers": True, | |
| "supportsExceptionInfoRequest": True, | |
| "supportsExceptionOptions": True, | |
| "supportsFunctionBreakpoints": True, | |
| "supportsHitConditionalBreakpoints": True, | |
| "supportsLogPoints": True, | |
| "supportsModulesRequest": True, | |
| "supportsSetExpression": True, | |
| "supportsSetVariable": True, | |
| "supportsValueFormattingOptions": True, | |
| "supportsTerminateRequest": True, | |
| "supportsGotoTargetsRequest": True, | |
| "supportsClipboardContext": True, | |
| "exceptionBreakpointFilters": exception_breakpoint_filters, | |
| "supportsStepInTargetsRequest": True, | |
| } | |
| # Common code for "launch" and "attach" request handlers. | |
| # | |
| # See https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522 | |
| # for the sequence of request and events necessary to orchestrate the start. | |
| def _start_message_handler(f): | |
| 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(str)) | |
| ) | |
| 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: | |
| redirecting = arguments.get("console") == "internalConsole" | |
| 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"} | |
| ) | |
| redirecting = True | |
| if arguments.get("redirectOutput"): | |
| arguments = dict(arguments) | |
| del arguments["redirectOutput"] | |
| redirecting = True | |
| arguments["isOutputRedirected"] = redirecting | |
| # 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( | |
| "{0} disconnected before responding to {1}".format( | |
| self.server, | |
| json.repr(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 | |
| # 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" | |
| return handle | |
| 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(str))) | |
| # 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}:false and "debugOptions":[{1}] are mutually exclusive', | |
| json.repr(prop_name), | |
| json.repr(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(str, vectorize=True, size=(0,))) | |
| if not len(python): | |
| python = [sys.executable] | |
| python += request("pythonArgs", json.array(str, size=(0,))) | |
| request.arguments["pythonArgs"] = python[1:] | |
| request.arguments["python"] = python | |
| launcher_python = request("debugLauncherPython", str, optional=True) | |
| if launcher_python == (): | |
| launcher_python = python[0] | |
| program = module = code = () | |
| if "program" in request: | |
| program = request("program", str) | |
| args = [program] | |
| request.arguments["processName"] = program | |
| if "module" in request: | |
| module = request("module", str) | |
| args = ["-m", module] | |
| request.arguments["processName"] = module | |
| if "code" in request: | |
| code = request("code", json.array(str, 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' | |
| ) | |
| console = request( | |
| "console", | |
| json.enum( | |
| "internalConsole", | |
| "integratedTerminal", | |
| "externalTerminal", | |
| optional=True, | |
| ), | |
| ) | |
| console_title = request("consoleTitle", json.default("Python Debug Console")) | |
| # Propagate "args" via CLI so that shell expansion can be applied if requested. | |
| target_args = request("args", json.array(str, vectorize=True)) | |
| args += target_args | |
| # If "args" was a single string rather than an array, shell expansion must be applied. | |
| shell_expand_args = len(target_args) > 0 and isinstance( | |
| request.arguments["args"], str | |
| ) | |
| if shell_expand_args: | |
| if not self.capabilities["supportsArgsCanBeInterpretedByShell"]: | |
| raise request.isnt_valid( | |
| 'Shell expansion in "args" is not supported by the client' | |
| ) | |
| if console == "internalConsole": | |
| raise request.isnt_valid( | |
| 'Shell expansion in "args" is not available for "console":"internalConsole"' | |
| ) | |
| cwd = request("cwd", str, 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) | |
| 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.') | |
| on_terminate = request("onTerminate", str, optional=True) | |
| if on_terminate: | |
| self._forward_terminate_request = on_terminate == "KeyboardInterrupt" | |
| launcher_path = request("debugLauncherPath", os.path.dirname(launcher.__file__)) | |
| adapter_host = request("debugAdapterHost", "127.0.0.1") | |
| try: | |
| servers.serve(adapter_host) | |
| except Exception as exc: | |
| raise request.cant_handle( | |
| "{0} couldn't create listener socket for servers: {1}", | |
| self.session, | |
| exc, | |
| ) | |
| launchers.spawn_debuggee( | |
| self.session, | |
| request, | |
| [launcher_python], | |
| launcher_path, | |
| adapter_host, | |
| args, | |
| shell_expand_args, | |
| cwd, | |
| console, | |
| console_title, | |
| sudo, | |
| ) | |
| def attach_request(self, request): | |
| if self.session.no_debug: | |
| raise request.isnt_valid('"noDebug" is not supported for "attach"') | |
| host = request("host", str, optional=True) | |
| port = request("port", int, optional=True) | |
| listen = request("listen", dict, optional=True) | |
| connect = request("connect", dict, optional=True) | |
| pid = request("processId", (int, str), optional=True) | |
| sub_pid = request("subProcessId", int, optional=True) | |
| on_terminate = request("onTerminate", bool, optional=True) | |
| if on_terminate: | |
| self._forward_terminate_request = on_terminate == "KeyboardInterrupt" | |
| 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 != (): | |
| if servers.is_serving(): | |
| raise request.isnt_valid( | |
| 'Multiple concurrent "listen" sessions are not supported' | |
| ) | |
| host = listen("host", "127.0.0.1") | |
| port = listen("port", int) | |
| adapter.access_token = None | |
| self.restart_requested = request("restart", False) | |
| host, port = servers.serve(host, port) | |
| else: | |
| if not servers.is_serving(): | |
| servers.serve() | |
| host, port = servers.listener.getsockname() | |
| # 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(str)) | |
| def on_output(category, output): | |
| self.channel.send_event( | |
| "output", | |
| { | |
| "category": category, | |
| "output": output, | |
| }, | |
| ) | |
| try: | |
| servers.inject(pid, debugpy_args, on_output) | |
| except Exception as e: | |
| log.swallow_exception() | |
| self.session.finalize( | |
| "Error when trying to attach to PID:\n%s" % (str(e),) | |
| ) | |
| return | |
| 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( | |
| 'No known subprocess with "subProcessId":{0}'.format(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 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( | |
| "{0} disconnected before responding to {1}".format( | |
| self.server, | |
| json.repr(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 evaluate_request(self, request): | |
| propagated_request = self.server.channel.propagate(request) | |
| def handle_response(response): | |
| request.respond(response.body) | |
| propagated_request.on_response(handle_response) | |
| return messaging.NO_RESPONSE | |
| def pause_request(self, request): | |
| request.arguments["threadId"] = "*" | |
| return self.server.channel.delegate(request) | |
| def continue_request(self, request): | |
| request.arguments["threadId"] = "*" | |
| try: | |
| return self.server.channel.delegate(request) | |
| except messaging.NoMoreMessages: | |
| # pydevd can sometimes allow the debuggee to exit before the queued | |
| # "continue" response gets sent. Thus, a failed "continue" response | |
| # indicating that the server disconnected should be treated as success. | |
| return {"allThreadsContinued": True} | |
| def debugpySystemInfo_request(self, request): | |
| result = {"debugpy": {"version": debugpy.__version__}} | |
| if self.server: | |
| try: | |
| pydevd_info = self.server.channel.request("pydevdSystemInfo") | |
| except Exception: | |
| # If the server has already disconnected, or couldn't handle it, | |
| # report what we've got. | |
| pass | |
| else: | |
| result.update(pydevd_info) | |
| return result | |
| def terminate_request(self, request): | |
| # If user specifically requests to terminate, it means that they don't want | |
| # debug session auto-restart kicking in. | |
| self.restart_requested = False | |
| if self._forward_terminate_request: | |
| # According to the spec, terminate should try to do a gracefull shutdown. | |
| # We do this in the server by interrupting the main thread with a Ctrl+C. | |
| # To force the kill a subsequent request would do a disconnect. | |
| # | |
| # We only do this if the onTerminate option is set though (the default | |
| # is a hard-kill for the process and subprocesses). | |
| return self.server.channel.delegate(request) | |
| self.session.finalize('client requested "terminate"', terminate_debuggee=True) | |
| return {} | |
| def disconnect_request(self, request): | |
| # If user specifically requests to disconnect, it means that they don't want | |
| # debug session auto-restart kicking in. | |
| self.restart_requested = False | |
| terminate_debuggee = request("terminateDebuggee", bool, optional=True) | |
| if terminate_debuggee == (): | |
| terminate_debuggee = None | |
| self.session.finalize('client requested "disconnect"', terminate_debuggee) | |
| request.respond({}) | |
| if self.using_stdio: | |
| # There's no way for the client to reconnect to this adapter once it disconnects | |
| # from this session, so close any remaining server connections. | |
| servers.stop_serving() | |
| log.info("{0} disconnected from stdio; closing remaining server connections.", self) | |
| for conn in servers.connections(): | |
| try: | |
| conn.channel.close() | |
| except Exception: | |
| log.swallow_exception() | |
| def disconnect(self): | |
| super().disconnect() | |
| def report_sockets(self): | |
| sockets = [ | |
| { | |
| "host": host, | |
| "port": port, | |
| "internal": listener is not clients.listener, | |
| } | |
| for listener in [clients.listener, launchers.listener, servers.listener] | |
| if listener is not None | |
| for (host, port) in [listener.getsockname()] | |
| ] | |
| self.channel.send_event( | |
| "debugpySockets", | |
| { | |
| "sockets": sockets | |
| }, | |
| ) | |
| def notify_of_subprocess(self, conn): | |
| log.info("{1} is a subprocess of {0}.", self, conn) | |
| with self.session: | |
| if self.start_request is None or conn in self.known_subprocesses: | |
| return | |
| if "processId" in self.start_request.arguments: | |
| log.warning( | |
| "Not reporting subprocess for {0}, because the parent process " | |
| 'was attached to using "processId" rather than "port".', | |
| self.session, | |
| ) | |
| return | |
| log.info("Notifying {0} about {1}.", self, conn) | |
| body = dict(self.start_request.arguments) | |
| self.known_subprocesses.add(conn) | |
| self.session.notify_changed() | |
| for key in "processId", "listen", "preLaunchTask", "postDebugTask", "request", "restart": | |
| body.pop(key, None) | |
| body["name"] = "Subprocess {0}".format(conn.pid) | |
| body["subProcessId"] = conn.pid | |
| for key in "args", "processName", "pythonArgs": | |
| body.pop(key, None) | |
| host = body.pop("host", None) | |
| port = body.pop("port", None) | |
| if "connect" not in body: | |
| body["connect"] = {} | |
| if "host" not in body["connect"]: | |
| body["connect"]["host"] = host if host is not None else "127.0.0.1" | |
| if "port" not in body["connect"]: | |
| if port is None: | |
| _, port = listener.getsockname() | |
| body["connect"]["port"] = port | |
| if self.capabilities["supportsStartDebuggingRequest"]: | |
| self.channel.request("startDebugging", { | |
| "request": "attach", | |
| "configuration": body, | |
| }) | |
| else: | |
| body["request"] = "attach" | |
| self.channel.send_event("debugpyAttach", body) | |
| def serve(host, port): | |
| global listener | |
| listener = sockets.serve("Client", Client, host, port) | |
| sessions.report_sockets() | |
| return listener.getsockname() | |
| def stop_serving(): | |
| global listener | |
| if listener is not None: | |
| try: | |
| listener.close() | |
| except Exception: | |
| log.swallow_exception(level="warning") | |
| listener = None | |
| sessions.report_sockets() | |