224 lines
7.9 KiB
Python
224 lines
7.9 KiB
Python
# 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).
|
|
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))
|