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