From 65168dd784aa313d7c0474c7d3df11756aa17b8f Mon Sep 17 00:00:00 2001 From: Pavel Minaev Date: Tue, 21 Apr 2020 19:24:29 -0700 Subject: [PATCH] Fix #86: Shell expansion is not working in "args" Propagate "args" to the launcher via command line rather than debug configuration JSON. Parse "cwd" in the adapter, and apply it when spawning the launcher. --- src/debugpy/adapter/clients.py | 50 ++++++++++++++++++++++++-------- src/debugpy/adapter/launchers.py | 21 +++++++------- src/debugpy/launcher/__main__.py | 2 +- src/debugpy/launcher/debuggee.py | 10 +++---- src/debugpy/launcher/handlers.py | 39 +++++-------------------- 5 files changed, 62 insertions(+), 60 deletions(-) diff --git a/src/debugpy/adapter/clients.py b/src/debugpy/adapter/clients.py index 4b00fa70..a69a9161 100644 --- a/src/debugpy/adapter/clients.py +++ b/src/debugpy/adapter/clients.py @@ -272,22 +272,41 @@ class Client(components.Component): 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". + # Propagate command line arguments via launcher CLI rather than "args", so that + # they get shell expansion applied to them in "runInTerminal" scenarios. + + program = module = code = () 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,)) + program = request("program", unicode) + args = [program] + # process_name = program + if "module" in request: + module = request("module", unicode) + args = ["-m", module] + # process_name = module + if "code" in request: + code = request("code", json.array(unicode, vectorize=True, size=(1,))) + args = ["-c", "\n".join(code)] + # process_name = cmdline[0] + + 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 "code" in request: - args = ["-c"] + request( - "code", json.array(unicode, vectorize=True, size=(1,)) + elif num_targets != 1: + raise request.isnt_valid( + '"program", "module", and "code" are mutually exclusive' ) - else: - args = [] + args += request("args", json.array(unicode)) + 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( @@ -307,7 +326,14 @@ class Client(components.Component): servers.serve() launchers.spawn_debuggee( - self.session, request, launcher_path, args, console, console_title, sudo + self.session, + request, + launcher_path, + args, + cwd, + console, + console_title, + sudo, ) @_start_message_handler diff --git a/src/debugpy/adapter/launchers.py b/src/debugpy/adapter/launchers.py index b9c1da00..ce502fb8 100644 --- a/src/debugpy/adapter/launchers.py +++ b/src/debugpy/adapter/launchers.py @@ -66,7 +66,7 @@ class Launcher(components.Component): def spawn_debuggee( - session, start_request, launcher_path, args, console, console_title, sudo + session, start_request, launcher_path, 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. @@ -119,6 +119,7 @@ def spawn_debuggee( # 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, @@ -130,16 +131,16 @@ def spawn_debuggee( 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", - { - "kind": kinds[console], - "title": console_title, - "args": cmdline, - "env": env, - }, - ) + session.client.channel.request("runInTerminal", request_args) except messaging.MessageHandlingError as exc: exc.propagate(start_request) diff --git a/src/debugpy/launcher/__main__.py b/src/debugpy/launcher/__main__.py index 845e0d96..482ef4e2 100644 --- a/src/debugpy/launcher/__main__.py +++ b/src/debugpy/launcher/__main__.py @@ -35,7 +35,7 @@ def main(): # and everything after "--" is command line arguments for the debuggee. sep = sys.argv.index("--") launcher_argv = sys.argv[1:sep] - sys.argv = sys.argv[sep + 1:] + sys.argv = [sys.argv[0]] + sys.argv[sep + 1:] # The first argument specifies the host/port on which the adapter is waiting # for launcher to connect. It's either host:port, or just port. diff --git a/src/debugpy/launcher/debuggee.py b/src/debugpy/launcher/debuggee.py index 9290a786..fc0b1c0c 100644 --- a/src/debugpy/launcher/debuggee.py +++ b/src/debugpy/launcher/debuggee.py @@ -31,13 +31,11 @@ def describe(): return fmt("Debuggee[PID={0}]", process.pid) -def spawn(process_name, cmdline, cwd, env, redirect_output): +def spawn(process_name, cmdline, env, redirect_output): log.info( "Spawning debuggee process:\n\n" - "Current directory: {0!r}\n\n" - "Command line: {1!r}\n\n" - "Environment variables: {2!r}\n\n", - cwd, + "Command line: {0!r}\n\n" + "Environment variables: {1!r}\n\n", cmdline, env, ) @@ -56,7 +54,7 @@ def spawn(process_name, cmdline, cwd, env, redirect_output): try: global process - process = subprocess.Popen(cmdline, cwd=cwd, env=env, bufsize=0, **kwargs) + 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) diff --git a/src/debugpy/launcher/handlers.py b/src/debugpy/launcher/handlers.py index b353a24c..811bc554 100644 --- a/src/debugpy/launcher/handlers.py +++ b/src/debugpy/launcher/handlers.py @@ -67,37 +67,14 @@ def launch_request(request): debugpy_args = request("debugpyArgs", json.array(unicode)) cmdline += debugpy_args - program = module = code = () - if "program" in request: - program = request("program", unicode) - cmdline += [program] - process_name = program - if "module" in request: - module = request("module", unicode) - cmdline += ["-m", module] - process_name = module - if "code" in request: - code = request("code", json.array(unicode, vectorize=True, size=(1,))) - cmdline += ["-c", "\n".join(code)] - process_name = cmdline[0] + # Use command line arguments propagated via launcher CLI, rather than "args", to get + # their values after shell expansion in "runInTerminal" scenarios. The command line + # parser in __main__ has already removed everything up to and including "--" by now. + cmdline += sys.argv[1:] - 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' - ) - - cmdline += request("args", json.array(unicode)) - - 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[0]) or None) + # We want the process name to reflect the target. So for -m, use the module name + # that follows; otherwise use the first argument, which is either -c or filename. + process_name = compat.filename(sys.argv[1] if sys.argv[1] != "-m" else sys.argv[2]) env = os.environ.copy() env_changes = request("env", json.object(unicode)) @@ -161,7 +138,7 @@ def launch_request(request): cmdline = [encode(s) for s in cmdline] env = {encode(k): encode(v) for k, v in env.items()} - debuggee.spawn(process_name, cmdline, cwd, env, redirect_output) + debuggee.spawn(process_name, cmdline, env, redirect_output) return {}