def spawn_debuggee( session, start_request, python, launcher_path, adapter_host, args, cwd, console, console_title, sudo, ): # -E tells sudo to propagate environment variables to the target process - this # is necessary for launcher to get DEBUGPY_LAUNCHER_PORT and DEBUGPY_LOG_DIR. cmdline = ["sudo", "-E"] if sudo else [] cmdline += python cmdline += [launcher_path] env = {} arguments = dict(start_request.arguments) if not session.no_debug: _, arguments["port"] = servers.listener.getsockname() arguments["adapterAccessToken"] = adapter.access_token def on_launcher_connected(sock): listener.close() stream = messaging.JsonIOStream.from_socket(sock) Launcher(session, stream) try: listener = sockets.serve("Launcher", on_launcher_connected, adapter_host, backlog=1) except Exception as exc: raise start_request.cant_handle( "{0} couldn't create listener socket for launcher: {1}", session, exc) try: launcher_host, launcher_port = listener.getsockname() launcher_addr = (launcher_port if launcher_host == "127.0.0.1" else fmt("{0}:{1}", launcher_host, launcher_port)) cmdline += [str(launcher_addr), "--"] cmdline += args if log.log_dir is not None: env[str("DEBUGPY_LOG_DIR")] = compat.filename_str(log.log_dir) if log.stderr.levels != {"warning", "error"}: env[str("DEBUGPY_LOG_STDERR")] = str(" ".join(log.stderr.levels)) if console == "internalConsole": log.info("{0} spawning launcher: {1!r}", session, cmdline) try: for i, arg in enumerate(cmdline): try: cmdline[i] = compat.filename_str(arg) except UnicodeEncodeError as exc: raise start_request.cant_handle( "Invalid command line argument {0!j}: {1}", arg, exc) # If we are talking to the client over stdio, sys.stdin and sys.stdout # are redirected to avoid mangling the DAP message stream. Make sure # the launcher also respects that. subprocess.Popen( cmdline, cwd=cwd, env=dict(list(os.environ.items()) + list(env.items())), stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr, ) except Exception as exc: raise start_request.cant_handle( "Failed to spawn launcher: {0}", exc) else: log.info('{0} spawning launcher via "runInTerminal" request.', session) session.client.capabilities.require("supportsRunInTerminalRequest") kinds = { "integratedTerminal": "integrated", "externalTerminal": "external" } request_args = { "kind": kinds[console], "title": console_title, "args": cmdline, "env": env, } if cwd is not None: request_args["cwd"] = cwd try: session.client.channel.request("runInTerminal", request_args) except messaging.MessageHandlingError as exc: exc.propagate(start_request) # If using sudo, it might prompt for password, and launcher won't start running # until the user enters it, so don't apply timeout in that case. if not session.wait_for( lambda: session.launcher, timeout=(None if sudo else common.PROCESS_SPAWN_TIMEOUT), ): raise start_request.cant_handle( "Timed out waiting for launcher to connect") try: session.launcher.channel.request(start_request.command, arguments) except messaging.MessageHandlingError as exc: exc.propagate(start_request) if session.no_debug: return if not session.wait_for( lambda: session.launcher.pid is not None, timeout=common.PROCESS_SPAWN_TIMEOUT, ): raise start_request.cant_handle( 'Timed out waiting for "process" event from launcher') # Wait for the first incoming connection regardless of the PID - it won't # necessarily match due to the use of stubs like py.exe or "conda run". conn = servers.wait_for_connection( session, lambda conn: True, timeout=common.PROCESS_SPAWN_TIMEOUT) if conn is None: raise start_request.cant_handle( "Timed out waiting for debuggee to spawn") conn.attach_to_session(session) finally: listener.close()
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 spawn_debuggee(session, start_request, args, console, console_title): cmdline = [sys.executable, os.path.dirname(launcher.__file__)] + args env = {} arguments = dict(start_request.arguments) if not session.no_debug: _, arguments["port"] = servers.listener.getsockname() arguments["adapterAccessToken"] = adapter.access_token def on_launcher_connected(sock): listener.close() stream = messaging.JsonIOStream.from_socket(sock) Launcher(session, stream) try: listener = sockets.serve( "Launcher", on_launcher_connected, "127.0.0.1", backlog=0 ) except Exception as exc: raise start_request.cant_handle( "{0} couldn't create listener socket for {1}: {2}", session, session.launcher, exc, ) try: _, launcher_port = listener.getsockname() env[str("DEBUGPY_LAUNCHER_PORT")] = str(launcher_port) if log.log_dir is not None: env[str("DEBUGPY_LOG_DIR")] = compat.filename_str(log.log_dir) if log.stderr.levels != {"warning", "error"}: env[str("DEBUGPY_LOG_STDERR")] = str(" ".join(log.stderr.levels)) if console == "internalConsole": log.info("{0} spawning launcher: {1!r}", session, cmdline) try: # If we are talking to the client over stdio, sys.stdin and sys.stdout # are redirected to avoid mangling the DAP message stream. Make sure # the launcher also respects that. subprocess.Popen( cmdline, env=dict(list(os.environ.items()) + list(env.items())), stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr, ) except Exception as exc: raise start_request.cant_handle( "{0} failed to spawn {1}: {2}", session, session.launcher, exc ) else: log.info('{0} spawning launcher via "runInTerminal" request.', session) session.client.capabilities.require("supportsRunInTerminalRequest") kinds = {"integratedTerminal": "integrated", "externalTerminal": "external"} try: session.client.channel.request( "runInTerminal", { "kind": kinds[console], "title": console_title, "args": cmdline, "env": env, }, ) except messaging.MessageHandlingError as exc: exc.propagate(start_request) if not session.wait_for(lambda: session.launcher, timeout=10): raise start_request.cant_handle( "{0} timed out waiting for {1} to connect", session, session.launcher ) try: session.launcher.channel.request(start_request.command, arguments) except messaging.MessageHandlingError as exc: exc.propagate(start_request) if session.no_debug: return if not session.wait_for(lambda: session.launcher.pid is not None, timeout=10): raise start_request.cant_handle( '{0} timed out waiting for "process" event from {1}', session, session.launcher, ) # Wait for the first incoming connection regardless of the PID - it won't # necessarily match due to the use of stubs like py.exe or "conda run". conn = servers.wait_for_connection(session, lambda conn: True, timeout=10) if conn is None: raise start_request.cant_handle( "{0} timed out waiting for debuggee to spawn", session ) conn.attach_to_session(session) finally: listener.close()