|
| 1 | +__author__ = "Microsoft Corporation <ptvshelp@microsoft.com>" |
| 2 | +__version__ = "3.0.0.0" |
| 3 | + |
| 4 | +__all__ = ['enable_attach', 'wait_for_attach', 'break_into_debugger', 'settrace', 'is_attached', 'AttachAlreadyEnabledError'] |
| 5 | + |
| 6 | +import atexit |
| 7 | +import getpass |
| 8 | +import os |
| 9 | +import os.path |
| 10 | +import platform |
| 11 | +import socket |
| 12 | +import struct |
| 13 | +import sys |
| 14 | +import threading |
| 15 | +try: |
| 16 | + import thread |
| 17 | +except ImportError: |
| 18 | + import _thread as thread |
| 19 | +try: |
| 20 | + import ssl |
| 21 | +except ImportError: |
| 22 | + ssl = None |
| 23 | + |
| 24 | +import ptvsd.visualstudio_py_debugger as vspd |
| 25 | +import ptvsd.visualstudio_py_repl as vspr |
| 26 | +from ptvsd.visualstudio_py_util import to_bytes, read_bytes, read_int, read_string, write_bytes, write_int, write_string |
| 27 | + |
| 28 | +PTVS_VER = '2.2' |
| 29 | +DEFAULT_PORT = 5678 |
| 30 | +PTVSDBG_VER = 6 |
| 31 | +PTVSDBG = to_bytes('PTVSDBG') |
| 32 | +ACPT = to_bytes('ACPT') |
| 33 | +RJCT = to_bytes('RJCT') |
| 34 | +INFO = to_bytes('INFO') |
| 35 | +ATCH = to_bytes('ATCH') |
| 36 | +REPL = to_bytes('REPL') |
| 37 | + |
| 38 | +_attach_enabled = False |
| 39 | +_attached = threading.Event() |
| 40 | +vspd.DONT_DEBUG.append(os.path.normcase(__file__)) |
| 41 | + |
| 42 | + |
| 43 | +class AttachAlreadyEnabledError(Exception): |
| 44 | + """`ptvsd.enable_attach` has already been called in this process.""" |
| 45 | + |
| 46 | + |
| 47 | +def enable_attach(secret, address = ('0.0.0.0', DEFAULT_PORT), certfile = None, keyfile = None, redirect_output = True): |
| 48 | + """Enables Python Tools for Visual Studio to attach to this process remotely |
| 49 | + to debug Python code. |
| 50 | +
|
| 51 | + Parameters |
| 52 | + ---------- |
| 53 | + secret : str |
| 54 | + Used to validate the clients - only those clients providing the valid |
| 55 | + secret will be allowed to connect to this server. On client side, the |
| 56 | + secret is prepended to the Qualifier string, separated from the |
| 57 | + hostname by ``'@'``, e.g.: ``'secret@myhost.cloudapp.net:5678'``. If |
| 58 | + secret is ``None``, there's no validation, and any client can connect |
| 59 | + freely. |
| 60 | + address : (str, int), optional |
| 61 | + Specifies the interface and port on which the debugging server should |
| 62 | + listen for TCP connections. It is in the same format as used for |
| 63 | + regular sockets of the `socket.AF_INET` family, i.e. a tuple of |
| 64 | + ``(hostname, port)``. On client side, the server is identified by the |
| 65 | + Qualifier string in the usual ``'hostname:port'`` format, e.g.: |
| 66 | + ``'myhost.cloudapp.net:5678'``. Default is ``('0.0.0.0', 5678)``. |
| 67 | + certfile : str, optional |
| 68 | + Used to enable SSL. If not specified, or if set to ``None``, the |
| 69 | + connection between this program and the debugger will be unsecure, |
| 70 | + and can be intercepted on the wire. If specified, the meaning of this |
| 71 | + parameter is the same as for `ssl.wrap_socket`. |
| 72 | + keyfile : str, optional |
| 73 | + Used together with `certfile` when SSL is enabled. Its meaning is the |
| 74 | + same as for ``ssl.wrap_socket``. |
| 75 | + redirect_output : bool, optional |
| 76 | + Specifies whether any output (on both `stdout` and `stderr`) produced |
| 77 | + by this program should be sent to the debugger. Default is ``True``. |
| 78 | +
|
| 79 | + Notes |
| 80 | + ----- |
| 81 | + This function returns immediately after setting up the debugging server, |
| 82 | + and does not block program execution. If you need to block until debugger |
| 83 | + is attached, call `ptvsd.wait_for_attach`. The debugger can be detached |
| 84 | + and re-attached multiple times after `enable_attach` is called. |
| 85 | +
|
| 86 | + This function can only be called once during the lifetime of the process. |
| 87 | + On a second call, `AttachAlreadyEnabledError` is raised. In circumstances |
| 88 | + where the caller does not control how many times the function will be |
| 89 | + called (e.g. when a script with a single call is run more than once by |
| 90 | + a hosting app or framework), the call should be wrapped in ``try..except``. |
| 91 | +
|
| 92 | + Only the thread on which this function is called, and any threads that are |
| 93 | + created after it returns, will be visible in the debugger once it is |
| 94 | + attached. Any threads that are already running before this function is |
| 95 | + called will not be visible. |
| 96 | + """ |
| 97 | + |
| 98 | + if not ssl and (certfile or keyfile): |
| 99 | + raise ValueError('could not import the ssl module - SSL is not supported on this version of Python') |
| 100 | + |
| 101 | + if sys.platform == 'cli': |
| 102 | + import clr |
| 103 | + x_tracing = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Tracing |
| 104 | + x_frames = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Frames |
| 105 | + if not x_tracing or not x_frames: |
| 106 | + raise RuntimeError('IronPython must be started with -X:Tracing and -X:Frames options to support PTVS remote debugging.') |
| 107 | + |
| 108 | + global _attach_enabled |
| 109 | + if _attach_enabled: |
| 110 | + raise AttachAlreadyEnabledError('ptvsd.enable_attach() has already been called in this process.') |
| 111 | + _attach_enabled = True |
| 112 | + |
| 113 | + atexit.register(vspd.detach_process_and_notify_debugger) |
| 114 | + |
| 115 | + server = socket.socket(proto=socket.IPPROTO_TCP) |
| 116 | + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| 117 | + server.bind(address) |
| 118 | + server.listen(1) |
| 119 | + def server_thread_func(): |
| 120 | + while True: |
| 121 | + client = None |
| 122 | + raw_client = None |
| 123 | + try: |
| 124 | + client, addr = server.accept() |
| 125 | + if certfile: |
| 126 | + client = ssl.wrap_socket(client, server_side = True, ssl_version = ssl.PROTOCOL_TLSv1, certfile = certfile, keyfile = keyfile) |
| 127 | + write_bytes(client, PTVSDBG) |
| 128 | + write_int(client, PTVSDBG_VER) |
| 129 | + |
| 130 | + response = read_bytes(client, 7) |
| 131 | + if response != PTVSDBG: |
| 132 | + continue |
| 133 | + dbg_ver = read_int(client) |
| 134 | + if dbg_ver != PTVSDBG_VER: |
| 135 | + continue |
| 136 | + |
| 137 | + client_secret = read_string(client) |
| 138 | + if secret is None or secret == client_secret: |
| 139 | + write_bytes(client, ACPT) |
| 140 | + else: |
| 141 | + write_bytes(client, RJCT) |
| 142 | + continue |
| 143 | + |
| 144 | + response = read_bytes(client, 4) |
| 145 | + |
| 146 | + if response == INFO: |
| 147 | + try: |
| 148 | + pid = os.getpid() |
| 149 | + except AttributeError: |
| 150 | + pid = 0 |
| 151 | + write_int(client, pid) |
| 152 | + |
| 153 | + exe = sys.executable or '' |
| 154 | + write_string(client, exe) |
| 155 | + |
| 156 | + try: |
| 157 | + username = getpass.getuser() |
| 158 | + except AttributeError: |
| 159 | + username = '' |
| 160 | + write_string(client, username) |
| 161 | + |
| 162 | + try: |
| 163 | + impl = platform.python_implementation() |
| 164 | + except AttributeError: |
| 165 | + try: |
| 166 | + impl = sys.implementation.name |
| 167 | + except AttributeError: |
| 168 | + impl = 'Python' |
| 169 | + |
| 170 | + major, minor, micro, release_level, serial = sys.version_info |
| 171 | + |
| 172 | + os_and_arch = platform.system() |
| 173 | + if os_and_arch == "": |
| 174 | + os_and_arch = sys.platform |
| 175 | + try: |
| 176 | + if sys.maxsize > 2**32: |
| 177 | + os_and_arch += ' 64-bit' |
| 178 | + else: |
| 179 | + os_and_arch += ' 32-bit' |
| 180 | + except AttributeError: |
| 181 | + pass |
| 182 | + |
| 183 | + version = '%s %s.%s.%s (%s)' % (impl, major, minor, micro, os_and_arch) |
| 184 | + write_string(client, version) |
| 185 | + |
| 186 | + client.recv(1) |
| 187 | + |
| 188 | + elif response == ATCH: |
| 189 | + debug_options = vspd.parse_debug_options(read_string(client)) |
| 190 | + if redirect_output: |
| 191 | + debug_options.add('RedirectOutput') |
| 192 | + |
| 193 | + if vspd.DETACHED: |
| 194 | + write_bytes(client, ACPT) |
| 195 | + try: |
| 196 | + pid = os.getpid() |
| 197 | + except AttributeError: |
| 198 | + pid = 0 |
| 199 | + write_int(client, pid) |
| 200 | + |
| 201 | + major, minor, micro, release_level, serial = sys.version_info |
| 202 | + write_int(client, major) |
| 203 | + write_int(client, minor) |
| 204 | + write_int(client, micro) |
| 205 | + |
| 206 | + vspd.attach_process_from_socket(client, debug_options, report = True) |
| 207 | + vspd.mark_all_threads_for_break(vspd.STEPPING_ATTACH_BREAK) |
| 208 | + _attached.set() |
| 209 | + client = None |
| 210 | + else: |
| 211 | + write_bytes(client, RJCT) |
| 212 | + |
| 213 | + elif response == REPL: |
| 214 | + if not vspd.DETACHED: |
| 215 | + write_bytes(client, ACPT) |
| 216 | + vspd.connect_repl_using_socket(client) |
| 217 | + client = None |
| 218 | + else: |
| 219 | + write_bytes(client, RJCT) |
| 220 | + |
| 221 | + except (socket.error, OSError): |
| 222 | + pass |
| 223 | + finally: |
| 224 | + if client is not None: |
| 225 | + client.close() |
| 226 | + |
| 227 | + server_thread = threading.Thread(target = server_thread_func) |
| 228 | + server_thread.setDaemon(True) |
| 229 | + server_thread.start() |
| 230 | + |
| 231 | + frames = [] |
| 232 | + f = sys._getframe() |
| 233 | + while True: |
| 234 | + f = f.f_back |
| 235 | + if f is None: |
| 236 | + break |
| 237 | + frames.append(f) |
| 238 | + frames.reverse() |
| 239 | + cur_thread = vspd.new_thread() |
| 240 | + for f in frames: |
| 241 | + cur_thread.push_frame(f) |
| 242 | + def replace_trace_func(): |
| 243 | + for f in frames: |
| 244 | + f.f_trace = cur_thread.trace_func |
| 245 | + replace_trace_func() |
| 246 | + sys.settrace(cur_thread.trace_func) |
| 247 | + vspd.intercept_threads(for_attach = True) |
| 248 | + |
| 249 | + |
| 250 | +settrace = enable_attach |
| 251 | + |
| 252 | + |
| 253 | +def wait_for_attach(timeout = None): |
| 254 | + """If a PTVS remote debugger is attached, returns immediately. Otherwise, |
| 255 | + blocks until a remote debugger attaches to this process, or until the |
| 256 | + optional timeout occurs. |
| 257 | +
|
| 258 | + Parameters |
| 259 | + ---------- |
| 260 | + timeout : float, optional |
| 261 | + The timeout for the operation in seconds (or fractions thereof). |
| 262 | + """ |
| 263 | + if vspd.DETACHED: |
| 264 | + _attached.clear() |
| 265 | + _attached.wait(timeout) |
| 266 | + |
| 267 | + |
| 268 | +def break_into_debugger(): |
| 269 | + """If a PTVS remote debugger is attached, pauses execution of all threads, |
| 270 | + and breaks into the debugger with current thread as active. |
| 271 | + """ |
| 272 | + if not vspd.DETACHED: |
| 273 | + vspd.SEND_BREAK_COMPLETE = thread.get_ident() |
| 274 | + vspd.mark_all_threads_for_break() |
| 275 | + |
| 276 | +def is_attached(): |
| 277 | + """Returns ``True`` if debugger is attached, ``False`` otherwise.""" |
| 278 | + return not vspd.DETACHED |
0 commit comments