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 argparse 6 import atexit 7 import codecs 8 import locale 9 import os 10 import sys 11 12 # WARNING: debugpy and submodules must not be imported on top level in this module, 13 # and should be imported locally inside main() instead. 14 15 16 def main(): 17 args = _parse_argv(sys.argv) 18 19 # If we're talking DAP over stdio, stderr is not guaranteed to be read from, 20 # so disable it to avoid the pipe filling and locking up. This must be done 21 # as early as possible, before the logging module starts writing to it. 22 if args.port is None: 23 sys.stderr = stderr = open(os.devnull, "w") 24 atexit.register(stderr.close) 25 26 from debugpy import adapter 27 from debugpy.common import json, log, sockets 28 from debugpy.adapter import clients, servers, sessions 29 30 if args.for_server is not None: 31 if os.name == "posix": 32 # On POSIX, we need to leave the process group and its session, and then 33 # daemonize properly by double-forking (first fork already happened when 34 # this process was spawned). 35 # NOTE: if process is already the session leader, then 36 # setsid would fail with `operation not permitted` 37 if os.getsid(os.getpid()) != os.getpid(): 38 os.setsid() 39 if os.fork() != 0: 40 sys.exit(0) 41 42 for stdio in sys.stdin, sys.stdout, sys.stderr: 43 if stdio is not None: 44 stdio.close() 45 46 if args.log_stderr: 47 log.stderr.levels |= set(log.LEVELS) 48 if args.log_dir is not None: 49 log.log_dir = args.log_dir 50 51 log.to_file(prefix="debugpy.adapter") 52 log.describe_environment("debugpy.adapter startup environment:") 53 54 servers.access_token = args.server_access_token 55 if args.for_server is None: 56 adapter.access_token = codecs.encode(os.urandom(32), "hex").decode("ascii") 57 58 endpoints = {} 59 try: 60 client_host, client_port = clients.serve(args.host, args.port) 61 except Exception as exc: 62 if args.for_server is None: 63 raise 64 endpoints = {"error": "Can't listen for client connections: " + str(exc)} 65 else: 66 endpoints["client"] = {"host": client_host, "port": client_port} 67 68 if args.for_server is not None: 69 try: 70 server_host, server_port = servers.serve() 71 except Exception as exc: 72 endpoints = {"error": "Can't listen for server connections: " + str(exc)} 73 else: 74 endpoints["server"] = {"host": server_host, "port": server_port} 75 76 log.info( 77 "Sending endpoints info to debug server at localhost:{0}:\n{1}", 78 args.for_server, 79 json.repr(endpoints), 80 ) 81 82 try: 83 sock = sockets.create_client() 84 try: 85 sock.settimeout(None) 86 sock.connect(("127.0.0.1", args.for_server)) 87 sock_io = sock.makefile("wb", 0) 88 try: 89 sock_io.write(json.dumps(endpoints).encode("utf-8")) 90 finally: 91 sock_io.close() 92 finally: 93 sockets.close_socket(sock) 94 except Exception: 95 log.reraise_exception("Error sending endpoints info to debug server:") 96 97 if "error" in endpoints: 98 log.error("Couldn't set up endpoints; exiting.") 99 sys.exit(1) 100 101 listener_file = os.getenv("DEBUGPY_ADAPTER_ENDPOINTS") 102 if listener_file is not None: 103 log.info( 104 "Writing endpoints info to {0!r}:\n{1}", listener_file, json.repr(endpoints) 105 ) 106 107 def delete_listener_file(): 108 log.info("Listener ports closed; deleting {0!r}", listener_file) 109 try: 110 os.remove(listener_file) 111 except Exception: 112 log.swallow_exception( 113 "Failed to delete {0!r}", listener_file, level="warning" 114 ) 115 116 try: 117 with open(listener_file, "w") as f: 118 atexit.register(delete_listener_file) 119 print(json.dumps(endpoints), file=f) 120 except Exception: 121 log.reraise_exception("Error writing endpoints info to file:") 122 123 if args.port is None: 124 clients.Client("stdio") 125 126 # These must be registered after the one above, to ensure that the listener sockets 127 # are closed before the endpoint info file is deleted - this way, another process 128 # can wait for the file to go away as a signal that the ports are no longer in use. 129 atexit.register(servers.stop_serving) 130 atexit.register(clients.stop_serving) 131 132 servers.wait_until_disconnected() 133 log.info("All debug servers disconnected; waiting for remaining sessions...") 134 135 sessions.wait_until_ended() 136 log.info("All debug sessions have ended; exiting.") 137 138 139 def _parse_argv(argv): 140 parser = argparse.ArgumentParser() 141 142 parser.add_argument( 143 "--for-server", type=int, metavar="PORT", help=argparse.SUPPRESS 144 ) 145 146 parser.add_argument( 147 "--port", 148 type=int, 149 default=None, 150 metavar="PORT", 151 help="start the adapter in debugServer mode on the specified port", 152 ) 153 154 parser.add_argument( 155 "--host", 156 type=str, 157 default="127.0.0.1", 158 metavar="HOST", 159 help="start the adapter in debugServer mode on the specified host", 160 ) 161 162 parser.add_argument( 163 "--access-token", type=str, help="access token expected from the server" 164 ) 165 166 parser.add_argument( 167 "--server-access-token", type=str, help="access token expected by the server" 168 ) 169 170 parser.add_argument( 171 "--log-dir", 172 type=str, 173 metavar="DIR", 174 help="enable logging and use DIR to save adapter logs", 175 ) 176 177 parser.add_argument( 178 "--log-stderr", action="store_true", help="enable logging to stderr" 179 ) 180 181 args = parser.parse_args(argv[1:]) 182 183 if args.port is None: 184 if args.log_stderr: 185 parser.error("--log-stderr requires --port") 186 if args.for_server is not None: 187 parser.error("--for-server requires --port") 188 189 return args 190 191 192 if __name__ == "__main__": 193 # debugpy can also be invoked directly rather than via -m. In this case, the first 194 # entry on sys.path is the one added automatically by Python for the directory 195 # containing this file. This means that import debugpy will not work, since we need 196 # the parent directory of debugpy/ to be in sys.path, rather than debugpy/adapter/. 197 # 198 # The other issue is that many other absolute imports will break, because they 199 # will be resolved relative to debugpy/adapter/ - e.g. `import state` will then try 200 # to import debugpy/adapter/state.py. 201 # 202 # To fix both, we need to replace the automatically added entry such that it points 203 # at parent directory of debugpy/ instead of debugpy/adapter, import debugpy with that 204 # in sys.path, and then remove the first entry entry altogether, so that it doesn't 205 # affect any further imports we might do. For example, suppose the user did: 206 # 207 # python /foo/bar/debugpy/adapter ... 208 # 209 # At the beginning of this script, sys.path will contain "/foo/bar/debugpy/adapter" 210 # as the first entry. What we want is to replace it with "/foo/bar', then import 211 # debugpy with that in effect, and then remove the replaced entry before any more 212 # code runs. The imported debugpy module will remain in sys.modules, and thus all 213 # future imports of it or its submodules will resolve accordingly. 214 if "debugpy" not in sys.modules: 215 # Do not use dirname() to walk up - this can be a relative path, e.g. ".". 216 if os.name == "nt": 217 import pathlib 218 219 windows_path = pathlib.Path(sys.path[0]) 220 sys.path[0] = str(windows_path.parent.parent) 221 else: 222 sys.path[0] = sys.path[0] + "/../../" 223 __import__("debugpy") 224 del sys.path[0] 225 226 # Apply OS-global and user-specific locale settings. 227 try: 228 locale.setlocale(locale.LC_ALL, "") 229 except Exception: 230 # On POSIX, locale is set via environment variables, and this can fail if 231 # those variables reference a non-existing locale. Ignore and continue using 232 # the default "C" locale if so. 233 pass 234 235 main() 236