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 {}