def test_unicode_dir(tmpdir, run, target): from debugpy.common import compat unicode_chars = "á" directory = os.path.join(compat.force_unicode(str(tmpdir), "ascii"), unicode_chars) directory = compat.filename_str(directory) os.makedirs(directory) code_to_debug = os.path.join(directory, compat.filename_str("experiment.py")) with open(code_to_debug, "wb") as stream: stream.write(b""" import debuggee from debuggee import backchannel debuggee.setup() backchannel.send('ok') """) with debug.Session() as session: backchannel = session.open_backchannel() with run(session, target(compat.filename_str(code_to_debug))): pass received = backchannel.receive() assert received == "ok"
def for_popen(self): """Returns a copy of this dict, with all strings converted to the type suitable for subprocess.Popen() and other similar APIs. """ return { compat.filename_str(k): compat.filename_str(v) for k, v in self.items() }
def spawn_debuggee(self, args, cwd=None, exe=sys.executable, setup=None): assert self.debuggee is None assert not len(self.captured_output - {"stdout", "stderr"}) args = [exe] + [ compat.filename_str(s.strpath if isinstance(s, py.path.local) else s) for s in args ] cwd = compat.filename_str(cwd) if isinstance(cwd, py.path.local) else cwd env = self._make_env(self.spawn_debuggee.env, codecov=False) env["DEBUGPY_ADAPTER_ENDPOINTS"] = self.adapter_endpoints = ( self.tmpdir / "adapter_endpoints" ) if setup is not None: env["DEBUGPY_TEST_DEBUGGEE_SETUP"] = setup log.info( "Spawning {0}:\n\n" "Current directory: {1!j}\n\n" "Command line: {2!j}\n\n" "Environment variables: {3!j}\n\n", self.debuggee_id, cwd, args, env, ) popen_fds = {} capture_fds = {} for stream_name in self.captured_output: rfd, wfd = os.pipe() popen_fds[stream_name] = wfd capture_fds[stream_name] = rfd self.debuggee = psutil.Popen( args, cwd=cwd, env=env.for_popen(), bufsize=0, stdin=subprocess.PIPE, **popen_fds ) log.info("Spawned {0} with PID={1}", self.debuggee_id, self.debuggee.pid) watchdog.register_spawn(self.debuggee.pid, self.debuggee_id) if len(capture_fds): self.captured_output = output.CapturedOutput(self, **capture_fds) for fd in popen_fds.values(): os.close(fd)
def attach_connect(session, target, method, cwd=None, wait=True, log_dir=None): log.info("Attaching {0} to {1} by socket using {2}.", session, target, method.upper()) assert method in ("api", "cli") config = _attach_common_config(session, target, cwd) config["connect"] = {} config["connect"]["host"] = host = attach_connect.host config["connect"]["port"] = port = attach_connect.port if method == "cli": args = [ os.path.dirname(debugpy.__file__), "--listen", compat.filename_str(host) + ":" + str(port), ] if wait: args += ["--wait-for-client"] if log_dir is not None: args += ["--log-to", log_dir] if "subProcess" in config: args += ["--configure-subProcess", str(config["subProcess"])] debuggee_setup = None elif method == "api": args = [] api_config = {k: v for k, v in config.items() if k in {"subProcess"}} debuggee_setup = """ import debugpy if {log_dir!r}: debugpy.log_to({log_dir!r}) debugpy.configure({api_config!r}) debugpy.listen(({host!r}, {port!r})) if {wait!r}: debugpy.wait_for_client() """ debuggee_setup = fmt( debuggee_setup, host=host, port=port, wait=wait, log_dir=log_dir, api_config=api_config, ) else: raise ValueError args += target.cli(session.spawn_debuggee.env) try: del config["subProcess"] except KeyError: pass session.spawn_debuggee(args, cwd=cwd, setup=debuggee_setup) if wait: session.wait_for_adapter_socket() session.connect_to_adapter((host, port)) return session.request_attach()
def attach_listen(session, target, method, cwd=None, log_dir=None): log.info("Attaching {0} to {1} by socket using {2}.", session, target, method.upper()) assert method in ("api", "cli") config = _attach_common_config(session, target, cwd) config["listen"] = {} config["listen"]["host"] = host = attach_listen.host config["listen"]["port"] = port = attach_listen.port if method == "cli": args = [ os.path.dirname(debugpy.__file__), "--connect", compat.filename_str(host) + ":" + str(port), ] if log_dir is not None: args += ["--log-to", log_dir] if "subProcess" in config: args += ["--configure-subProcess", str(config["subProcess"])] debuggee_setup = None elif method == "api": args = [] api_config = {k: v for k, v in config.items() if k in {"subProcess"}} debuggee_setup = """ import debugpy if {log_dir!r}: debugpy.log_to({log_dir!r}) debugpy.configure({api_config!r}) debugpy.connect({address!r}) """ debuggee_setup = fmt(debuggee_setup, address=(host, port), log_dir=log_dir, api_config=api_config) else: raise ValueError args += target.cli(session.spawn_debuggee.env) try: del config["subProcess"] except KeyError: pass def spawn_debuggee(occ): assert occ.body == some.dict.containing({"host": host, "port": port}) session.spawn_debuggee(args, cwd=cwd, setup=debuggee_setup) session.timeline.when(timeline.Event("debugpyWaitingForServer"), spawn_debuggee) session.spawn_adapter( args=[] if log_dir is None else ["--log-dir", log_dir]) return session.request_attach()
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, str("")) # 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] target_as_str = compat.filename_str(options.target) try: if sys.version_info >= (3, ): from importlib.util import find_spec spec = find_spec(target_as_str) if spec is not None: argv_0 = spec.origin else: _, _, _, argv_0 = runpy._get_module_details(target_as_str) 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. log.describe_environment("Pre-launch environment:") log.info("Running module {0!r}", options.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_as_str, alter_sys=True) else: run_module_as_main(target_as_str, alter_argv=True)
def start_debugging(argv_0): # We need to set up sys.argv[0] before invoking either listen() or connect(), # because they use it to report the "process" event. Thus, we can't rely on # run_path() and run_module() doing that, even though they will eventually. sys.argv[0] = compat.filename_str(argv_0) log.debug("sys.argv after patching: {0!r}", sys.argv) debugpy.configure(options.config) if options.mode == "listen": debugpy.listen(options.address) elif options.mode == "connect": debugpy.connect(options.address, access_token=options.adapter_access_token) else: raise AssertionError(repr(options.mode)) if options.wait_for_client: debugpy.wait_for_client()
def run_file(): target = options.target start_debugging(target) target_as_str = compat.filename_str(target) # run_path has one difference with invoking Python from command-line: # if the target is a file (rather than a directory), it does not add its # parent directory to sys.path. Thus, importing other modules from the # same directory is broken unless sys.path is patched here. if os.path.isfile(target_as_str): dir = os.path.dirname(target_as_str) sys.path.insert(0, dir) else: log.debug("Not a file: {0!r}", target) log.describe_environment("Pre-launch environment:") log.info("Running file {0!r}", target) runpy.run_path(target_as_str, run_name=compat.force_str("__main__"))
def describe_environment(header): import sysconfig import site # noqa result = [header, "\n\n"] def report(*args, **kwargs): result.append(fmt(*args, **kwargs)) def report_paths(get_paths, label=None): prefix = fmt(" {0}: ", label or get_paths) expr = None if not callable(get_paths): expr = get_paths get_paths = lambda: util.evaluate(expr) try: paths = get_paths() except AttributeError: report("{0}<missing>\n", prefix) return except Exception: swallow_exception( "Error evaluating {0}", repr(expr) if expr else compat.srcnameof(get_paths), ) return if not isinstance(paths, (list, tuple)): paths = [paths] for p in sorted(paths): report("{0}{1}", prefix, p) rp = os.path.realpath(p) if p != rp: report("({0})", rp) report("\n") prefix = " " * len(prefix) report("System paths:\n") report_paths("sys.prefix") report_paths("sys.base_prefix") report_paths("sys.real_prefix") report_paths("site.getsitepackages()") report_paths("site.getusersitepackages()") site_packages = [ p for p in sys.path if os.path.exists(p) and os.path.basename(p) == compat.filename_str("site-packages") ] report_paths(lambda: site_packages, "sys.path (site-packages)") for name in sysconfig.get_path_names(): expr = fmt("sysconfig.get_path({0!r})", name) report_paths(expr) report_paths("os.__file__") report_paths("threading.__file__") result = "".join(result).rstrip("\n") info("{0}", result)
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 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()
def __str__(self): return compat.filename_str(self.path)