Skip to content

Commit 32c00bc

Browse files
committed
Fix #92: Debugging as sudo fails to terminate the debugger when you stop
Apply sudo to debugpy.launcher, rather than the debuggee itself.
1 parent 2d35573 commit 32c00bc

File tree

5 files changed

+47
-19
lines changed

5 files changed

+47
-19
lines changed

src/debugpy/adapter/clients.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,29 @@ def launch_request(self, request):
250250
if self.session.id != 1 or len(servers.connections()):
251251
raise request.cant_handle('"attach" expected')
252252

253+
debug_options = set(request("debugOptions", json.array(unicode)))
254+
255+
# Handling of properties that can also be specified as legacy "debugOptions" flags.
256+
# If property is explicitly set to false, but the flag is in "debugOptions", treat
257+
# it as an error. Returns None if the property wasn't explicitly set either way.
258+
def property_or_debug_option(prop_name, flag_name):
259+
assert prop_name[0].islower() and flag_name[0].isupper()
260+
261+
value = request(prop_name, bool, optional=True)
262+
if value == ():
263+
value = None
264+
265+
if flag_name in debug_options:
266+
if value is False:
267+
raise request.isnt_valid(
268+
'{0!j}:false and "debugOptions":[{1!j}] are mutually exclusive',
269+
prop_name,
270+
flag_name,
271+
)
272+
value = True
273+
274+
return value
275+
253276
# Launcher doesn't use the command line at all, but we pass the arguments so
254277
# that they show up in the terminal if we're using "runInTerminal".
255278
if "program" in request:
@@ -277,8 +300,12 @@ def launch_request(self, request):
277300
)
278301
console_title = request("consoleTitle", json.default("Python Debug Console"))
279302

303+
sudo = bool(property_or_debug_option("sudo", "Sudo"))
304+
if sudo and sys.platform == "win32":
305+
raise request.cant_handle('"sudo":true is not supported on Windows.')
306+
280307
servers.serve()
281-
launchers.spawn_debuggee(self.session, request, args, console, console_title)
308+
launchers.spawn_debuggee(self.session, request, args, console, console_title, sudo)
282309

283310
@_start_message_handler
284311
def attach_request(self, request):

src/debugpy/adapter/launchers.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,13 @@ def terminate_debuggee(self):
6565
pass
6666

6767

68-
def spawn_debuggee(session, start_request, args, console, console_title):
69-
cmdline = [sys.executable, os.path.dirname(launcher.__file__)] + args
68+
def spawn_debuggee(session, start_request, args, console, console_title, sudo):
69+
# -E tells sudo to propagate environment variables to the target process - this
70+
# is necessary for launcher to get DEBUGPY_LAUNCHER_PORT and DEBUGPY_LOG_DIR.
71+
cmdline = ["sudo", "-E"] if sudo else []
72+
73+
cmdline += [sys.executable, os.path.dirname(launcher.__file__)]
74+
cmdline += args
7075
env = {}
7176

7277
arguments = dict(start_request.arguments)
@@ -131,7 +136,9 @@ def on_launcher_connected(sock):
131136
except messaging.MessageHandlingError as exc:
132137
exc.propagate(start_request)
133138

134-
if not session.wait_for(lambda: session.launcher, timeout=10):
139+
# If using sudo, it might prompt for password, and launcher won't start running
140+
# until the user enters it, so don't apply timeout in that case.
141+
if not session.wait_for(lambda: session.launcher, timeout=(None if sudo else 10)):
135142
raise start_request.cant_handle("Timed out waiting for launcher to connect")
136143

137144
try:

src/debugpy/launcher/handlers.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,6 @@ def property_or_debug_option(prop_name, flag_name):
3838

3939
return value
4040

41-
cmdline = []
42-
if property_or_debug_option("sudo", "Sudo"):
43-
if sys.platform == "win32":
44-
raise request.cant_handle('"sudo":true is not supported on Windows.')
45-
else:
46-
cmdline += ["sudo"]
47-
4841
# "pythonPath" is a deprecated legacy spelling. If "python" is missing, then try
4942
# the alternative. But if both are missing, the error message should say "python".
5043
python_key = "python"
@@ -55,10 +48,9 @@ def property_or_debug_option(prop_name, flag_name):
5548
)
5649
elif "pythonPath" in request:
5750
python_key = "pythonPath"
58-
python = request(python_key, json.array(unicode, vectorize=True, size=(0,)))
59-
if not len(python):
60-
python = [compat.filename(sys.executable)]
61-
cmdline += python
51+
cmdline = request(python_key, json.array(unicode, vectorize=True, size=(0,)))
52+
if not len(cmdline):
53+
cmdline = [compat.filename(sys.executable)]
6254

6355
if not request("noDebug", json.default(False)):
6456
port = request("port", int)
@@ -87,7 +79,7 @@ def property_or_debug_option(prop_name, flag_name):
8779
if "code" in request:
8880
code = request("code", json.array(unicode, vectorize=True, size=(1,)))
8981
cmdline += ["-c", "\n".join(code)]
90-
process_name = python[0]
82+
process_name = cmdline[0]
9183

9284
num_targets = len([x for x in (program, module, code) if x != ()])
9385
if num_targets == 0:

tests/debug/session.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -492,8 +492,7 @@ def _process_request(self, request):
492492
env = request("env", json.object(unicode))
493493
try:
494494
exe = args.pop(0)
495-
assert not len(self.spawn_debuggee.env)
496-
self.spawn_debuggee.env = env
495+
self.spawn_debuggee.env.update(env)
497496
self.spawn_debuggee(args, cwd, exe=exe)
498497
return {}
499498
except OSError as exc:

tests/debugpy/test_run.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def test_sudo(pyfile, tmpdir, run, target):
9999
sudo = tmpdir / "sudo"
100100
sudo.write(
101101
"""#!/bin/sh
102+
if [ "$1" = "-E" ]; then shift; fi
102103
exec env DEBUGPY_SUDO=1 "$@"
103104
"""
104105
)
@@ -116,7 +117,9 @@ def code_to_debug():
116117

117118
with debug.Session() as session:
118119
session.config["sudo"] = True
119-
session.config.env["PATH"] = tmpdir.strpath + ":" + os.environ["PATH"]
120+
session.spawn_adapter.env["PATH"] = session.spawn_debuggee.env["PATH"] = (
121+
tmpdir.strpath + ":" + os.environ["PATH"]
122+
)
120123

121124
backchannel = session.open_backchannel()
122125
with run(session, target(code_to_debug)):

0 commit comments

Comments
 (0)