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