1 # Copyright (c) Microsoft Corporation. All rights reserved. 2 # Licensed under the MIT License. See LICENSE in the project root 3 # for license information. 4 5 import os 6 import subprocess 7 import sys 8 9 from debugpy import adapter, common 10 from debugpy.common import log, messaging, sockets 11 from debugpy.adapter import components, servers, sessions 12 13 listener = None 14 15 16 class Launcher(components.Component): 17 """Handles the launcher side of a debug session.""" 18 19 message_handler = components.Component.message_handler 20 21 def __init__(self, session, stream): 22 with session: 23 assert not session.launcher 24 super().__init__(session, stream) 25 26 self.pid = None 27 """Process ID of the debuggee process, as reported by the launcher.""" 28 29 self.exit_code = None 30 """Exit code of the debuggee process.""" 31 32 session.launcher = self 33 34 @message_handler 35 def process_event(self, event): 36 self.pid = event("systemProcessId", int) 37 self.client.propagate_after_start(event) 38 39 @message_handler 40 def output_event(self, event): 41 self.client.propagate_after_start(event) 42 43 @message_handler 44 def exited_event(self, event): 45 self.exit_code = event("exitCode", int) 46 # We don't want to tell the client about this just yet, because it will then 47 # want to disconnect, and the launcher might still be waiting for keypress 48 # (if wait-on-exit was enabled). Instead, we'll report the event when we 49 # receive "terminated" from the launcher, right before it exits. 50 51 @message_handler 52 def terminated_event(self, event): 53 try: 54 self.client.channel.send_event("exited", {"exitCode": self.exit_code}) 55 except Exception: 56 pass 57 self.channel.close() 58 59 def terminate_debuggee(self): 60 with self.session: 61 if self.exit_code is None: 62 try: 63 self.channel.request("terminate") 64 except Exception: 65 pass 66 67 68 def spawn_debuggee( 69 session, 70 start_request, 71 python, 72 launcher_path, 73 adapter_host, 74 args, 75 shell_expand_args, 76 cwd, 77 console, 78 console_title, 79 sudo, 80 ): 81 global listener 82 83 # -E tells sudo to propagate environment variables to the target process - this 84 # is necessary for launcher to get DEBUGPY_LAUNCHER_PORT and DEBUGPY_LOG_DIR. 85 cmdline = ["sudo", "-E"] if sudo else [] 86 cmdline += python 87 cmdline += [launcher_path] 88 env = {} 89 90 arguments = dict(start_request.arguments) 91 if not session.no_debug: 92 _, arguments["port"] = servers.listener.getsockname() 93 arguments["adapterAccessToken"] = adapter.access_token 94 95 def on_launcher_connected(sock): 96 listener.close() 97 stream = messaging.JsonIOStream.from_socket(sock) 98 Launcher(session, stream) 99 100 try: 101 listener = sockets.serve( 102 "Launcher", on_launcher_connected, adapter_host, backlog=1 103 ) 104 except Exception as exc: 105 raise start_request.cant_handle( 106 "{0} couldn't create listener socket for launcher: {1}", session, exc 107 ) 108 sessions.report_sockets() 109 110 try: 111 launcher_host, launcher_port = listener.getsockname() 112 launcher_addr = ( 113 launcher_port 114 if launcher_host == "127.0.0.1" 115 else f"{launcher_host}:{launcher_port}" 116 ) 117 cmdline += [str(launcher_addr), "--"] 118 cmdline += args 119 120 if log.log_dir is not None: 121 env[str("DEBUGPY_LOG_DIR")] = log.log_dir 122 if log.stderr.levels != {"warning", "error"}: 123 env[str("DEBUGPY_LOG_STDERR")] = str(" ".join(log.stderr.levels)) 124 125 if console == "internalConsole": 126 log.info("{0} spawning launcher: {1!r}", session, cmdline) 127 try: 128 # If we are talking to the client over stdio, sys.stdin and sys.stdout 129 # are redirected to avoid mangling the DAP message stream. Make sure 130 # the launcher also respects that. 131 subprocess.Popen( 132 cmdline, 133 cwd=cwd, 134 env=dict(list(os.environ.items()) + list(env.items())), 135 stdin=sys.stdin, 136 stdout=sys.stdout, 137 stderr=sys.stderr, 138 ) 139 except Exception as exc: 140 raise start_request.cant_handle("Failed to spawn launcher: {0}", exc) 141 else: 142 log.info('{0} spawning launcher via "runInTerminal" request.', session) 143 session.client.capabilities.require("supportsRunInTerminalRequest") 144 kinds = {"integratedTerminal": "integrated", "externalTerminal": "external"} 145 request_args = { 146 "kind": kinds[console], 147 "title": console_title, 148 "args": cmdline, 149 "env": env, 150 } 151 if cwd is not None: 152 request_args["cwd"] = cwd 153 if shell_expand_args: 154 request_args["argsCanBeInterpretedByShell"] = True 155 try: 156 # It is unspecified whether this request receives a response immediately, or only 157 # after the spawned command has completed running, so do not block waiting for it. 158 session.client.channel.send_request("runInTerminal", request_args) 159 except messaging.MessageHandlingError as exc: 160 exc.propagate(start_request) 161 162 # If using sudo, it might prompt for password, and launcher won't start running 163 # until the user enters it, so don't apply timeout in that case. 164 if not session.wait_for( 165 lambda: session.launcher, 166 timeout=(None if sudo else common.PROCESS_SPAWN_TIMEOUT), 167 ): 168 raise start_request.cant_handle("Timed out waiting for launcher to connect") 169 170 try: 171 session.launcher.channel.request(start_request.command, arguments) 172 except messaging.MessageHandlingError as exc: 173 exc.propagate(start_request) 174 175 if not session.wait_for( 176 lambda: session.launcher.pid is not None, 177 timeout=common.PROCESS_SPAWN_TIMEOUT, 178 ): 179 raise start_request.cant_handle( 180 'Timed out waiting for "process" event from launcher' 181 ) 182 183 if session.no_debug: 184 return 185 186 # Wait for the first incoming connection regardless of the PID - it won't 187 # necessarily match due to the use of stubs like py.exe or "conda run". 188 conn = servers.wait_for_connection( 189 session, lambda conn: True, timeout=common.PROCESS_SPAWN_TIMEOUT 190 ) 191 if conn is None: 192 raise start_request.cant_handle("Timed out waiting for debuggee to spawn") 193 conn.attach_to_session(session) 194 195 finally: 196 listener.close() 197 listener = None 198 sessions.report_sockets() 199