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