From 32c00bc85c284791e9e55b74885b6825b9ef111e Mon Sep 17 00:00:00 2001 From: Pavel Minaev Date: Wed, 1 Apr 2020 19:17:13 -0700 Subject: [PATCH] Fix #92: Debugging as sudo fails to terminate the debugger when you stop Apply sudo to debugpy.launcher, rather than the debuggee itself. --- src/debugpy/adapter/clients.py | 29 ++++++++++++++++++++++++++++- src/debugpy/adapter/launchers.py | 13 ++++++++++--- src/debugpy/launcher/handlers.py | 16 ++++------------ tests/debug/session.py | 3 +-- tests/debugpy/test_run.py | 5 ++++- 5 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/debugpy/adapter/clients.py b/src/debugpy/adapter/clients.py index 8b0eb5ce..9ece52da 100644 --- a/src/debugpy/adapter/clients.py +++ b/src/debugpy/adapter/clients.py @@ -250,6 +250,29 @@ class Client(components.Component): 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: @@ -277,8 +300,12 @@ class Client(components.Component): ) 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) + launchers.spawn_debuggee(self.session, request, args, console, console_title, sudo) @_start_message_handler def attach_request(self, request): diff --git a/src/debugpy/adapter/launchers.py b/src/debugpy/adapter/launchers.py index c4829a2a..1a91f4b3 100644 --- a/src/debugpy/adapter/launchers.py +++ b/src/debugpy/adapter/launchers.py @@ -65,8 +65,13 @@ class Launcher(components.Component): pass -def spawn_debuggee(session, start_request, args, console, console_title): - cmdline = [sys.executable, os.path.dirname(launcher.__file__)] + args +def spawn_debuggee(session, start_request, args, 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 += [sys.executable, os.path.dirname(launcher.__file__)] + cmdline += args env = {} arguments = dict(start_request.arguments) @@ -131,7 +136,9 @@ def spawn_debuggee(session, start_request, args, console, console_title): except messaging.MessageHandlingError as exc: exc.propagate(start_request) - if not session.wait_for(lambda: session.launcher, timeout=10): + # 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 10)): raise start_request.cant_handle("Timed out waiting for launcher to connect") try: diff --git a/src/debugpy/launcher/handlers.py b/src/debugpy/launcher/handlers.py index 93b6c4b3..b353a24c 100644 --- a/src/debugpy/launcher/handlers.py +++ b/src/debugpy/launcher/handlers.py @@ -38,13 +38,6 @@ def launch_request(request): return value - cmdline = [] - if property_or_debug_option("sudo", "Sudo"): - if sys.platform == "win32": - raise request.cant_handle('"sudo":true is not supported on Windows.') - else: - cmdline += ["sudo"] - # "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" @@ -55,10 +48,9 @@ def launch_request(request): ) 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)] - cmdline += python + cmdline = request(python_key, json.array(unicode, vectorize=True, size=(0,))) + if not len(cmdline): + cmdline = [compat.filename(sys.executable)] if not request("noDebug", json.default(False)): port = request("port", int) @@ -87,7 +79,7 @@ def launch_request(request): if "code" in request: code = request("code", json.array(unicode, vectorize=True, size=(1,))) cmdline += ["-c", "\n".join(code)] - process_name = python[0] + process_name = cmdline[0] num_targets = len([x for x in (program, module, code) if x != ()]) if num_targets == 0: diff --git a/tests/debug/session.py b/tests/debug/session.py index 441eb164..2081ee6e 100644 --- a/tests/debug/session.py +++ b/tests/debug/session.py @@ -492,8 +492,7 @@ class Session(object): 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.env.update(env) self.spawn_debuggee(args, cwd, exe=exe) return {} except OSError as exc: diff --git a/tests/debugpy/test_run.py b/tests/debugpy/test_run.py index 79efdc3b..1a96098f 100644 --- a/tests/debugpy/test_run.py +++ b/tests/debugpy/test_run.py @@ -99,6 +99,7 @@ def test_sudo(pyfile, tmpdir, run, target): sudo = tmpdir / "sudo" sudo.write( """#!/bin/sh + if [ "$1" = "-E" ]; then shift; fi exec env DEBUGPY_SUDO=1 "$@" """ ) @@ -116,7 +117,9 @@ def test_sudo(pyfile, tmpdir, run, target): with debug.Session() as session: session.config["sudo"] = True - session.config.env["PATH"] = tmpdir.strpath + ":" + os.environ["PATH"] + session.spawn_adapter.env["PATH"] = session.spawn_debuggee.env["PATH"] = ( + tmpdir.strpath + ":" + os.environ["PATH"] + ) backchannel = session.open_backchannel() with run(session, target(code_to_debug)):