__main__.py 8.4 KB

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