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 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 main(args): # If we're talking DAP over stdio, stderr is not guaranteed to be read from, # so disable it to avoid the pipe filling and locking up. This must be done # as early as possible, before the logging module starts writing to it. if args.port is None: sys.stderr = open(os.devnull, "w") from debugpy import adapter from debugpy.common import compat, log, sockets from debugpy.adapter import clients, servers, sessions if args.for_server is not None: if os.name == "posix": # On POSIX, we need to leave the process group and its session, and then # daemonize properly by double-forking (first fork already happened when # this process was spawned). os.setsid() if os.fork() != 0: sys.exit(0) for stdio in sys.stdin, sys.stdout, sys.stderr: if stdio is not None: stdio.close() if args.log_stderr: log.stderr.levels |= set(log.LEVELS) if args.log_dir is not None: log.log_dir = args.log_dir log.to_file(prefix="debugpy.adapter") log.describe_environment("debugpy.adapter startup environment:") servers.access_token = args.server_access_token if args.for_server is None: adapter.access_token = compat.force_str( codecs.encode(os.urandom(32), "hex")) endpoints = {} try: client_host, client_port = clients.serve(args.host, args.port) except Exception as exc: if args.for_server is None: raise endpoints = { "error": "Can't listen for client connections: " + str(exc) } else: endpoints["client"] = {"host": client_host, "port": client_port} if args.for_server is not None: try: server_host, server_port = servers.serve() except Exception as exc: endpoints = { "error": "Can't listen for server connections: " + str(exc) } else: endpoints["server"] = {"host": server_host, "port": server_port} log.info( "Sending endpoints info to debug server at localhost:{0}:\n{1!j}", args.for_server, endpoints, ) try: sock = sockets.create_client() try: sock.settimeout(None) sock.connect(("127.0.0.1", args.for_server)) sock_io = sock.makefile("wb", 0) try: sock_io.write(json.dumps(endpoints).encode("utf-8")) finally: sock_io.close() finally: sockets.close_socket(sock) except Exception: log.reraise_exception( "Error sending endpoints info to debug server:") if "error" in endpoints: log.error("Couldn't set up endpoints; exiting.") sys.exit(1) listener_file = os.getenv("DEBUGPY_ADAPTER_ENDPOINTS") if listener_file is not None: log.info("Writing endpoints info to {0!r}:\n{1!j}", listener_file, endpoints) def delete_listener_file(): log.info("Listener ports closed; deleting {0!r}", listener_file) try: os.remove(listener_file) except Exception: log.swallow_exception("Failed to delete {0!r}", listener_file, level="warning") try: with open(listener_file, "w") as f: atexit.register(delete_listener_file) print(json.dumps(endpoints), file=f) except Exception: log.reraise_exception("Error writing endpoints info to file:") if args.port is None: clients.Client("stdio") # These must be registered after the one above, to ensure that the listener sockets # are closed before the endpoint info file is deleted - this way, another process # can wait for the file to go away as a signal that the ports are no longer in use. atexit.register(servers.stop_serving) atexit.register(clients.stop_serving) servers.wait_until_disconnected() log.info( "All debug servers disconnected; waiting for remaining sessions...") sessions.wait_until_ended() log.info("All debug sessions have ended; exiting.")
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)