def __init__(self, whose, category, fd, stream): assert category not in self.instances self.instances[category] = self log.info("Capturing {0} of {1}.", category, whose) self.category = category self._whose = whose self._fd = fd self._decoder = codecs.getincrementaldecoder("utf-8")( errors="surrogateescape") if stream is None: # Can happen if running under pythonw.exe. self._stream = None else: self._stream = stream if sys.version_info < ( 3, ) else stream.buffer encoding = stream.encoding if encoding is None or encoding == "cp65001": encoding = "utf-8" try: self._encode = codecs.getencoder(encoding) except Exception: log.swallow_exception( "Unsupported {0} encoding {1!r}; falling back to UTF-8.", category, encoding, level="warning", ) self._encode = codecs.getencoder("utf-8") self._worker_thread = threading.Thread(target=self._worker, name=category) self._worker_thread.start()
def wait_for_exit(): try: code = process.wait() if sys.platform != "win32" and code < 0: # On POSIX, if the process was terminated by a signal, Popen will use # a negative returncode to indicate that - but the actual exit code of # the process is always an unsigned number, and can be determined by # taking the lowest 8 bits of that negative returncode. code &= 0xFF except Exception: log.swallow_exception("Couldn't determine process exit code") code = -1 log.info("{0} exited with code {1}", describe(), code) output.wait_for_remaining_output() # Determine whether we should wait or not before sending "exited", so that any # follow-up "terminate" requests don't affect the predicates. should_wait = any(pred(code) for pred in wait_on_exit_predicates) try: launcher.channel.send_event("exited", {"exitCode": code}) except Exception: pass if should_wait: _wait_for_user_input() try: launcher.channel.send_event("terminated") except Exception: pass
def spawn(process_name, cmdline, env, redirect_output): log.info( "Spawning debuggee process:\n\n" "Command line: {0!r}\n\n" "Environment variables: {1!r}\n\n", cmdline, env, ) close_fds = set() try: if redirect_output: # subprocess.PIPE behavior can vary substantially depending on Python version # and platform; using our own pipes keeps it simple, predictable, and fast. stdout_r, stdout_w = os.pipe() stderr_r, stderr_w = os.pipe() close_fds |= {stdout_r, stdout_w, stderr_r, stderr_w} kwargs = dict(stdout=stdout_w, stderr=stderr_w) else: kwargs = {} try: global process process = subprocess.Popen(cmdline, env=env, bufsize=0, **kwargs) except Exception as exc: raise messaging.MessageHandlingError( fmt("Couldn't spawn debuggee: {0}\n\nCommand line:{1!r}", exc, cmdline) ) log.info("Spawned {0}.", describe()) atexit.register(kill) launcher.channel.send_event( "process", { "startMethod": "launch", "isLocalProcess": True, "systemProcessId": process.pid, "name": process_name, "pointerSize": struct.calcsize(compat.force_str("P")) * 8, }, ) if redirect_output: for category, fd, tee in [ ("stdout", stdout_r, sys.stdout), ("stderr", stderr_r, sys.stderr), ]: output.CaptureOutput(describe(), category, fd, tee) close_fds.remove(fd) wait_thread = threading.Thread(target=wait_for_exit, name="wait_for_exit()") wait_thread.daemon = True wait_thread.start() finally: for fd in close_fds: try: os.close(fd) except Exception: log.swallow_exception()
def finalize(self, why, terminate_debuggee=None): """Finalizes the debug session. If the server is present, sends "disconnect" request with "terminateDebuggee" set as specified request to it; waits for it to disconnect, allowing any remaining messages from it to be handled; and closes the server channel. If the launcher is present, sends "terminate" request to it, regardless of the value of terminate; waits for it to disconnect, allowing any remaining messages from it to be handled; and closes the launcher channel. If the client is present, sends "terminated" event to it. If terminate_debuggee=None, it is treated as True if the session has a Launcher component, and False otherwise. """ if self.is_finalizing: return self.is_finalizing = True log.info("{0}; finalizing {1}.", why, self) if terminate_debuggee is None: terminate_debuggee = bool(self.launcher) try: self._finalize(why, terminate_debuggee) except Exception: # Finalization should never fail, and if it does, the session is in an # indeterminate and likely unrecoverable state, so just fail fast. log.swallow_exception("Fatal error while finalizing {0}", self) os._exit(1) log.info("{0} finalized.", self)
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")
def stop(): if _stream is None: return try: _invoke("stop") _stream.close() except Exception: log.swallow_exception()
def kill(): if process is None: return try: if process.poll() is None: log.info("Killing {0}", describe()) process.kill() except Exception: log.swallow_exception("Failed to kill {0}", describe())
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 inject(pid, debugpy_args): host, port = listener.getsockname() cmdline = [ sys.executable, compat.filename(os.path.dirname(debugpy.__file__)), "--connect", host + ":" + str(port), ] if adapter.access_token is not None: cmdline += ["--adapter-access-token", adapter.access_token] cmdline += debugpy_args cmdline += ["--pid", str(pid)] log.info("Spawning attach-to-PID debugger injector: {0!r}", cmdline) try: injector = subprocess.Popen( cmdline, bufsize=0, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) except Exception as exc: log.swallow_exception( "Failed to inject debug server into process with PID={0}", pid ) raise messaging.MessageHandlingError( fmt( "Failed to inject debug server into process with PID={0}: {1}", pid, exc ) ) # We need to capture the output of the injector - otherwise it can get blocked # on a write() syscall when it tries to print something. def capture_output(): while True: line = injector.stdout.readline() if not line: break log.info("Injector[PID={0}] output:\n{1}", pid, line.rstrip()) log.info("Injector[PID={0}] exited.", pid) thread = threading.Thread( target=capture_output, name=fmt("Injector[PID={0}] output", pid) ) thread.daemon = True thread.start()
def run_module(): # Add current directory to path, like Python itself does for -m. This must # be in place before trying to use find_spec below to resolve submodules. sys.path.insert(0, "") # We want to do the same thing that run_module() would do here, without # actually invoking it. On Python 3, it's exposed as a public API, but # on Python 2, we have to invoke a private function in runpy for this. # Either way, if it fails to resolve for any reason, just leave argv as is. argv_0 = sys.argv[0] try: if sys.version_info >= (3,): from importlib.util import find_spec spec = find_spec(options.target) if spec is not None: argv_0 = spec.origin else: _, _, _, argv_0 = runpy._get_module_details(options.target) except Exception: log.swallow_exception("Error determining module path for sys.argv") start_debugging(argv_0) # On Python 2, module name must be a non-Unicode string, because it ends up # a part of module's __package__, and Python will refuse to run the module # if __package__ is Unicode. target = ( compat.filename_bytes(options.target) if sys.version_info < (3,) else options.target ) log.describe_environment("Pre-launch environment:") log.info("Running module {0!r}", target) # Docs say that runpy.run_module is equivalent to -m, but it's not actually # the case for packages - -m sets __name__ to "__main__", but run_module sets # it to "pkg.__main__". This breaks everything that uses the standard pattern # __name__ == "__main__" to detect being run as a CLI app. On the other hand, # runpy._run_module_as_main is a private function that actually implements -m. try: run_module_as_main = runpy._run_module_as_main except AttributeError: log.warning("runpy._run_module_as_main is missing, falling back to run_module.") runpy.run_module(target, alter_sys=True) else: run_module_as_main(target, alter_argv=True)
def kill(): if process is None: return try: if process.poll() is None: log.info("Killing {0}", describe()) # Clean up the process tree if sys.platform == "win32": # On Windows, kill the job object. winapi.kernel32.TerminateJobObject(job_handle, 0) else: # On POSIX, kill the debuggee's process group. os.killpg(process.pid, signal.SIGKILL) except Exception: log.swallow_exception("Failed to kill {0}", describe())
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 _dump_worker_log(command, problem, exc_info=None): reason = fmt("{0}.{1}() {2}", _name, command, problem) if _worker_log_filename is None: reason += ", but there is no log." else: try: with open(_worker_log_filename) as f: worker_log = f.read() except Exception: reason += fmt(", but log {0} could not be retrieved.", _worker_log_filename) else: reason += fmt("; watchdog worker process log:\n\n{0}", worker_log) if exc_info is None: log.error("{0}", reason) else: log.swallow_exception("{0}", reason, exc_info=exc_info) return reason
def preexec_fn(): try: # Start the debuggee in a new process group, so that the launcher can # kill the entire process tree later. os.setpgrp() # Make the new process group the foreground group in its session, so # that it can interact with the terminal. The debuggee will receive # SIGTTOU when tcsetpgrp() is called, and must ignore it. old_handler = signal.signal(signal.SIGTTOU, signal.SIG_IGN) try: tty = os.open("/dev/tty", os.O_RDWR) try: os.tcsetpgrp(tty, os.getpgrp()) finally: os.close(tty) finally: signal.signal(signal.SIGTTOU, old_handler) except Exception: # Not an error - /dev/tty doesn't work when there's no terminal. log.swallow_exception("Failed to set up process group", level="info")
def stop_serving(): try: listener.close() except Exception: log.swallow_exception(level="warning")
def __init__(self, sock): from debugpy.adapter import sessions self.disconnected = False self.server = None """The Server component, if this debug server belongs to Session. """ self.pid = None stream = messaging.JsonIOStream.from_socket(sock, str(self)) self.channel = messaging.JsonMessageChannel(stream, self) self.channel.start() try: self.authenticate() info = self.channel.request("pydevdSystemInfo") process_info = info("process", json.object()) self.pid = process_info("pid", int) self.ppid = process_info("ppid", int, optional=True) if self.ppid == (): self.ppid = None self.channel.name = stream.name = str(self) debugpy_dir = os.path.dirname(os.path.dirname(debugpy.__file__)) # Note: we must check if 'debugpy' is not already in sys.modules because the # evaluation of an import at the wrong time could deadlock Python due to # its import lock. # # So, in general this evaluation shouldn't do anything. It's only # important when pydevd attaches automatically to a subprocess. In this # case, we have to make sure that debugpy is properly put back in the game # for users to be able to use it.v # # In this case (when the import is needed), this evaluation *must* be done # before the configurationDone request is sent -- if this is not respected # it's possible that pydevd already started secondary threads to handle # commands, in which case it's very likely that this command would be # evaluated at the wrong thread and the import could potentially deadlock # the program. # # Note 2: the sys module is guaranteed to be in the frame globals and # doesn't need to be imported. inject_debugpy = """ if 'debugpy' not in sys.modules: sys.path.insert(0, {debugpy_dir!r}) try: import debugpy finally: del sys.path[0] """ inject_debugpy = fmt(inject_debugpy, debugpy_dir=debugpy_dir) try: self.channel.request("evaluate", {"expression": inject_debugpy}) except messaging.MessageHandlingError: # Failure to inject is not a fatal error - such a subprocess can # still be debugged, it just won't support "import debugpy" in user # code - so don't terminate the session. log.swallow_exception( "Failed to inject debugpy into {0}:", self, level="warning" ) with _lock: # The server can disconnect concurrently before we get here, e.g. if # it was force-killed. If the disconnect() handler has already run, # don't register this server or report it, since there's nothing to # deregister it. if self.disconnected: return if any(conn.pid == self.pid for conn in _connections): raise KeyError( fmt("{0} is already connected to this adapter", self) ) _connections.append(self) _connections_changed.set() except Exception: log.swallow_exception("Failed to accept incoming server connection:") self.channel.close() # If this was the first server to connect, and the main thread is inside # wait_until_disconnected(), we want to unblock it and allow it to exit. dont_wait_for_first_connection() # If we couldn't retrieve all the necessary info from the debug server, # or there's a PID clash, we don't want to track this debuggee anymore, # but we want to continue accepting connections. return parent_session = sessions.get(self.ppid) if parent_session is None: log.info("No active debug session for parent process of {0}.", self) else: try: parent_session.client.notify_of_subprocess(self) except Exception: # This might fail if the client concurrently disconnects from the parent # session. We still want to keep the connection around, in case the # client reconnects later. If the parent session was "launch", it'll take # care of closing the remaining server connections. log.swallow_exception("Failed to notify parent session about {0}:", self)
def dumper(): time.sleep(secs) try: dump() except: log.swallow_exception()
def main(tests_pid): # To import debugpy, the "" entry in sys.path - which is added automatically on # Python 2 - must be removed first; otherwise, we end up importing tests/debugpy. if "" in sys.path: sys.path.remove("") from debugpy.common import fmt, log, messaging # log.stderr_levels |= {"info"} log.timestamp_format = "06.3f" log_file = log.to_file(prefix="tests.watchdog") stream = messaging.JsonIOStream.from_stdio(fmt("tests-{0}", tests_pid)) log.info("Spawned WatchDog-{0} for tests-{0}", tests_pid) tests_process = psutil.Process(tests_pid) stream.write_json(["watchdog", log_file.filename]) spawned_processes = {} # pid -> ProcessInfo try: stop = False while not stop: try: message = stream.read_json() except Exception: break command = message[0] args = message[1:] if command == "stop": assert not args stop = True elif command == "register_spawn": pid, name = args pid = int(pid) log.info( "WatchDog-{0} registering spawned process {1} (pid={2})", tests_pid, name, pid, ) try: _, old_name = spawned_processes[pid] except KeyError: pass else: log.warning( "WatchDog-{0} already tracks a process with pid={1}: {2}", tests_pid, pid, old_name, ) spawned_processes[pid] = ProcessInfo(psutil.Process(pid), name) elif command == "unregister_spawn": pid, name = args pid = int(pid) log.info( "WatchDog-{0} unregistering spawned process {1} (pid={2})", tests_pid, name, pid, ) spawned_processes.pop(pid, None) else: raise AssertionError( fmt("Unknown watchdog command: {0!r}", command)) stream.write_json(["ok"]) except Exception as exc: stream.write_json(["error", str(exc)]) log.reraise_exception() finally: try: stream.close() except Exception: log.swallow_exception() # If the test runner becomes a zombie process, it is still considered alive, # and wait() will block indefinitely. Poll status instead. while True: try: status = tests_process.status() except Exception: # If we can't even get its status, assume that it's dead. break # If it's dead or a zombie, time to clean it up. if status in (psutil.STATUS_DEAD, psutil.STATUS_ZOMBIE): break # Otherwise, let's wait a bit to see if anything changes. try: tests_process.wait(0.1) except Exception: pass leftover_processes = {proc for proc, _ in spawned_processes.values()} for proc, _ in spawned_processes.values(): try: leftover_processes |= proc.children(recursive=True) except Exception: pass leftover_processes = { proc for proc in leftover_processes if proc.is_running() } if not leftover_processes: return # Wait a bit to allow the terminal to catch up on the test runner output. time.sleep(0.3) log.newline(level="warning") log.warning( "tests-{0} process terminated unexpectedly, and left some orphan child " "processes behind: {1!r}", tests_pid, sorted({proc.pid for proc in leftover_processes}), ) for proc in leftover_processes: log.warning( "WatchDog-{0} killing orphaned test child process (pid={1})", tests_pid, proc.pid, ) try: proc.kill() except psutil.NoSuchProcess: pass except Exception: log.swallow_exception() log.info("WatchDog-{0} exiting", tests_pid)
def __exit__(self, exc_type, exc_val, exc_tb): log.info("Ending {0}.", self) if self.timeline.is_frozen: self.timeline.unfreeze() # Only wait for exit if there was no exception in the test - if there was one, # the debuggee might still be waiting for further requests. if exc_type is None: # If expected_exit_code is set to None, the debuggee is not expected to # exit after this Session is closed (e.g. because another Session will # attach to it later on). if self.expected_exit_code is not None: self.wait_for_exit() else: # Log the error, in case another one happens during shutdown. log.swallow_exception(exc_info=(exc_type, exc_val, exc_tb)) if exc_type is None: self.disconnect() self.timeline.close() else: # If there was an exception, don't try to send any more messages to avoid # spamming log with irrelevant entries - just close the channel and kill # all the processes immediately. Don't close or finalize the timeline, # either, since it'll likely have unobserved events in it. if self.adapter is not None: log.info("Killing {0}.", self.adapter_id) try: self.adapter.kill() except Exception: pass if self.debuggee is not None: log.info("Killing {0}.", self.debuggee_id) try: self.debuggee.kill() except Exception: pass self.disconnect(force=True) if self.adapter_endpoints is not None and self.expected_exit_code is not None: log.info("Waiting for {0} to close listener ports ...", self.adapter_id) while self.adapter_endpoints.check(): time.sleep(0.1) if self.adapter is not None: log.info( "Waiting for {0} with PID={1} to exit.", self.adapter_id, self.adapter.pid, ) self.adapter.wait() watchdog.unregister_spawn(self.adapter.pid, self.adapter_id) self.adapter = None if self.backchannel is not None: self.backchannel.close() self.backchannel = None # Work around https://bugs.python.org/issue37380 for popen in self.debuggee, self.adapter: if popen is not None and popen.returncode is None: popen.returncode = -1
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)
def spawn(process_name, cmdline, env, redirect_output): log.info( "Spawning debuggee process:\n\n" "Command line: {0!r}\n\n" "Environment variables: {1!r}\n\n", cmdline, env, ) close_fds = set() try: if redirect_output: # subprocess.PIPE behavior can vary substantially depending on Python version # and platform; using our own pipes keeps it simple, predictable, and fast. stdout_r, stdout_w = os.pipe() stderr_r, stderr_w = os.pipe() close_fds |= {stdout_r, stdout_w, stderr_r, stderr_w} kwargs = dict(stdout=stdout_w, stderr=stderr_w) else: kwargs = {} if sys.platform != "win32": def preexec_fn(): try: # Start the debuggee in a new process group, so that the launcher can # kill the entire process tree later. os.setpgrp() # Make the new process group the foreground group in its session, so # that it can interact with the terminal. The debuggee will receive # SIGTTOU when tcsetpgrp() is called, and must ignore it. old_handler = signal.signal(signal.SIGTTOU, signal.SIG_IGN) try: tty = os.open("/dev/tty", os.O_RDWR) try: os.tcsetpgrp(tty, os.getpgrp()) finally: os.close(tty) finally: signal.signal(signal.SIGTTOU, old_handler) except Exception: # Not an error - /dev/tty doesn't work when there's no terminal. log.swallow_exception( "Failed to set up process group", level="info" ) kwargs.update(preexec_fn=preexec_fn) try: global process process = subprocess.Popen(cmdline, env=env, bufsize=0, **kwargs) except Exception as exc: raise messaging.MessageHandlingError( fmt("Couldn't spawn debuggee: {0}\n\nCommand line:{1!r}", exc, cmdline) ) log.info("Spawned {0}.", describe()) if sys.platform == "win32": # Assign the debuggee to a new job object, so that the launcher can kill # the entire process tree later. try: global job_handle job_handle = winapi.kernel32.CreateJobObjectA(None, None) job_info = winapi.JOBOBJECT_EXTENDED_LIMIT_INFORMATION() job_info_size = winapi.DWORD(ctypes.sizeof(job_info)) winapi.kernel32.QueryInformationJobObject( job_handle, winapi.JobObjectExtendedLimitInformation, ctypes.pointer(job_info), job_info_size, ctypes.pointer(job_info_size), ) # Setting this flag ensures that the job will be terminated by the OS once the # launcher exits, even if it doesn't terminate the job explicitly. job_info.BasicLimitInformation.LimitFlags |= ( winapi.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ) winapi.kernel32.SetInformationJobObject( job_handle, winapi.JobObjectExtendedLimitInformation, ctypes.pointer(job_info), job_info_size, ) process_handle = winapi.kernel32.OpenProcess( winapi.PROCESS_TERMINATE | winapi.PROCESS_SET_QUOTA, False, process.pid, ) winapi.kernel32.AssignProcessToJobObject(job_handle, process_handle) except Exception: log.swallow_exception("Failed to set up job object", level="warning") atexit.register(kill) launcher.channel.send_event( "process", { "startMethod": "launch", "isLocalProcess": True, "systemProcessId": process.pid, "name": process_name, "pointerSize": struct.calcsize(compat.force_str("P")) * 8, }, ) if redirect_output: for category, fd, tee in [ ("stdout", stdout_r, sys.stdout), ("stderr", stderr_r, sys.stderr), ]: output.CaptureOutput(describe(), category, fd, tee) close_fds.remove(fd) wait_thread = threading.Thread(target=wait_for_exit, name="wait_for_exit()") wait_thread.daemon = True wait_thread.start() finally: for fd in close_fds: try: os.close(fd) except Exception: log.swallow_exception(level="warning")
def listen(address, settrace_kwargs): # Errors below are logged with level="info", because the caller might be catching # and handling exceptions, and we don't want to spam their stderr unnecessarily. import subprocess server_access_token = compat.force_str(codecs.encode(os.urandom(32), "hex")) try: endpoints_listener = sockets.create_server("127.0.0.1", 0, timeout=10) except Exception as exc: log.swallow_exception("Can't listen for adapter endpoints:") raise RuntimeError("can't listen for adapter endpoints: " + str(exc)) endpoints_host, endpoints_port = endpoints_listener.getsockname() log.info( "Waiting for adapter endpoints on {0}:{1}...", endpoints_host, endpoints_port ) host, port = address adapter_args = [ sys.executable, os.path.dirname(adapter.__file__), "--for-server", str(endpoints_port), "--host", host, "--port", str(port), "--server-access-token", server_access_token, ] if log.log_dir is not None: adapter_args += ["--log-dir", log.log_dir] log.info("debugpy.listen() spawning adapter: {0!j}", adapter_args) # On Windows, detach the adapter from our console, if any, so that it doesn't # receive Ctrl+C from it, and doesn't keep it open once we exit. creationflags = 0 if sys.platform == "win32": creationflags |= 0x08000000 # CREATE_NO_WINDOW creationflags |= 0x00000200 # CREATE_NEW_PROCESS_GROUP # Adapter will outlive this process, so we shouldn't wait for it. However, we # need to ensure that the Popen instance for it doesn't get garbage-collected # by holding a reference to it in a non-local variable, to avoid triggering # https://bugs.python.org/issue37380. try: global _adapter_process _adapter_process = subprocess.Popen( adapter_args, close_fds=True, creationflags=creationflags ) if os.name == "posix": # It's going to fork again to daemonize, so we need to wait on it to # clean it up properly. _adapter_process.wait() else: # Suppress misleading warning about child process still being alive when # this process exits (https://bugs.python.org/issue38890). _adapter_process.returncode = 0 pydevd.add_dont_terminate_child_pid(_adapter_process.pid) except Exception as exc: log.swallow_exception("Error spawning debug adapter:", level="info") raise RuntimeError("error spawning debug adapter: " + str(exc)) try: sock, _ = endpoints_listener.accept() try: sock.settimeout(None) sock_io = sock.makefile("rb", 0) try: endpoints = json.loads(sock_io.read().decode("utf-8")) finally: sock_io.close() finally: sockets.close_socket(sock) except socket.timeout: log.swallow_exception("Timed out waiting for adapter to connect:", level="info") raise RuntimeError("timed out waiting for adapter to connect") except Exception as exc: log.swallow_exception("Error retrieving adapter endpoints:", level="info") raise RuntimeError("error retrieving adapter endpoints: " + str(exc)) log.info("Endpoints received from adapter: {0!j}", endpoints) if "error" in endpoints: raise RuntimeError(str(endpoints["error"])) try: server_host = str(endpoints["server"]["host"]) server_port = int(endpoints["server"]["port"]) client_host = str(endpoints["client"]["host"]) client_port = int(endpoints["client"]["port"]) except Exception as exc: log.swallow_exception( "Error parsing adapter endpoints:\n{0!j}\n", endpoints, level="info" ) raise RuntimeError("error parsing adapter endpoints: " + str(exc)) log.info( "Adapter is accepting incoming client connections on {0}:{1}", client_host, client_port, ) _settrace( host=server_host, port=server_port, wait_for_ready_to_run=False, block_until_connected=True, access_token=server_access_token, **settrace_kwargs ) log.info("pydevd is connected to adapter at {0}:{1}", server_host, server_port) return client_host, client_port