diff --git a/src/ptvsd/__init__.py b/src/ptvsd/__init__.py index ba3ebbe5..5ff19eab 100644 --- a/src/ptvsd/__init__.py +++ b/src/ptvsd/__init__.py @@ -37,7 +37,7 @@ from ptvsd import _version __version__ = _version.get_versions()["version"] del _version -from ptvsd.server.attach_server import ( +from ptvsd.server.attach import ( attach, break_into_debugger, debug_this_thread, diff --git a/src/ptvsd/_vendored/__init__.py b/src/ptvsd/_vendored/__init__.py index f963e11a..4e4873bc 100644 --- a/src/ptvsd/_vendored/__init__.py +++ b/src/ptvsd/_vendored/__init__.py @@ -1,3 +1,9 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root +# for license information. + +from __future__ import absolute_import, print_function, unicode_literals + import contextlib from importlib import import_module import os diff --git a/src/ptvsd/_vendored/_pydevd_packaging.py b/src/ptvsd/_vendored/_pydevd_packaging.py index 2a713309..0728d98e 100644 --- a/src/ptvsd/_vendored/_pydevd_packaging.py +++ b/src/ptvsd/_vendored/_pydevd_packaging.py @@ -1,3 +1,9 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root +# for license information. + +from __future__ import absolute_import, print_function, unicode_literals + from . import VENDORED_ROOT from ._util import cwd, iter_all_files diff --git a/src/ptvsd/_vendored/_util.py b/src/ptvsd/_vendored/_util.py index 7ac4d37a..117c0d92 100644 --- a/src/ptvsd/_vendored/_util.py +++ b/src/ptvsd/_vendored/_util.py @@ -1,3 +1,9 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root +# for license information. + +from __future__ import absolute_import, print_function, unicode_literals + import contextlib import os import os.path diff --git a/src/ptvsd/_vendored/force_pydevd.py b/src/ptvsd/_vendored/force_pydevd.py index fcaaf223..461e2286 100644 --- a/src/ptvsd/_vendored/force_pydevd.py +++ b/src/ptvsd/_vendored/force_pydevd.py @@ -1,3 +1,9 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root +# for license information. + +from __future__ import absolute_import, print_function, unicode_literals + from importlib import import_module import warnings @@ -50,3 +56,15 @@ def ptvsd_breakpointhook(): pydevd.install_breakpointhook(ptvsd_breakpointhook) + +# Ensure that pydevd uses JSON protocol +from _pydevd_bundle import pydevd_constants +from _pydevd_bundle import pydevd_defaults +pydevd_defaults.PydevdCustomization.DEFAULT_PROTOCOL = pydevd_constants.HTTP_JSON_PROTOCOL + +# Ensure our patch args is used. This is invoked when a child process is spawned +# with multiproc debugging enabled. +from _pydev_bundle import pydev_monkey +from ptvsd.server import multiproc +pydev_monkey.patch_args = multiproc.patch_and_quote_args +pydev_monkey.patch_new_process_functions() diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py index 5f7adb04..7aa0137f 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_api.py @@ -26,8 +26,10 @@ class PyDevdAPI(object): def run(self, py_db): py_db.ready_to_run = True + def notify_initialize(self, py_db): + py_db.on_initialize() + def notify_configuration_done(self, py_db): - py_db.dap_debugger_attached.set() py_db.on_configuration_done() def notify_disconnect(self, py_db): @@ -124,7 +126,7 @@ class PyDevdAPI(object): self.remove_all_breakpoints(py_db, filename='*') self.remove_all_exception_breakpoints(py_db) self.notify_disconnect(py_db) - py_db.dap_debugger_attached.clear() + if resume_threads: self.request_resume_thread(thread_id='*') @@ -568,7 +570,10 @@ class PyDevdAPI(object): reset_caches = True def custom_dont_trace_external_files(abs_path): - return abs_path.startswith(start_patterns) or abs_path.endswith(end_patterns) + for p in start_patterns: + if p in abs_path: + return True + return abs_path.endswith(end_patterns) custom_dont_trace_external_files.start_patterns = start_patterns custom_dont_trace_external_files.end_patterns = end_patterns diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py index b0e43dd3..c3ee1954 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py @@ -71,9 +71,9 @@ from _pydev_bundle.pydev_imports import _queue from _pydev_imps._pydev_saved_modules import time from _pydev_imps._pydev_saved_modules import thread from _pydev_imps._pydev_saved_modules import threading -from socket import AF_INET, SOCK_STREAM, SHUT_RD, SHUT_WR, SOL_SOCKET, SO_REUSEADDR, SHUT_RDWR -from _pydevd_bundle.pydevd_constants import (DebugInfoHolder, get_thread_id, IS_JYTHON, IS_PY2, - IS_PY36_OR_GREATER, STATE_RUN, dict_keys, ASYNC_EVAL_TIMEOUT_SEC, GlobalDebuggerHolder, +from socket import AF_INET, SOCK_STREAM, SHUT_RD, SHUT_WR, SOL_SOCKET, SO_REUSEADDR, SHUT_RDWR, IPPROTO_TCP +from _pydevd_bundle.pydevd_constants import (DebugInfoHolder, get_thread_id, IS_WINDOWS, IS_JYTHON, + IS_PY2, IS_PY36_OR_GREATER, STATE_RUN, dict_keys, ASYNC_EVAL_TIMEOUT_SEC, GlobalDebuggerHolder, get_global_debugger, GetGlobalDebugger, set_global_debugger) # Keep for backward compatibility @UnusedImport from _pydev_bundle.pydev_override import overrides import weakref @@ -227,7 +227,10 @@ class ReaderThread(PyDBDaemonThread): self._buffer = self._buffer[size:] return ret - r = self.sock.recv(max(size - buffer_len, 1024)) + try: + r = self.sock.recv(max(size - buffer_len, 1024)) + except OSError: + return b'' if not r: return b'' self._buffer += r @@ -241,7 +244,10 @@ class ReaderThread(PyDBDaemonThread): self._buffer = self._buffer[i:] return ret else: - r = self.sock.recv(1024) + try: + r = self.sock.recv(1024) + except OSError: + return b'' if not r: return b'' self._buffer += r @@ -343,7 +349,7 @@ class WriterThread(PyDBDaemonThread): def add_command(self, cmd): ''' cmd is NetCommand ''' if not self.killReceived: # we don't take new data after everybody die - self.cmdQueue.put(cmd) + self.cmdQueue.put(cmd, False) @overrides(PyDBDaemonThread._on_run) def _on_run(self): @@ -353,7 +359,7 @@ class WriterThread(PyDBDaemonThread): while True: try: try: - cmd = self.cmdQueue.get(1, 0.1) + cmd = self.cmdQueue.get(True, 0.1) except _queue.Empty: if self.killReceived: try: @@ -367,6 +373,9 @@ class WriterThread(PyDBDaemonThread): return # break if queue is empty and killReceived else: + if time is None: + break # interpreter shutdown + time.sleep(self.timeout) continue except: # pydev_log.info('Finishing debug communication...(1)') @@ -404,18 +413,21 @@ class WriterThread(PyDBDaemonThread): def create_server_socket(host, port): - s = socket(AF_INET, SOCK_STREAM) - s.settimeout(None) - try: - from socket import SO_REUSEPORT - s.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1) - except ImportError: - s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) + if IS_WINDOWS: + from socket import SO_EXCLUSIVEADDRUSE + server.setsockopt(SOL_SOCKET, SO_EXCLUSIVEADDRUSE, 1) + else: + server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) - s.bind((host, port)) - pydev_log.info("Bound to port :%s", port) - return s + server.bind((host, port)) + server.settimeout(None) + except Exception: + server.close() + raise + + return server def start_server(port): diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_defaults.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_defaults.py new file mode 100644 index 00000000..aa4c87f8 --- /dev/null +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_defaults.py @@ -0,0 +1,8 @@ +''' +This module holds the customization settings for the debugger. +''' +from _pydevd_bundle.pydevd_constants import QUOTED_LINE_PROTOCOL + + +class PydevdCustomization(object): + DEFAULT_PROTOCOL = QUOTED_LINE_PROTOCOL \ No newline at end of file diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_dont_trace_files.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_dont_trace_files.py index df4760f0..520c0a3d 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_dont_trace_files.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_dont_trace_files.py @@ -75,6 +75,7 @@ DONT_TRACE = { 'pydevd_constants.py': PYDEV_FILE, 'pydevd_custom_frames.py': PYDEV_FILE, 'pydevd_cython_wrapper.py': PYDEV_FILE, + 'pydevd_defaults.py': PYDEV_FILE, 'pydevd_dont_trace.py': PYDEV_FILE, 'pydevd_dont_trace_files.py': PYDEV_FILE, 'pydevd_exec.py': PYDEV_FILE, diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py index 3ed3ed3a..6e2f6399 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py @@ -195,7 +195,6 @@ class _PyDevJsonCommandProcessor(object): py_db._main_lock.acquire() try: - cmd = on_request(py_db, request) if cmd is not None: py_db.writer.add_command(cmd) @@ -225,6 +224,7 @@ class _PyDevJsonCommandProcessor(object): {'filter': 'uncaught', 'label': 'Uncaught Exceptions', 'default': True}, ], } + self.api.notify_initialize(py_db) response = pydevd_base_schema.build_response(request, kwargs={'body': body}) return NetCommand(CMD_RETURN, 0, response, is_json=True) diff --git a/src/ptvsd/_vendored/pydevd/pydevd.py b/src/ptvsd/_vendored/pydevd/pydevd.py index 44580a60..17fb86ae 100644 --- a/src/ptvsd/_vendored/pydevd/pydevd.py +++ b/src/ptvsd/_vendored/pydevd/pydevd.py @@ -38,6 +38,7 @@ from _pydevd_bundle.pydevd_constants import (IS_JYTH_LESS25, get_thread_id, get_ clear_cached_thread_id, INTERACTIVE_MODE_AVAILABLE, SHOW_DEBUG_INFO_ENV, IS_PY34_OR_GREATER, IS_PY2, NULL, NO_FTRACE, IS_IRONPYTHON) from _pydevd_bundle.pydevd_custom_frames import CustomFramesContainer, custom_frames_container_init +from _pydevd_bundle.pydevd_defaults import PydevdCustomization from _pydevd_bundle.pydevd_dont_trace_files import DONT_TRACE, PYDEV_FILE from _pydevd_bundle.pydevd_extension_api import DebuggerEventHandler from _pydevd_bundle.pydevd_frame_utils import add_exception_to_frame, remove_exception_from_frame @@ -397,6 +398,9 @@ class PyDB(object): self.breakpoints = {} + # Set communication protocol + PyDevdAPI().set_protocol(self, 0, PydevdCustomization.DEFAULT_PROTOCOL) + # mtime to be raised when breakpoints change self.mtime = 0 @@ -475,6 +479,9 @@ class PyDB(object): self._local_thread_trace_func = threading.local() + self._server_socket_ready_event = threading.Event() + self._server_socket_name = None + # Bind many locals to the debugger because upon teardown those names may become None # in the namespace (and thus can't be relied upon unless the reference was previously # saved). @@ -515,8 +522,11 @@ class PyDB(object): self._exclude_by_filter_cache = {} self._apply_filter_cache = {} - # State for use with DAP based connections - self.dap_debugger_attached = threading.Event() + def on_initialize(self): + ''' + Note: only called when using the DAP (Debug Adapter Protocol). + ''' + self._on_configuration_done_event.clear() def on_configuration_done(self): ''' @@ -524,6 +534,9 @@ class PyDB(object): ''' self._on_configuration_done_event.set() + def is_attached(self): + return self._on_configuration_done_event.is_set() + def on_disconnect(self): ''' Note: only called when using the DAP (Debug Adapter Protocol). @@ -924,9 +937,16 @@ class PyDB(object): if self._waiting_for_connection_thread is not None: raise AssertionError('There is already another thread waiting for a connection.') + self._server_socket_ready_event.clear() self._waiting_for_connection_thread = self._WaitForConnectionThread(self) self._waiting_for_connection_thread.start() + def set_server_socket_ready(self): + self._server_socket_ready_event.set() + + def wait_for_server_socket_ready(self): + self._server_socket_ready_event.wait() + class _WaitForConnectionThread(PyDBDaemonThread): def __init__(self, py_db): @@ -939,6 +959,8 @@ class PyDB(object): port = SetupHolder.setup['port'] self._server_socket = create_server_socket(host=host, port=port) + self.py_db._server_socket_name = self._server_socket.getsockname() + self.py_db.set_server_socket_ready() while not self.killReceived: try: @@ -1726,7 +1748,6 @@ class PyDB(object): sys.path.insert(0, os.path.split(rPath(file))[0]) if set_trace: - while not self.ready_to_run: time.sleep(0.1) # busy wait until we receive run command @@ -1942,12 +1963,15 @@ def _enable_attach(address): if port != SetupHolder.setup['port']: raise AssertionError('Unable to listen in port: %s (already listening in port: %s)' % (port, SetupHolder.setup['port'])) settrace(host=host, port=port, suspend=False, wait_for_ready_to_run=False, block_until_connected=False) + py_db = get_global_debugger() + py_db.wait_for_server_socket_ready() + return py_db._server_socket_name def _wait_for_attach(): ''' Meant to be called after _enable_attach() -- the current thread will only unblock after a - connection is in place and the the DAP (Debug Adapter Protocol) sends the ConfigurationDone + connection is in place and the DAP (Debug Adapter Protocol) sends the ConfigurationDone request. ''' py_db = get_global_debugger() @@ -1957,6 +1981,14 @@ def _wait_for_attach(): py_db.block_until_configuration_done() +def _is_attached(): + ''' + Can be called any time to check if the connection was established and the DAP (Debug Adapter Protocol) has sent + the ConfigurationDone request. + ''' + py_db = get_global_debugger() + return (py_db is not None) and py_db.is_attached() + #======================================================================================================================= # settrace #======================================================================================================================= diff --git a/src/ptvsd/_version.py b/src/ptvsd/_version.py index 957691fc..5b844f93 100644 --- a/src/ptvsd/_version.py +++ b/src/ptvsd/_version.py @@ -2,6 +2,8 @@ # Licensed under the MIT License. See LICENSE in the project root # for license information. +from __future__ import absolute_import, print_function, unicode_literals + # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build diff --git a/src/ptvsd/adapter/messages.py b/src/ptvsd/adapter/messages.py index 447e320d..7d6f4d9d 100644 --- a/src/ptvsd/adapter/messages.py +++ b/src/ptvsd/adapter/messages.py @@ -5,9 +5,11 @@ from __future__ import absolute_import, print_function, unicode_literals import functools +import platform import ptvsd from ptvsd.common import json, log, messaging, singleton +from ptvsd.common.compat import unicode from ptvsd.adapter import channels, debuggee, contract, options, state @@ -170,7 +172,7 @@ class IDEMessages(Messages): # TODO: nodebug debuggee.spawn_and_connect(request) - return self._configure() + return self._configure(request) @_replay_to_server @_only_allowed_while("initializing") @@ -183,12 +185,34 @@ class IDEMessages(Messages): options.port = int(request.arguments.get("port", options.port)) _channels.connect_to_server(address=(options.host, options.port)) - return self._configure() + return self._configure(request) + + def _set_debugger_properties(self, request): + debug_options = set(request("debugOptions", json.array(unicode))) + client_os_type = None + if 'WindowsClient' in debug_options or 'WINDOWS' in debug_options: + client_os_type = 'WINDOWS' + elif 'UnixClient' in debug_options or 'UNIX' in debug_options: + client_os_type = 'UNIX' + else: + client_os_type = 'WINDOWS' if platform.system() == 'Windows' else 'UNIX' + + try: + self._server.request("setDebuggerProperty", arguments={ + "dontTraceStartPatterns": ["\\ptvsd\\", "/ptvsd/"], + "dontTraceEndPatterns": ["ptvsd_launcher.py"], + "skipSuspendOnBreakpointException": ("BaseException",), + "skipPrintBreakpointException": ("NameError",), + "multiThreadsSingleNotification": True, + "ideOS": client_os_type, + }) + except messaging.MessageHandlingError as exc: + exc.propagate(request) # Handles the configuration request sequence for "launch" or "attach", from when # the "initialized" event is sent, to when "configurationDone" is received; see # https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522 - def _configure(self): + def _configure(self, request): log.debug("Replaying previously received messages to server.") assert len(self._initial_messages) @@ -211,6 +235,8 @@ class IDEMessages(Messages): log.debug("Finished replaying messages to server.") self.initial_messages = None + self._set_debugger_properties(request) + # Let the IDE know that it can begin configuring the adapter. state.change("configuring") self._ide.send_event("initialized") diff --git a/src/ptvsd/common/_util.py b/src/ptvsd/common/_util.py deleted file mode 100644 index 6a2a72ca..00000000 --- a/src/ptvsd/common/_util.py +++ /dev/null @@ -1,343 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See LICENSE in the project root -# for license information. - -from __future__ import print_function - -import contextlib -import threading -import time -import sys - -import ptvsd.common.log - - -@contextlib.contextmanager -def ignore_errors(log=None): - """A context manager that masks any raised exceptions.""" - try: - yield - except Exception: - if log is not None: - ptvsd.common.log.exception('Ignoring error', level='info') - - -def call_all(callables, *args, **kwargs): - """Return the result of calling every given object.""" - results = [] - for call in callables: - try: - call(*args, **kwargs) - except Exception as exc: - results.append((call, exc)) - else: - results.append((call, None)) - return results - - -######################## -# threading stuff - -try: - ThreadError = threading.ThreadError -except AttributeError: - ThreadError = RuntimeError - - -try: - base = __builtins__.TimeoutError -except AttributeError: - base = OSError -class TimeoutError(base): # noqa - """Timeout expired.""" - timeout = None - reason = None - - @classmethod - def from_timeout(cls, timeout, reason=None): - """Return a TimeoutError with the given timeout.""" - msg = 'timed out (after {} seconds)'.format(timeout) - if reason is not None: - msg += ' ' + reason - self = cls(msg) - self.timeout = timeout - self.reason = reason - return self -del base # noqa - - -def wait(check, timeout=None, reason=None): - """Wait for the given func to return True. - - If a timeout is given and reached then raise TimeoutError. - """ - if timeout is None or timeout <= 0: - while not check(): - time.sleep(0.01) - else: - if not _wait(check, timeout): - raise TimeoutError.from_timeout(timeout, reason) - - -def is_locked(lock): - """Return True if the lock is locked.""" - if lock is None: - return False - if not lock.acquire(False): - return True - lock_release(lock) - return False - - -def lock_release(lock): - """Ensure that the lock is released.""" - if lock is None: - return - try: - lock.release() - except ThreadError: # already unlocked - pass - - -def lock_wait(lock, timeout=None, reason='waiting for lock'): - """Wait until the lock is not locked.""" - if not _lock_acquire(lock, timeout): - raise TimeoutError.from_timeout(timeout, reason) - lock_release(lock) - - -if sys.version_info >= (3,): - def _lock_acquire(lock, timeout): - if timeout is None: - timeout = -1 - return lock.acquire(timeout=timeout) -else: - def _lock_acquire(lock, timeout): - if timeout is None or timeout <= 0: - return lock.acquire() - - def check(): - return lock.acquire(False) - return _wait(check, timeout) - - -def _wait(check, timeout): - if check(): - return True - for _ in range(int(timeout * 100)): - time.sleep(0.01) - if check(): - return True - else: - return False - - -def new_hidden_thread(name, target, prefix='ptvsd.common.', daemon=True, **kwargs): - """Return a thread that will be ignored by pydevd.""" - if prefix is not None and not name.startswith(prefix): - name = prefix + name - t = threading.Thread( - name=name, - target=target, - **kwargs - ) - t.pydev_do_not_trace = True - t.is_pydev_daemon_thread = True - if daemon: - t.daemon = True - return t - - -######################## -# closing stuff - -class ClosedError(RuntimeError): - """Indicates that the object is closed.""" - - -def close_all(closeables): - """Return the result of closing every given object.""" - results = [] - for obj in closeables: - try: - obj.close() - except Exception as exc: - results.append((obj, exc)) - else: - results.append((obj, None)) - return results - - -class Closeable(object): - """A base class for types that may be closed.""" - - NAME = None - FAIL_ON_ALREADY_CLOSED = True - - def __init__(self): - super(Closeable, self).__init__() - self._closed = False - self._closedlock = threading.Lock() - self._handlers = [] - - def __del__(self): - try: - self.close() - except ClosedError: - pass - - def __enter__(self): - return self - - def __exit__(self, *args): - self.close() - - @property - def closed(self): - return self._closed - - def add_resource_to_close(self, resource, before=False): - """Add a resource to be closed when closing.""" - close = resource.close - if before: - def handle_closing(before): - if not before: - return - close() - else: - def handle_closing(before): - if before: - return - close() - self.add_close_handler(handle_closing) - - def add_close_handler(self, handle_closing, nodupe=True): - """Add a func to be called when closing. - - The func takes one arg: True if it was called before the main - close func and False if after. - """ - with self._closedlock: - if self._closed: - if self.FAIL_ON_ALREADY_CLOSED: - raise ClosedError('already closed') - return - if nodupe and handle_closing in self._handlers: - raise ValueError('close func already added') - - self._handlers.append(handle_closing) - - def check_closed(self): - """Raise ClosedError if closed.""" - if self._closed: - if self.NAME: - raise ClosedError('{} closed'.format(self.NAME)) - else: - raise ClosedError('closed') - - @contextlib.contextmanager - def while_not_closed(self): - """A context manager under which the object will not be closed.""" - with self._closedlock: - self.check_closed() - yield - - def close(self): - """Release any owned resources and clean up.""" - with self._closedlock: - if self._closed: - if self.FAIL_ON_ALREADY_CLOSED: - raise ClosedError('already closed') - return - self._closed = True - handlers = list(self._handlers) - - results = call_all(handlers, True) - self._log_results(results) - self._close() - results = call_all(handlers, False) - self._log_results(results) - - # implemented by subclasses - - def _close(self): - pass - - # internal methods - - def _log_results(self, results, log=None): - if log is None: - return - for obj, exc in results: - if exc is None: - continue - log('failed to close {!r} ({!r})'.format(obj, exc)) - - -######################## -# running stuff - -class NotRunningError(RuntimeError): - """Something isn't currently running.""" - - -class AlreadyStartedError(RuntimeError): - """Something was already started.""" - - -class AlreadyRunningError(AlreadyStartedError): - """Something is already running.""" - - -class Startable(object): - """A base class for types that may be started.""" - - RESTARTABLE = False - FAIL_ON_ALREADY_STOPPED = True - - def __init__(self): - super(Startable, self).__init__() - self._is_running = None - self._startlock = threading.Lock() - self._numstarts = 0 - - def is_running(self, checkclosed=True): - """Return True if currently running.""" - if checkclosed and hasattr(self, 'check_closed'): - self.check_closed() - is_running = self._is_running - if is_running is None: - return False - return is_running() - - def start(self, *args, **kwargs): - """Begin internal execution.""" - with self._startlock: - if self.is_running(): - raise AlreadyRunningError() - if not self.RESTARTABLE and self._numstarts > 0: - raise AlreadyStartedError() - - self._is_running = self._start(*args, **kwargs) - self._numstarts += 1 - - def stop(self, *args, **kwargs): - """Stop execution and wait until done.""" - with self._startlock: - # TODO: Call self.check_closed() here? - if not self.is_running(checkclosed=False): - if not self.FAIL_ON_ALREADY_STOPPED: - return - raise NotRunningError() - self._is_running = None - - self._stop(*args, **kwargs) - - # implemented by subclasses - - def _start(self, *args, **kwargs): - """Return an "is_running()" func after starting.""" - raise NotImplementedError - - def _stop(self): - raise NotImplementedError diff --git a/src/ptvsd/common/log.py b/src/ptvsd/common/log.py index 37bd075f..36a97e3d 100644 --- a/src/ptvsd/common/log.py +++ b/src/ptvsd/common/log.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. See LICENSE in the project root # for license information. -from __future__ import print_function, absolute_import, unicode_literals +from __future__ import absolute_import, print_function, unicode_literals import contextlib import functools diff --git a/src/ptvsd/common/messaging.py b/src/ptvsd/common/messaging.py index 7cf3c29e..7646a542 100644 --- a/src/ptvsd/common/messaging.py +++ b/src/ptvsd/common/messaging.py @@ -19,8 +19,7 @@ import itertools import sys import threading -from ptvsd.common import compat, fmt, json, log -from ptvsd.common._util import new_hidden_thread +from ptvsd.common import compat, fmt, json, log, util class JsonIOStream(object): @@ -774,7 +773,7 @@ class JsonMessageChannel(object): self._stop = threading.Event() self._seq_iter = itertools.count(1) self._requests = {} - self._worker = new_hidden_thread(repr(self), self._process_incoming_messages) + self._worker = util.new_hidden_thread(repr(self), self._process_incoming_messages) self._worker.daemon = True def __repr__(self): @@ -1199,7 +1198,7 @@ class JsonMessageChannel(object): error_message = str(error_message) exc_type = MessageHandlingError if error_message.startswith(InvalidMessageError.PREFIX): - error_message = error_message[len(InvalidMessageError.PREFIX) :] + error_message = error_message[len(InvalidMessageError.PREFIX):] exc_type = InvalidMessageError body = exc_type(error_message, request) diff --git a/src/ptvsd/common/options.py b/src/ptvsd/common/options.py index 13de8fc0..36cdd872 100644 --- a/src/ptvsd/common/options.py +++ b/src/ptvsd/common/options.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. See LICENSE in the project root # for license information. -from __future__ import print_function, with_statement, absolute_import +from __future__ import absolute_import, print_function, unicode_literals import os diff --git a/src/ptvsd/common/socket.py b/src/ptvsd/common/socket.py index 7e6f26b4..2ec39e95 100644 --- a/src/ptvsd/common/socket.py +++ b/src/ptvsd/common/socket.py @@ -2,80 +2,18 @@ # Licensed under the MIT License. See LICENSE in the project root # for license information. -from __future__ import absolute_import +from __future__ import absolute_import, print_function, unicode_literals -from collections import namedtuple -import contextlib -import errno import platform import socket -try: - from urllib.parse import urlparse -except ImportError: - from urlparse import urlparse - - -try: - ConnectionError # noqa - BrokenPipeError # noqa - ConnectionResetError # noqa -except NameError: - class BrokenPipeError(Exception): - # EPIPE and ESHUTDOWN - pass - - class ConnectionResetError(Exception): - # ECONNRESET - pass - - -NOT_CONNECTED = ( - errno.ENOTCONN, - errno.EBADF, -) - -CLOSED = ( - errno.EPIPE, - errno.ESHUTDOWN, - errno.ECONNRESET, - # Windows - 10038, # "An operation was attempted on something that is not a socket" - 10058, -) - -EOF = NOT_CONNECTED + CLOSED - - -@contextlib.contextmanager -def convert_eof(): - """A context manager to convert some socket errors into EOFError.""" - try: - yield - except ConnectionResetError: - raise EOFError - except BrokenPipeError: - raise EOFError - except OSError as exc: - if exc.errno in EOF: - raise EOFError - raise - - -class TimeoutError(socket.timeout): - """A socket timeout happened.""" - - -def is_socket(sock): - """Return True if the object can be used as a socket.""" - return isinstance(sock, socket.socket) def create_server(host, port, timeout=None): """Return a local server socket listening on the given port.""" if host is None: host = 'localhost' - server = _new_sock() try: + server = _new_sock() server.bind((host, port)) if timeout is not None: server.settimeout(timeout) @@ -92,116 +30,17 @@ def create_client(): def _new_sock(): - sock = socket.socket(socket.AF_INET, - socket.SOCK_STREAM, - socket.IPPROTO_TCP) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) if platform.system() == 'Windows': - try: - sock.ioctl(socket.SIO_LOOPBACK_FAST_PATH, True) - except AttributeError: - pass # Not supported in python 2.* or <3.6 - except OSError as ose: - if ose.winerror == 10045: # Not supported by OS - pass - else: - raise sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) else: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) return sock -@contextlib.contextmanager -def ignored_errno(*ignored): - """A context manager that ignores the given errnos.""" - try: - yield - except OSError as exc: - if exc.errno not in ignored: - raise - - -class KeepAlive(namedtuple('KeepAlive', 'interval idle maxfails')): - """TCP keep-alive settings.""" - - INTERVAL = 3 # seconds - IDLE = 1 # seconds after idle - MAX_FAILS = 5 - - @classmethod - def from_raw(cls, raw): - """Return the corresponding KeepAlive.""" - if raw is None: - return None - elif isinstance(raw, cls): - return raw - elif isinstance(raw, (str, int, float)): - return cls(raw) - else: - try: - raw = dict(raw) - except TypeError: - return cls(*raw) - else: - return cls(**raw) - - def __new__(cls, interval=None, idle=None, maxfails=None): - self = super(KeepAlive, cls).__new__( - cls, - float(interval) if interval or interval == 0 else cls.INTERVAL, - float(idle) if idle or idle == 0 else cls.IDLE, - float(maxfails) if maxfails or maxfails == 0 else cls.MAX_FAILS, - ) - return self - - def apply(self, sock): - """Set the keepalive values on the socket.""" - sock.setsockopt(socket.SOL_SOCKET, - socket.SO_KEEPALIVE, - 1) - interval = self.interval - idle = self.idle - maxfails = self.maxfails - try: - if interval > 0: - sock.setsockopt(socket.IPPROTO_TCP, - socket.TCP_KEEPINTVL, - interval) - if idle > 0: - sock.setsockopt(socket.IPPROTO_TCP, - socket.TCP_KEEPIDLE, - idle) - if maxfails >= 0: - sock.setsockopt(socket.IPPROTO_TCP, - socket.TCP_KEEPCNT, - maxfails) - except AttributeError: - # mostly linux-only - pass - - -def connect(sock, addr, keepalive=None): - """Return the client socket for the next connection.""" - if addr is None: - if keepalive is None or keepalive is True: - keepalive = KeepAlive() - elif keepalive: - keepalive = KeepAlive.from_raw(keepalive) - client, _ = sock.accept() - if keepalive: - keepalive.apply(client) - return client - else: - if keepalive: - raise NotImplementedError - sock.connect(addr) - return sock - - -def shut_down(sock, how=socket.SHUT_RDWR, ignored=NOT_CONNECTED): +def shut_down(sock, how=socket.SHUT_RDWR): """Shut down the given socket.""" - with ignored_errno(*ignored or ()): - sock.shutdown(how) + sock.shutdown(how) def close_socket(sock): @@ -211,90 +50,3 @@ def close_socket(sock): except Exception: pass sock.close() - - -class Address(namedtuple('Address', 'host port')): - """An IP address to use for sockets.""" - - @classmethod - def from_raw(cls, raw, defaultport=None): - """Return an address corresponding to the given data.""" - if isinstance(raw, cls): - return raw - elif isinstance(raw, int): - return cls(None, raw) - elif isinstance(raw, str): - if raw == '': - return cls('', defaultport) - parsed = urlparse(raw) - if not parsed.netloc: - if parsed.scheme: - raise ValueError('invalid address {!r}'.format(raw)) - return cls.from_raw('x://' + raw, defaultport=defaultport) - return cls( - parsed.hostname or '', - parsed.port if parsed.port else defaultport, - ) - elif not raw: - return cls(None, defaultport) - else: - try: - kwargs = dict(**raw) - except TypeError: - return cls(*raw) - else: - kwargs.setdefault('host', None) - kwargs.setdefault('port', defaultport) - return cls(**kwargs) - - @classmethod - def as_server(cls, host, port): - """Return an address to use as a server address.""" - return cls(host, port, isserver=True) - - @classmethod - def as_client(cls, host, port): - """Return an address to use as a server address.""" - return cls(host, port, isserver=False) - - def __new__(cls, host, port, **kwargs): - if host == '*': - host = '' - isserver = kwargs.pop('isserver', None) - if isserver is None: - isserver = (host is None or host == '') - else: - isserver = bool(isserver) - if host is None: - host = 'localhost' - self = super(Address, cls).__new__( - cls, - str(host), - int(port) if port is not None else None, - **kwargs - ) - self._isserver = isserver - return self - - def __init__(self, *args, **kwargs): - if self.port is None: - raise TypeError('missing port') - if self.port < 0 or self.port > 65535: - raise ValueError('port must be non-negative int < 65535') - - def __repr__(self): - orig = super(Address, self).__repr__() - return '{}, isserver={})'.format(orig[:-1], self._isserver) - - def __eq__(self, other): - if not super(Address, self).__eq__(other): - return False - try: - other = self.from_raw(other) - except Exception: - return False - return self._isserver == other._isserver - - @property - def isserver(self): - return self._isserver diff --git a/src/ptvsd/common/util.py b/src/ptvsd/common/util.py new file mode 100644 index 00000000..4ba11cc4 --- /dev/null +++ b/src/ptvsd/common/util.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root +# for license information. + +from __future__ import absolute_import, print_function, unicode_literals + +import threading + + +def new_hidden_thread(name, target, prefix='ptvsd.common.', daemon=True, **kwargs): + """Return a thread that will be ignored by pydevd.""" + if prefix is not None and not name.startswith(prefix): + name = prefix + name + t = threading.Thread( + name=name, + target=target, + **kwargs + ) + t.pydev_do_not_trace = True + t.is_pydev_daemon_thread = True + if daemon: + t.daemon = False + return t diff --git a/src/ptvsd/debugger.py b/src/ptvsd/debugger.py index c579e263..a7cfe18b 100644 --- a/src/ptvsd/debugger.py +++ b/src/ptvsd/debugger.py @@ -2,8 +2,9 @@ # Licensed under the MIT License. See LICENSE in the project root # for license information. -import ptvsd.server.log -import ptvsd.server.options +from __future__ import absolute_import, print_function, unicode_literals + +from ptvsd.server import log, options from ptvsd.server.__main__ import run_file, run_module, run_code @@ -16,8 +17,8 @@ DONT_DEBUG = [] # A legacy entrypoint for Visual Studio, to allow older versions to work with new ptvsd.server. # All new code should use the entrypoints in ptvsd.server.__main__ directly. def debug(filename, port_num, debug_id, debug_options, run_as): - ptvsd.server.log.to_file() - ptvsd.server.log.info( + log.to_file() + log.info( "debug{0!r}", (filename, port_num, debug_id, debug_options, run_as) ) @@ -26,10 +27,10 @@ def debug(filename, port_num, debug_id, debug_options, run_as): except KeyError: raise ValueError("run_as must be one of: {0!r}".format(tuple(RUNNERS.keys()))) - ptvsd.server.options.target_kind = "file" if run_as == "script" else run_as - ptvsd.server.options.target = filename - ptvsd.server.options.port = port_num - ptvsd.server.options.client = True + options.target_kind = "file" if run_as == "script" else run_as + options.target = filename + options.port = port_num + options.client = True # debug_id is ignored because it has no meaning in DAP. # debug_options are ignored, because they will be passed later via DAP "launch" request. diff --git a/src/ptvsd/server/__init__.py b/src/ptvsd/server/__init__.py index bfb77683..b57df6c5 100644 --- a/src/ptvsd/server/__init__.py +++ b/src/ptvsd/server/__init__.py @@ -2,11 +2,8 @@ # Licensed under the MIT License. See LICENSE in the project root # for license information. +from __future__ import absolute_import, print_function, unicode_literals + # "force_pydevd" must be imported first to ensure (via side effects) # that the ptvsd-vendored copy of pydevd gets used. -import ptvsd._vendored.force_pydevd - - -# Treat all modules in ptvsd.common as if they were also in this package for now. -import ptvsd.common -__path__ += ptvsd.common.__path__ +import ptvsd._vendored.force_pydevd # noqa diff --git a/src/ptvsd/server/__main__.py b/src/ptvsd/server/__main__.py index 238aa7a6..2beab014 100644 --- a/src/ptvsd/server/__main__.py +++ b/src/ptvsd/server/__main__.py @@ -2,9 +2,8 @@ # Licensed under the MIT License. See LICENSE in the project root # for license information. -from __future__ import absolute_import, print_function, with_statement +from __future__ import absolute_import, print_function, unicode_literals -import numbers import os.path import runpy import site @@ -18,9 +17,8 @@ assert "pydevd" in sys.modules import pydevd from ptvsd.common import log -import ptvsd.server._remote import ptvsd.server.options -import ptvsd.server.runner +import ptvsd.server.attach from ptvsd.server.multiproc import listen_for_subprocesses @@ -191,9 +189,6 @@ def parse(args): return it -daemon = None - - def setup_connection(): ptvsd.common.log.debug('sys.prefix: {0}', (sys.prefix,)) @@ -265,15 +260,10 @@ def setup_connection(): addr = (opts.host, opts.port) - global daemon - if opts.no_debug: - daemon = ptvsd.server.runner.Daemon() - if not daemon.wait_for_launch(addr): - return - elif opts.client: - daemon = ptvsd.server._remote.attach(addr) + if opts.client: + ptvsd.server.attach.attach(addr) else: - daemon = ptvsd.server._remote.enable_attach(addr) + ptvsd.server.attach.enable_attach(addr) if opts.wait: ptvsd.wait_for_attach() @@ -420,14 +410,7 @@ def main(argv=sys.argv): }[ptvsd.server.options.target_kind] run() except SystemExit as ex: - ptvsd.common.log.exception('Debuggee exited via SystemExit', level='debug') - if daemon is not None: - if ex.code is None: - daemon.exitcode = 0 - elif isinstance(ex.code, numbers.Integral): - daemon.exitcode = int(ex.code) - else: - daemon.exitcode = 1 + ptvsd.common.log.exception('Debuggee exited via SystemExit: {0!r}', ex.code, level='debug') raise diff --git a/src/ptvsd/server/_local.py b/src/ptvsd/server/_local.py deleted file mode 100644 index 60ac2f7c..00000000 --- a/src/ptvsd/server/_local.py +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See LICENSE in the project root -# for license information. - -import numbers -import sys -import time - -import pydevd -from _pydevd_bundle.pydevd_comm import get_global_debugger - -from ptvsd.server.pydevd_hooks import install -from ptvsd.server.runner import run as no_debug_runner -from ptvsd.server.socket import Address -from ptvsd.server._util import new_hidden_thread - - -PYDEVD_DEFAULTS = { - '--qt-support=auto', -} - - -def _set_pydevd_defaults(pydevd_args): - args_to_append = [] - for arg in PYDEVD_DEFAULTS: - if arg not in pydevd_args: - args_to_append.append(arg) - return pydevd_args + args_to_append - - -######################## -# high-level functions - -def debug_main(address, name, kind, *extra, **kwargs): - if not kwargs.pop('wait', False) and address.isserver: - def unblock_debugger(): - debugger = get_global_debugger() - while debugger is None: - time.sleep(0.1) - debugger = get_global_debugger() - debugger.ready_to_run = True - new_hidden_thread('ptvsd.server.unblock_debugger', unblock_debugger).start() - if kind == 'module': - run_module(address, name, *extra, **kwargs) - else: - run_file(address, name, *extra, **kwargs) - - -def run_main(address, name, kind, *extra, **kwargs): - addr = Address.from_raw(address) - sys.argv[:] = _run_main_argv(name, extra) - runner = kwargs.pop('_runner', no_debug_runner) - runner(addr, name, kind == 'module', *extra, **kwargs) - - -######################## -# low-level functions - -def run_module(address, modname, *extra, **kwargs): - """Run pydevd for the given module.""" - addr = Address.from_raw(address) - if not addr.isserver: - kwargs['singlesession'] = True - run = kwargs.pop('_run', _run) - prog = kwargs.pop('_prog', sys.argv[0]) - filename = modname + ':' - argv = _run_argv(addr, filename, extra, _prog=prog) - argv.insert(argv.index('--file'), '--module') - run(argv, addr, **kwargs) - - -def run_file(address, filename, *extra, **kwargs): - """Run pydevd for the given Python file.""" - addr = Address.from_raw(address) - if not addr.isserver: - kwargs['singlesession'] = True - run = kwargs.pop('_run', _run) - prog = kwargs.pop('_prog', sys.argv[0]) - argv = _run_argv(addr, filename, extra, _prog=prog) - run(argv, addr, **kwargs) - - -def _run_argv(address, filename, extra, _prog=sys.argv[0]): - """Convert the given values to an argv that pydevd.main() supports.""" - if '--' in extra: - pydevd = list(extra[:extra.index('--')]) - extra = list(extra[len(pydevd) + 1:]) - else: - pydevd = [] - extra = list(extra) - - pydevd = _set_pydevd_defaults(pydevd) - host, port = address - argv = [ - _prog, - '--port', str(port), - ] - if not address.isserver: - argv.extend([ - '--client', host or 'localhost', - ]) - return argv + pydevd + [ - '--file', filename, - ] + extra - - -def _run_main_argv(filename, extra): - if '--' in extra: - pydevd = list(extra[:extra.index('--')]) - extra = list(extra[len(pydevd) + 1:]) - else: - extra = list(extra) - return [filename] + extra - - -def _run(argv, addr, _pydevd=pydevd, _install=install, **kwargs): - """Start pydevd with the given commandline args.""" - - # Pydevd assumes that the "__main__" module is the "pydevd" module - # and does some tricky stuff under that assumption. For example, - # when the debugger starts up it calls save_main_module() - # (in pydevd_bundle/pydevd_utils.py). That function explicitly sets - # sys.modules["pydevd"] to sys.modules["__main__"] and then sets - # the __main__ module to a new one. This makes some sense since - # it gives the debugged script a fresh __main__ module. - # - # This complicates things for us since we are running a different - # file (i.e. this one) as the __main__ module. Consequently, - # sys.modules["pydevd"] gets set to ptvsd/__main__.py. Subsequent - # imports of the "pydevd" module then return the wrong module. We - # work around this by avoiding lazy imports of the "pydevd" module. - # We also replace the __main__ module with the "pydevd" module here. - if sys.modules['__main__'].__file__ != _pydevd.__file__: - sys.modules['__main___orig'] = sys.modules['__main__'] - sys.modules['__main__'] = _pydevd - - daemon = _install(_pydevd, addr, **kwargs) - sys.argv[:] = argv - try: - _pydevd.main() - except SystemExit as ex: - if ex.code is None: - daemon.exitcode = 0 - elif isinstance(ex.code, numbers.Integral): - daemon.exitcode = int(ex.code) - else: - daemon.exitcode = 1 - raise diff --git a/src/ptvsd/server/_remote.py b/src/ptvsd/server/_remote.py deleted file mode 100644 index ae35d3c5..00000000 --- a/src/ptvsd/server/_remote.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See LICENSE in the project root -# for license information. - -import pydevd -import time - -from _pydevd_bundle.pydevd_comm import get_global_debugger - -import ptvsd -import ptvsd.server.log -import ptvsd.server.options -from ptvsd.server._util import new_hidden_thread -from ptvsd.server.pydevd_hooks import install -from ptvsd.server.daemon import session_not_bound, DaemonClosedError - - -global_next_session = lambda: None - - -def enable_attach(address, on_attach=lambda: None, **kwargs): - - host, port = address - - def wait_for_connection(daemon, host, port, next_session=None): - ptvsd.server.log.debug('Waiting for pydevd ...') - debugger = get_global_debugger() - while debugger is None: - time.sleep(0.1) - debugger = get_global_debugger() - - ptvsd.server.log.debug('Unblocking pydevd.') - debugger.ready_to_run = True - - while True: - try: - session_not_bound.wait() - try: - global_next_session() - on_attach() - except DaemonClosedError: - return - except TypeError: - # May happen during interpreter shutdown - # (if some global -- such as global_next_session becomes None). - return - - def start_daemon(): - daemon._sock = daemon._start() - _, next_session = daemon.start_server(addr=(host, port)) - global global_next_session - global_next_session = next_session - return daemon._sock - - daemon = install(pydevd, - address, - start_server=None, - start_client=(lambda daemon, h, port: start_daemon()), - singlesession=False, - **kwargs) - - ptvsd.server.log.debug('Starting connection listener thread') - connection_thread = new_hidden_thread('ptvsd.server.listen_for_connection', - wait_for_connection, - args=(daemon, host, port)) - connection_thread.start() - - if ptvsd.server.options.no_debug: - _setup_nodebug() - else: - ptvsd.server.log.debug('pydevd.settrace()') - pydevd.settrace(host=host, - port=port, - suspend=False, - patch_multiprocessing=ptvsd.server.options.multiprocess) - - return daemon - - -def attach(address, **kwargs): - host, port = address - daemon = install(pydevd, address, singlesession=False, **kwargs) - - if ptvsd.server.options.no_debug: - _setup_nodebug() - else: - ptvsd.server.log.debug('pydevd.settrace()') - pydevd.settrace(host=host, - port=port, - suspend=False, - patch_multiprocessing=ptvsd.server.options.multiprocess) - - return daemon - - -def _setup_nodebug(): - ptvsd.server.log.debug('Running pydevd in nodebug mode.') - debugger = pydevd.PyDB() - debugger.init_matplotlib_support = lambda *arg: None - # We are invoking run() solely for side effects here - setting up the - # debugger and connecting to our socket - so the code run is a no-op. - debugger.run( - file='ptvsd.server._remote:_nop', - globals=None, - locals=None, - is_module=True, - set_trace=False) - - -def _nop(): - pass diff --git a/src/ptvsd/server/attach_server.py b/src/ptvsd/server/attach.py similarity index 53% rename from src/ptvsd/server/attach_server.py rename to src/ptvsd/server/attach.py index bf51f76c..f45b3324 100644 --- a/src/ptvsd/server/attach_server.py +++ b/src/ptvsd/server/attach.py @@ -2,29 +2,16 @@ # Licensed under the MIT License. See LICENSE in the project root # for license information. -import sys -import warnings +from __future__ import absolute_import, print_function, unicode_literals -import ptvsd.server.log -from ptvsd.server._remote import ( - attach as ptvsd_attach, - enable_attach as ptvsd_enable_attach, -) +import sys import pydevd + +from ptvsd.common import log, options as common_opts +from ptvsd.server import multiproc, options as server_opts from _pydevd_bundle.pydevd_constants import get_global_debugger from pydevd_file_utils import get_abs_path_real_path_and_base_from_frame -WAIT_TIMEOUT = 1.0 - -DEFAULT_HOST = '0.0.0.0' -DEFAULT_PORT = 5678 - -_pending_threads = set() - -_redirect_output_deprecation_msg = ( - "'redirect_output' setting via enable_attach will be deprecated in the future versions of the debugger. " - "This can be set using redirectOutput in Launch config in VS Code, using Tee output option in Visual Studio, " - "or debugOptions configuration for any client.") def wait_for_attach(timeout=None): """If a remote debugger is attached, returns immediately. Otherwise, @@ -36,17 +23,19 @@ def wait_for_attach(timeout=None): timeout : float, optional The timeout for the operation in seconds (or fractions thereof). """ - ptvsd.server.log.info('wait_for_attach{0!r}', (timeout,)) + log.info('wait_for_attach()') dbg = get_global_debugger() - if bool(dbg): - dbg.dap_debugger_attached.wait(timeout) - else: + if not bool(dbg): msg = 'wait_for_attach() called before enable_attach().' - ptvsd.server.log.info(msg) + log.info(msg) raise AssertionError(msg) + pydevd._wait_for_attach() -def enable_attach(address=(DEFAULT_HOST, DEFAULT_PORT), redirect_output=None, log_dir=None): + +def enable_attach( + address=(server_opts.host, server_opts.port), + log_dir=None): """Enables a client to attach to this process remotely to debug Python code. Parameters @@ -58,9 +47,6 @@ def enable_attach(address=(DEFAULT_HOST, DEFAULT_PORT), redirect_output=None, lo ``(hostname, port)``. On client side, the server is identified by the Qualifier string in the usual ``'hostname:port'`` format, e.g.: ``'myhost.cloudapp.net:5678'``. Default is ``('0.0.0.0', 5678)``. - redirect_output : bool, optional - (Deprecated) Specifies whether any output (on both `stdout` and `stderr`) produced - by this program should be sent to the debugger. Default is ``True``. log_dir : str, optional Name of the directory that debugger will create its log files in. If not specified, logging is disabled. @@ -79,30 +65,27 @@ def enable_attach(address=(DEFAULT_HOST, DEFAULT_PORT), redirect_output=None, lo """ if log_dir: - ptvsd.common.options.log_dir = log_dir - ptvsd.server.log.to_file() - ptvsd.server.log.info('enable_attach{0!r}', (address,)) - - if redirect_output is not None: - ptvsd.server.log.info('redirect_output deprecation warning.') - warnings.warn(_redirect_output_deprecation_msg, DeprecationWarning, stacklevel=2) + common_opts.log_dir = log_dir + log.to_file() + log.info('enable_attach{0!r}', (address,)) if is_attached(): - ptvsd.server.log.info('enable_attach() ignored - already attached.') - return - - dbg = get_global_debugger() - if bool(dbg): - dbg.dap_debugger_attached.clear() + log.info('enable_attach() ignored - already attached.') + return None, None # Ensure port is int - port = address[1] - address = (address[0], port if type(port) is int else int(port)) + host, port = address + address = (host, int(port)) - ptvsd_enable_attach(address) + server_opts.host, server_opts.port = pydevd._enable_attach(address) + + if server_opts.subprocess_notify: + multiproc.notify_root(server_opts.port) + + return (server_opts.host, server_opts.port) -def attach(address, redirect_output=None, log_dir=None): +def attach(address, log_dir=None): """Attaches this process to the debugger listening on a given address. Parameters @@ -112,42 +95,36 @@ def attach(address, redirect_output=None, log_dir=None): for TCP connections. It is in the same format as used for regular sockets of the `socket.AF_INET` family, i.e. a tuple of ``(hostname, port)``. - redirect_output : bool, optional - (Deprecated) Specifies whether any output (on both `stdout` and `stderr`) produced - by this program should be sent to the debugger. Default is ``True``. log_dir : str, optional Name of the directory that debugger will create its log files in. If not specified, logging is disabled. """ if log_dir: - ptvsd.common.options.log_dir = log_dir - ptvsd.server.log.to_file() - ptvsd.server.log.info('attach{0!r}', (address, redirect_output)) - - if redirect_output is not None: - ptvsd.server.log.info('redirect_output deprecation warning.') - warnings.warn(_redirect_output_deprecation_msg, DeprecationWarning) + common_opts.log_dir = log_dir + log.to_file() + log.info('attach{0!r}', (address,)) if is_attached(): - ptvsd.server.log.info('attach() ignored - already attached.') - return - - dbg = get_global_debugger() - if bool(dbg): - dbg.dap_debugger_attached.clear() + log.info('attach() ignored - already attached.') + return None, None # Ensure port is int - port = address[1] - address = (address[0], port if type(port) is int else int(port)) + host, port = address + address = (host, int(port)) + server_opts.host, server_opts.port = address - ptvsd_attach(address) + log.debug('pydevd.settrace()') + pydevd.settrace( + host=host, + port=port, + suspend=False, + patch_multiprocessing=server_opts.multiprocess) def is_attached(): """Returns ``True`` if debugger is attached, ``False`` otherwise.""" - dbg = get_global_debugger() - return bool(dbg) and dbg.dap_debugger_attached.is_set() + return pydevd._is_attached() def break_into_debugger(): @@ -155,10 +132,10 @@ def break_into_debugger(): and breaks into the debugger with current thread as active. """ - ptvsd.server.log.info('break_into_debugger()') + log.info('break_into_debugger()') if not is_attached(): - ptvsd.server.log.info('break_into_debugger() ignored - debugger not attached') + log.info('break_into_debugger() ignored - debugger not attached') return # Get the first frame in the stack that's not an internal frame. @@ -168,17 +145,6 @@ def break_into_debugger(): get_abs_path_real_path_and_base_from_frame(stop_at_frame)) == global_debugger.PYDEV_FILE: stop_at_frame = stop_at_frame.f_back - - # pydevd.settrace() only enables debugging of the current - # thread and all future threads. PyDevd is not enabled for - # existing threads (other than the current one). Consequently, - # pydevd.settrace() must be called ASAP in the current thread. - # See issue #509. - # - # This is tricky, however, because settrace() will block until - # it receives a CMD_RUN message. You can't just call it in a - # thread to avoid blocking; doing so would prevent the current - # thread from being debugged. pydevd.settrace( suspend=True, trace_only_current_thread=True, diff --git a/src/ptvsd/server/daemon.py b/src/ptvsd/server/daemon.py deleted file mode 100644 index cc43d70c..00000000 --- a/src/ptvsd/server/daemon.py +++ /dev/null @@ -1,560 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See LICENSE in the project root -# for license information. - -import contextlib -import sys -import threading - -import ptvsd.server.log -from ptvsd.server import wrapper, options, multiproc -from ptvsd.server.socket import ( - close_socket, create_server, create_client, connect, Address) -from .exit_handlers import ( - ExitHandlers, UnsupportedSignalError, - kill_current_proc) -from .session import PyDevdDebugSession -from ._util import ( - ClosedError, NotRunningError, ignore_errors, lock_wait) - - -session_not_bound = threading.Event() -session_not_bound.set() - - -def _wait_for_user(): - if sys.__stdout__ is not None: - try: - import msvcrt - except ImportError: - sys.__stdout__.write('Press Enter to continue . . . ') - sys.__stdout__.flush() - sys.__stdin__.read(1) - else: - sys.__stdout__.write('Press any key to continue . . . ') - sys.__stdout__.flush() - msvcrt.getch() - - -class DaemonError(RuntimeError): - """Indicates that a Daemon had a problem.""" - MSG = 'error' - - def __init__(self, msg=None): - if msg is None: - msg = self.MSG - super(DaemonError, self).__init__(msg) - - -class DaemonClosedError(DaemonError): - """Indicates that a Daemon was unexpectedly closed.""" - MSG = 'closed' - - -class DaemonStoppedError(DaemonError): - """Indicates that a Daemon was unexpectedly stopped.""" - MSG = 'stopped' - - -# TODO: Inherit from Closeable. -# TODO: Inherit from Startable? - -class DaemonBase(object): - """The base class for DAP daemons.""" - - SESSION = None - - exitcode = None - - def __init__(self, wait_for_user=_wait_for_user, - addhandlers=True, killonclose=True, - singlesession=False): - - self._lock = threading.Lock() - self._started = False - self._stopped = False - self._closed = False - - # socket-related - - self._sock = None # set when started - self._server = None - - # session-related - - self._singlesession = singlesession - - self._session = None - self._numsessions = 0 - self._sessionlock = None - - # proc-related - - self._wait_for_user = wait_for_user - self._killonclose = killonclose - - self._exiting_via_atexit_handler = False - - self._exithandlers = ExitHandlers() - if addhandlers: - self._install_exit_handlers() - - @property - def session(self): - """The current session.""" - return self._session - - @contextlib.contextmanager - def started(self): - """A context manager that starts the daemon and stops it for errors.""" - self.start() - try: - yield self - except Exception: - self._stop_quietly() - raise - - @contextlib.contextmanager - def running(self): - """A context manager that starts the daemon. - - If there's a failure then the daemon is stopped. It is also - stopped at the end of the with block. - """ - self.start() - try: - yield self - finally: - self._stop_quietly() - - def is_running(self): - """Return True if the daemon is running.""" - with self._lock: - if self._closed: - return False - if self._sock is None: - return False - return self._started and not self._stopped - - def start(self): - """Return the "socket" to use for pydevd after setting it up.""" - with self._lock: - if self._closed: - raise DaemonClosedError() - if self._started: - raise RuntimeError('already started') - self._started = True - - sock = self._start() - self._sock = sock - return sock - - def start_server(self, addr, hidebadsessions=True): - """Return ("socket", next_session) with a new server socket.""" - - ptvsd.server.log.debug('Starting server daemon on {0!r}.', addr) - - addr = Address.from_raw(addr) - with self.started(): - assert self._sessionlock is None - assert self.session is None - self._server = create_server(addr.host, addr.port) - host, port = self._server.getsockname() - ptvsd.server.log.debug('Server socket created on {0!r}', addr) - self._sessionlock = threading.Lock() - sock = self._sock - - def check_ready(**kwargs): - self._check_ready_for_session(**kwargs) - if self._server is None: - raise DaemonStoppedError() - - def next_session(timeout=None, **kwargs): - server = self._server - sessionlock = self._sessionlock - check_ready(checksession=False) - - ptvsd.server.log.debug('Getting next session...') - sessionlock.acquire() # Released in _finish_session(). - ptvsd.server.log.debug('Session lock acquired.') - # It may have closed or stopped while we waited. - check_ready() - - timeout = kwargs.pop('timeout', None) - try: - ptvsd.server.log.debug('Getting session socket...') - client = connect(server, None, **kwargs) - self._bind_session(client) - ptvsd.server.log.debug('Starting session...') - self._start_session_safely('ptvsd.server.Server', timeout=timeout) - ptvsd.server.log.debug('Session started.') - return self._session - except Exception: - ptvsd.server.log.exception(level=('debug' if hidebadsessions else 'error')) - with ignore_errors(): - self._finish_session() - if hidebadsessions: - ptvsd.server.log.debug('Hiding bad session') - return None - self._stop_quietly() - raise - - if options.subprocess_notify: - multiproc.notify_root(port) - - return sock, next_session - - def start_client(self, addr): - """Return ("socket", start_session) with a new client socket.""" - - ptvsd.server.log.debug('Starting client daemon on {0!r}.', addr) - - addr = Address.from_raw(addr) - self._singlesession = True - with self.started(): - assert self.session is None - client = create_client() - connect(client, addr) - sock = self._sock - - def start_session(**kwargs): - self._check_ready_for_session() - if self._server is not None: - raise RuntimeError('running as server') - if self._numsessions: - raise RuntimeError('session stopped') - - try: - self._bind_session(client) - self._start_session_safely('ptvsd.server.Client', **kwargs) - return self._session - except Exception: - self._stop_quietly() - raise - - return sock, start_session - - def start_session(self, session, threadname, **kwargs): - """Start the debug session and remember it. - - If "session" is a client socket then a session is created - from it. - """ - - ptvsd.server.log.debug('Starting session.') - - self._check_ready_for_session() - if self._server is not None: - raise RuntimeError('running as server') - - self._bind_session(session) - self._start_session_safely(threadname, **kwargs) - return self.session - - def close(self): - """Stop all loops and release all resources.""" - - ptvsd.server.log.debug('Stopping daemon.') - - with self._lock: - if self._closed: - raise DaemonClosedError('already closed') - self._closed = True - - self._close() - - # internal methods - - def _check_ready_for_session(self, checksession=True): - with self._lock: - if self._closed: - raise DaemonClosedError() - if not self._started: - raise DaemonStoppedError('never started') - if self._stopped or self._sock is None: - raise DaemonStoppedError() - if checksession and self.session is not None: - raise RuntimeError('session already started') - - def _close(self): - self._stop() - - self._sock = None - - def _stop(self): - with self._lock: - if self._stopped: - return - self._stopped = True - - server = self._server - self._server = None - - with ignore_errors(): - self._finish_session() - - self._sessionlock = None # TODO: Call self._clear_sessionlock? - - # TODO: Close the server socket *before* finish the session? - if server is not None: - with ignore_errors(): - close_socket(server) - - # TODO: Close self._sock *before* finishing the session? - if self._sock is not None: - with ignore_errors(): - close_socket(self._sock) - - def _stop_quietly(self): - with ignore_errors(): - self._stop() - - def _handle_session_disconnecting(self, session): - ptvsd.server.log.debug('Handling disconnecting session') - if self._singlesession: - if self._killonclose: - with self._lock: - if not self._exiting_via_atexit_handler: - # Ensure the proc is exiting before closing - # socket. Note that we kill the proc instead - # of calling sys.exit(0). - # Note that this will trigger either the atexit - # handler or the signal handler. - kill_current_proc() - else: - try: - self.close() - except DaemonClosedError: - pass - - def _handle_session_closing(self, session): - ptvsd.server.log.debug('Handling closing session') - - if self._singlesession: - if self._killonclose: - with self._lock: - if not self._exiting_via_atexit_handler: - # Ensure the proc is exiting before closing - # socket. Note that we kill the proc instead - # of calling sys.exit(0). - # Note that this will trigger either the atexit - # handler or the signal handler. - kill_current_proc() - else: - try: - self.close() - except DaemonClosedError: - pass - else: - self._finish_session() - - def _clear_sessionlock(self, done=False): - sessionlock = self._sessionlock - if done: - self._sessionlock = None - if sessionlock is not None: - try: - sessionlock.release() - except Exception: # TODO: Make it more specific? - ptvsd.server.log.exception('Session lock not released', level='debug') - else: - ptvsd.server.log.debug('Session lock released') - - # internal session-related methods - - def _bind_session(self, session): - session_not_bound.clear() - # TODO: Pass notify_* to session.start() instead. - session = self.SESSION.from_raw( - session, - notify_closing=self._handle_session_closing, - notify_disconnecting=self._handle_session_disconnecting, - ownsock=True, - **self._session_kwargs() or {} - ) - self._session = session - self._numsessions += 1 - - def _start_session_safely(self, threadname, **kwargs): - try: - self._start_session(threadname, **kwargs) - except Exception: - ptvsd.server.log.exception() - with ignore_errors(): - self._finish_session() - raise - - def _finish_session(self): - self._numsessions -= 1 - session_not_bound.set() - try: - session = self._release_session() - ptvsd.server.log.debug('Session stopped') - finally: - self._clear_sessionlock() - - if self._singlesession: - ptvsd.server.log.debug('Closing daemon after single session') - try: - self.close() - except DaemonClosedError: - pass - return session - - def _release_session(self): - session = self.session - if not self._singlesession: - # TODO: This shouldn't happen if we are exiting? - self._session = None - - try: - session.stop() - except NotRunningError: - pass - try: - session.close() - except ClosedError: - pass - - return session - - # internal proc-related methods - - def _install_exit_handlers(self): - """Set the placeholder handlers.""" - self._exithandlers.install() - - try: - self._exithandlers.add_atexit_handler(self._handle_atexit) - except ValueError: - pass - for signum in self._exithandlers.SIGNALS: - try: - self._exithandlers.add_signal_handler(signum, - self._handle_signal) - except ValueError: - # Already added. - pass - except UnsupportedSignalError: - # TODO: This shouldn't happen. - pass - - def _handle_atexit(self): - ptvsd.server.log.debug('Handling atexit') - with self._lock: - self._exiting_via_atexit_handler = True - session = self.session - - if session is not None: - lock = threading.Lock() - lock.acquire() - - def wait_debugger(timeout=None): - lock_wait(lock, timeout) - - def wait_exiting(cfg): - if cfg: - self._wait_for_user() - lock.release() - # TODO: Rely on self._stop_debugger(). - session.handle_debugger_stopped(wait_debugger) - session.handle_exiting(self.exitcode, wait_exiting) - - try: - self.close() - except DaemonClosedError: - pass - if session is not None: - session.wait_until_stopped() - - def _handle_signal(self, signum, frame): - ptvsd.server.log.debug('Handling signal') - try: - self.close() - except DaemonClosedError: - pass - if not self._exiting_via_atexit_handler: - sys.exit(0) - - # methods for subclasses to override - - def _start(self): - """Return the debugger client socket after starting the daemon.""" - raise NotImplementedError - - def _start_session(self, threadname, **kwargs): - self.session.start( - threadname, - **kwargs - ) - - def _session_kwargs(self): - return None - - -class Daemon(DaemonBase): - """The process-level manager for the VSC protocol debug adapter.""" - - SESSION = PyDevdDebugSession - - def __init__(self, wait_for_user=_wait_for_user, - notify_session_debugger_ready=None, - **kwargs): - super(Daemon, self).__init__(wait_for_user, **kwargs) - - self._notify_session_debugger_ready = notify_session_debugger_ready - - @property - def pydevd(self): - return self._sock - - # internal methods - - def _start(self): - return wrapper.PydevdSocket( - self._handle_pydevd_message, - self._handle_pydevd_close, - self._getpeername, - self._getsockname, - ) - - def _start_session(self, threadname, **kwargs): - super(Daemon, self)._start_session( - threadname, - pydevd_notify=self.pydevd.pydevd_notify, - pydevd_request=self.pydevd.pydevd_request, - **kwargs - ) - - def _session_kwargs(self): - def debugger_ready(session): - if self._notify_session_debugger_ready is not None: - self._notify_session_debugger_ready(session) - - return dict( - notify_debugger_ready=debugger_ready, - ) - - # internal methods for PyDevdSocket(). - - def _handle_pydevd_message(self, cmdid, seq, text): - if self.session is None or self.session.closed: - # TODO: Do more than ignore? - return - self.session.handle_pydevd_message(cmdid, seq, text) - - def _handle_pydevd_close(self): - try: - self.close() - except DaemonClosedError: - pass - - def _getpeername(self): - if self.session is None or self.session.closed: - raise NotImplementedError - return self.session.socket.getpeername() - - def _getsockname(self): - if self.session is None or self.session.closed: - raise NotImplementedError - return self.session.socket.getsockname() diff --git a/src/ptvsd/server/exit_handlers.py b/src/ptvsd/server/exit_handlers.py deleted file mode 100644 index 3ca96318..00000000 --- a/src/ptvsd/server/exit_handlers.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See LICENSE in the project root -# for license information. - -import atexit -import os -import platform -import signal - - -class AlreadyInstalledError(RuntimeError): - """Exit handlers were already installed.""" - - -class UnsupportedSignalError(RuntimeError): - """A signal is not supported.""" - - -def kill_current_proc(signum=signal.SIGTERM): - """Kill the current process. - - Note that this directly kills the process (with SIGTERM, by default) - rather than using sys.exit(). - """ - os.kill(os.getpid(), signum) - - -class ExitHandlers(object): - """Manages signal and atexit handlers.""" - - if platform.system() == 'Windows' or not hasattr(signal, 'SIGHUP'): - # TODO: Windows *does* support these signals: - # SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, SIGBREAK - SIGNALS = [] - else: - SIGNALS = [ - signal.SIGHUP, - ] - - def __init__(self): - self._signal_handlers = {sig: [] - for sig in self.SIGNALS} - self._atexit_handlers = [] - self._installed = False - - @property - def supported_signals(self): - return set(self.SIGNALS) - - @property - def installed(self): - return self._installed - - def install(self): - """Set the parent handlers. - - This must be called in the main thread. - """ - if self._installed: - raise AlreadyInstalledError('exit handlers already installed') - self._installed = True - self._install_signal_handler() - self._install_atexit_handler() - - # TODO: Add uninstall()? - - def add_atexit_handler(self, handle_atexit, nodupe=True): - """Add an atexit handler to the list managed here.""" - if nodupe and handle_atexit in self._atexit_handlers: - raise ValueError('atexit handler alraedy added') - self._atexit_handlers.append(handle_atexit) - - def add_signal_handler(self, signum, handle_signal, nodupe=True, - ignoreunsupported=False): - """Add a signal handler to the list managed here.""" - # TODO: The initialization of self.SIGNALS should make this - # special-casing unnecessary. - if platform.system() == 'Windows': - return - - try: - handlers = self._signal_handlers[signum] - except KeyError: - if ignoreunsupported: - return - raise UnsupportedSignalError(signum) - if nodupe and handle_signal in handlers: - raise ValueError('signal handler alraedy added') - handlers.append(handle_signal) - - # internal methods - - def _install_signal_handler(self): - # TODO: The initialization of self.SIGNALS should make this - # special-casing unnecessary. - if platform.system() == 'Windows': - return - - orig = {} - try: - for sig in self._signal_handlers: - # TODO: Skip or fail if signal.getsignal() returns None? - orig[sig] = signal.signal(sig, self._signal_handler) - except ValueError: - # Wasn't called in main thread! - raise - - def _signal_handler(self, signum, frame): - for handle_signal in self._signal_handlers.get(signum, ()): - handle_signal(signum, frame) - - def _install_atexit_handler(self): - self._atexit_handlers = [] - atexit.register(self._atexit_handler) - - def _atexit_handler(self): - for handle_atexit in self._atexit_handlers: - handle_atexit() diff --git a/src/ptvsd/server/futures.py b/src/ptvsd/server/futures.py deleted file mode 100644 index 2176aff5..00000000 --- a/src/ptvsd/server/futures.py +++ /dev/null @@ -1,202 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See LICENSE in the project root -# for license information. - -from __future__ import print_function, with_statement, absolute_import - -import sys -import threading -import traceback - -import ptvsd.server.log -from ptvsd.server.reraise import reraise - - -class Future(object): - # TODO: docstring - - def __init__(self, loop): - self._lock = threading.Lock() - self._loop = loop - self._done = None - self._observed = False - self._done_callbacks = [] - self._exc_info = None - self._handling = ptvsd.server.log.current_handler() - - # It's expensive, so only capture the origin if logging is enabled. - if ptvsd.server.log.file: - self._origin = traceback.extract_stack() - else: - self._origin = None - - def __del__(self): - # Do not run any checks if Python is shutting down. - if not sys or not self._lock: - return - - with self._lock: - assert self._done or self._done is None - exc_info = self._exc_info - if exc_info and not self._observed: - msg = 'Unobserved failed future' - origin = self._origin - if origin: - origin = '\n'.join(traceback.format_list(origin)) - msg += ' originating from:\n\n{origin}\n\n' - ptvsd.server.log.exception(msg, origin=origin, exc_info=exc_info) - traceback.print_exception(*exc_info, file=sys.__stderr__) - - def result(self): - # TODO: docstring - with self._lock: - self._observed = True - if self._exc_info: - reraise(self._exc_info) - return self._result - - def exc_info(self): - # TODO: docstring - with self._lock: - self._observed = True - return self._exc_info - - def set_result(self, result): - # TODO: docstring - with self._lock: - self._result = result - self._exc_info = None - self._done = True - callbacks = list(self._done_callbacks) - - def invoke_callbacks(): - for cb in callbacks: - cb(self) - - self._loop.call_soon(invoke_callbacks) - - def set_exc_info(self, exc_info): - # TODO: docstring - with self._lock: - self._exc_info = exc_info - self._done = True - callbacks = list(self._done_callbacks) - - def invoke_callbacks(): - for cb in callbacks: - cb(self) - - self._loop.call_soon(invoke_callbacks) - - def add_done_callback(self, callback): - # TODO: docstring - with self._lock: - done = self._done - self._done_callbacks.append(callback) - if done: - self._loop.call_soon(lambda: callback(self)) - - def remove_done_callback(self, callback): - # TODO: docstring - with self._lock: - self._done_callbacks.remove(callback) - - -class EventLoop(object): - # TODO: docstring - - def __init__(self): - self._queue = [] - self._lock = threading.Lock() - self._event = threading.Event() - self._event.set() - - self._stop = False - - def create_future(self): - return Future(self) - - def run_forever(self): - try: - while not self._stop: - if not self._event.wait(timeout=0.1): - continue - with self._lock: - queue = self._queue - self._queue = [] - self._event.clear() - for (f, args) in queue: - f(*args) - except: - if sys is None or traceback is None: - # Errors during shutdown are expected as this is a daemon thread, - # so just silence it. We can't even log it at this point. - pass - else: - # Try to log it, but guard against the possibility of shutdown - # in the middle of it. - try: - ptvsd.server.log.exception('Exception escaped to event loop') - except: - pass - - def stop(self): - self._stop = True - - def call_soon(self, f, *args): - with self._lock: - self._queue.append((f, args)) - self._event.set() - - def call_soon_threadsafe(self, f, *args): - return self.call_soon(f, *args) - - -class Result(object): - # TODO: docstring - - __slots__ = ['value'] - - def __init__(self, value): - self.value = value - - -def wrap_async(f): - def g(self, loop, *args, **kwargs): - it = f(self, *args, **kwargs) - result = Future(loop) - if it is None: - result.set_result(None) - return result - - async_handler = ptvsd.server.log.current_handler() - - def resume(fut): - try: - if fut is None: - x = next(it) - else: - exc_info = fut.exc_info() - if exc_info: - x = it.throw(*exc_info) - else: - x = it.send(fut.result()) - except AssertionError: - raise - except StopIteration: - result.set_result(None) - except: - result.set_exc_info(sys.exc_info()) - else: - if isinstance(x, Result): - result.set_result(x.value) - else: - def callback(fut): - with ptvsd.server.log.handling(async_handler): - resume(fut) - x.add_done_callback(callback) - - resume(None) - return result - - return g diff --git a/src/ptvsd/server/ipcjson.py b/src/ptvsd/server/ipcjson.py deleted file mode 100644 index 7a0a39aa..00000000 --- a/src/ptvsd/server/ipcjson.py +++ /dev/null @@ -1,340 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See LICENSE in the project root -# for license information. - -from __future__ import print_function, with_statement, absolute_import - -import errno -import itertools -import json -import os -import os.path -from socket import create_connection -import sys -import time -import threading -import traceback - -import ptvsd.server.log -from ptvsd.server.socket import TimeoutError, convert_eof - - -SKIP_TB_PREFIXES = [ - os.path.normcase( - os.path.dirname( - os.path.abspath(__file__))), -] - -TIMEOUT = os.environ.get('PTVSD_SOCKET_TIMEOUT') -if TIMEOUT: - TIMEOUT = float(TIMEOUT) - - -if sys.version_info[0] >= 3: - from encodings import ascii - - def to_bytes(cmd_str): - return ascii.Codec.encode(cmd_str)[0] -else: - def to_bytes(cmd_str): - return cmd_str - - class BrokenPipeError(Exception): - pass - - -def _str_or_call(m): - # TODO: Use callable() here. - try: - call = m.__call__ - except AttributeError: - return str(m) - else: - return str(call()) - - -class InvalidHeaderError(Exception): - # TODO: docstring - pass - - -class InvalidContentError(Exception): - # TODO: docstring - pass - - -class SocketIO(object): - # TODO: docstring - - def __init__(self, *args, **kwargs): - port = kwargs.pop('port', None) - socket = kwargs.pop('socket', None) - own_socket = kwargs.pop('own_socket', True) - if socket is None: - if port is None: - raise ValueError( - "A 'port' or a 'socket' must be passed to SocketIO initializer as a keyword argument.") # noqa - addr = ('127.0.0.1', port) - socket = create_connection(addr) - own_socket = True - super(SocketIO, self).__init__(*args, **kwargs) - - self.__buffer = to_bytes('') - self.__port = port - self.__socket = socket - self.__own_socket = own_socket - - def _send(self, **payload): - ptvsd.server.log.debug('IDE <-- {0!j}', payload) - content = json.dumps(payload).encode('utf-8') - headers = ('Content-Length: {}\r\n\r\n'.format(len(content)) - ).encode('ascii') - - try: - self.__socket.send(headers) - self.__socket.send(content) - except BrokenPipeError: - pass - except OSError as exc: - if exc.errno in (errno.EPIPE, errno.ESHUTDOWN): # BrokenPipeError - pass - elif exc.errno not in (errno.ENOTCONN, errno.EBADF): - raise - - def _buffered_read_line_as_ascii(self): - """Return the next line from the buffer as a string. - - Reads bytes until it encounters newline chars, and returns the bytes - ascii decoded, newline chars are excluded from the return value. - Blocks until: newline chars are read OR socket is closed. - """ - newline = '\r\n'.encode('ascii') - while newline not in self.__buffer: - temp = self.__socket.recv(1024) - if not temp: - break - self.__buffer += temp - - if not self.__buffer: - return None - - try: - index = self.__buffer.index(newline) - except ValueError: - raise InvalidHeaderError('Header line not terminated') - - line = self.__buffer[:index] - self.__buffer = self.__buffer[index+len(newline):] - return line.decode('ascii', 'replace') - - def _buffered_read_as_utf8(self, length): - # TODO: docstring - while len(self.__buffer) < length: - temp = self.__socket.recv(1024) - if not temp: - break - self.__buffer += temp - - if len(self.__buffer) < length: - raise InvalidContentError( - 'Expected to read {} bytes of content, but only read {} bytes.'.format(length, len(self.__buffer))) # noqa - - content = self.__buffer[:length] - self.__buffer = self.__buffer[length:] - return content.decode('utf-8', 'replace') - - def _wait_for_message(self): - # TODO: docstring - # base protocol defined at: - # https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#base-protocol - - # read all headers, ascii encoded separated by '\r\n' - # end of headers is indicated by an empty line - headers = {} - line = self._buffered_read_line_as_ascii() - while line: - parts = line.split(':') - if len(parts) == 2: - headers[parts[0]] = parts[1] - else: - raise InvalidHeaderError( - "Malformed header, expected 'name: value'\n" + line) - line = self._buffered_read_line_as_ascii() - - # end of stream - if not line and not headers: - return - - # validate headers - try: - length_text = headers['Content-Length'] - try: - length = int(length_text) - except ValueError: - raise InvalidHeaderError( - 'Invalid Content-Length: ' + length_text) - except NameError: - raise InvalidHeaderError('Content-Length not specified in headers') - except KeyError: - raise InvalidHeaderError('Content-Length not specified in headers') - - if length < 0 or length > 2147483647: - raise InvalidHeaderError('Invalid Content-Length: ' + length_text) - - # read content, utf-8 encoded - content = self._buffered_read_as_utf8(length) - try: - msg = json.loads(content) - ptvsd.server.log.debug('IDE --> {0!j}', msg) - self._receive_message(msg) - except ValueError: - ptvsd.server.log.exception('IDE --> {0}', content) - raise InvalidContentError('Error deserializing message content.') - except json.decoder.JSONDecodeError: - ptvsd.server.log.exception('IDE --> {0}', content) - raise InvalidContentError('Error deserializing message content.') - - def _close(self): - # TODO: docstring - if self.__own_socket: - self.__socket.close() - - -class IpcChannel(object): - # TODO: docstring - - def __init__(self, *args, **kwargs): - timeout = kwargs.pop('timeout', None) - if timeout is None: - timeout = TIMEOUT - super(IpcChannel, self).__init__(*args, **kwargs) - # This class is meant to be last in the list of base classes - # Don't call super because object's __init__ doesn't take arguments - - self.__seq = itertools.count() - self.__exit = False - self.__lock = threading.Lock() - self.__message = [] - self._timeout = timeout - self._fail_after = None - - def close(self): - # TODO: docstring - self._close() - - def send_event(self, _name, **kwargs): - # TODO: docstring - with self.__lock: - self._send( - type='event', - seq=next(self.__seq), - event=_name, - body=kwargs, - ) - - def send_response(self, request, success=True, message=None, **kwargs): - # TODO: docstring - with self.__lock: - self._send( - type='response', - seq=next(self.__seq), - request_seq=int(request.get('seq', 0)), - success=success, - command=request.get('command', ''), - message=message or '', - body=kwargs, - ) - - def set_exit(self): - # TODO: docstring - self.__exit = True - - def process_messages(self): - # TODO: docstring - if self._timeout is not None: - self._fail_after = time.time() + self._timeout - while not self.__exit: - try: - self.process_one_message() - except (AssertionError, EOFError): - raise - except Exception: - ptvsd.server.log.exception(level=('debug' if self.__exit else 'error')) - if not self.__exit: - raise - - def process_one_message(self): - # TODO: docstring - try: - msg = self.__message.pop(0) - except IndexError: - with convert_eof(): - self._wait_for_message() - try: - msg = self.__message.pop(0) - except IndexError: - # No messages received. - if self._fail_after is not None: - if time.time() < self._fail_after: - raise TimeoutError('connection closed?') - raise EOFError('no more messages') - if self._fail_after is not None: - self._fail_after = time.time() + self._timeout - - what = msg.copy() - what.pop('arguments', None) - what.pop('body', None) - - with ptvsd.server.log.handling(what): - try: - if msg['type'] == 'request': - self.on_request(msg) - elif msg['type'] == 'response': - self.on_response(msg) - elif msg['type'] == 'event': - self.on_event(msg) - else: - self.on_invalid_request(msg, {}) - except AssertionError: - ptvsd.server.log.exception() - raise - except Exception: - ptvsd.server.log.exception() - - def on_request(self, request): - # TODO: docstring - assert request.get('type', '') == 'request', \ - "Only handle 'request' messages in on_request" - - cmd = request.get('command', '') - args = request.get('arguments', {}) - target = getattr(self, 'on_' + cmd, self.on_invalid_request) - - try: - target(request, args) - except AssertionError: - raise - except Exception: - self.send_response( - request, - success=False, - message=traceback.format_exc(), - ) - - def on_response(self, msg): - # TODO: docstring - # this class is only used for server side only for now - raise NotImplementedError - - def on_event(self, msg): - # TODO: docstring - # this class is only used for server side only for now - raise NotImplementedError - - def on_invalid_request(self, request, args): - # TODO: docstring - self.send_response(request, success=False, message='Unknown command') - - def _receive_message(self, message): - with self.__lock: - self.__message.append(message) diff --git a/src/ptvsd/server/multiproc.py b/src/ptvsd/server/multiproc.py index 7ff9edbe..4756b297 100644 --- a/src/ptvsd/server/multiproc.py +++ b/src/ptvsd/server/multiproc.py @@ -2,14 +2,13 @@ # Licensed under the MIT License. See LICENSE in the project root # for license information. -from __future__ import print_function, with_statement, absolute_import +from __future__ import absolute_import, print_function, unicode_literals import atexit import itertools import os import re import signal -import socket import sys import threading import time @@ -19,12 +18,8 @@ try: except ImportError: import Queue as queue -import ptvsd.server.log -from ptvsd.server import options -from ptvsd.server.socket import create_server, create_client -from ptvsd.server.messaging import JsonIOStream, JsonMessageChannel -from ptvsd.server._util import new_hidden_thread - +from ptvsd.common import log, messaging, options as common_opts, socket, util +from ptvsd.server import options as server_opts from _pydev_bundle import pydev_monkey from _pydevd_bundle.pydevd_comm import get_global_debugger @@ -73,24 +68,24 @@ def listen_for_subprocesses(): global subprocess_listener_socket assert subprocess_listener_socket is None - subprocess_listener_socket = create_server('localhost', 0) - ptvsd.server.log.debug( + subprocess_listener_socket = socket.create_server('localhost', 0) + log.debug( 'Listening for subprocess notifications on port {0}.', subprocess_listener_port()) atexit.register(stop_listening_for_subprocesses) atexit.register(kill_subprocesses) - new_hidden_thread('SubprocessListener', _subprocess_listener).start() + util.new_hidden_thread('SubprocessListener', _subprocess_listener).start() def stop_listening_for_subprocesses(): - ptvsd.server.log.debug('Stopping listening for subprocess notifications.') + log.debug('Stopping listening for subprocess notifications.') global subprocess_listener_socket if subprocess_listener_socket is None: return try: - subprocess_listener_socket.shutdown(socket.SHUT_RDWR) + socket.shut_down(subprocess_listener_socket) except Exception: pass subprocess_listener_socket = None @@ -100,16 +95,16 @@ def kill_subprocesses(): with subprocess_lock: pids = list(subprocesses.keys()) - ptvsd.server.log.debug('Killing remaining subprocesses: PID={0}', pids) + log.debug('Killing remaining subprocesses: PID={0}', pids) for pid in pids: - ptvsd.server.log.debug('Killing subprocess with PID={0}.', pid) + log.debug('Killing subprocess with PID={0}.', pid) with subprocess_lock: subprocesses.pop(pid, None) try: os.kill(pid, signal.SIGTERM) except Exception: - ptvsd.server.log.exception('Failed to kill process with PID={0}.', pid, level='debug') + log.exception('Failed to kill process with PID={0}.', pid, level='debug') def subprocess_listener_port(): @@ -129,9 +124,9 @@ def _subprocess_listener(): n = next(counter) name = 'subprocess-{}'.format(n) - ptvsd.server.log.debug('Accepted incoming connection from {0}', name) + log.debug('Accepted incoming connection from {0}', name) - stream = JsonIOStream.from_socket(sock, name=name) + stream = messaging.JsonIOStream.from_socket(sock, name=name) _handle_subprocess(n, stream) @@ -153,7 +148,7 @@ def _handle_subprocess(n, stream): with subprocess_lock: subprocesses[self._pid] = channel - ptvsd.server.log.debug( + log.debug( 'Subprocess {0} (PID={1}) registered, notifying IDE.', stream.name, self._pid) @@ -164,24 +159,24 @@ def _handle_subprocess(n, stream): return response def disconnect(self): - ptvsd.server.log.debug('Subprocess {0} disconnected, presumed to have terminated.', self._pid) + log.debug('Subprocess {0} disconnected, presumed to have terminated.', self._pid) if self._pid is not None: with subprocess_lock: subprocesses.pop(self._pid, None) name = 'subprocess-%d' % n - channel = JsonMessageChannel(stream, Handlers(), name) + channel = messaging.JsonMessageChannel(stream, Handlers(), name) channel.start() def notify_root(port): - assert options.subprocess_of + assert server_opts.subprocess_of - ptvsd.server.log.debug('Subprocess (PID={0}) notifying root process at port {1}', os.getpid(), options.subprocess_notify) - conn = create_client() - conn.connect(('localhost', options.subprocess_notify)) - stream = JsonIOStream.from_socket(conn, 'root-process') - channel = JsonMessageChannel(stream) + log.debug('Subprocess (PID={0}) notifying root process at port {1}', os.getpid(), server_opts.subprocess_notify) + conn = socket.create_client() + conn.connect(('localhost', server_opts.subprocess_notify)) + stream = messaging.JsonIOStream.from_socket(conn, 'root-process') + channel = messaging.JsonMessageChannel(stream) channel.start() # Send the notification about ourselves to root, and wait for it to tell us @@ -192,7 +187,7 @@ def notify_root(port): # in that case, just exit immediately. request = channel.send_request('ptvsd_subprocess', { - 'parentProcessId': options.subprocess_of, + 'parentProcessId': server_opts.subprocess_of, 'processId': os.getpid(), 'port': port, }) @@ -200,7 +195,7 @@ def notify_root(port): try: response = request.wait_for_response() except Exception: - ptvsd.server.log.exception('Failed to send subprocess notification; exiting') + log.exception('Failed to send subprocess notification; exiting') sys.exit(0) # Keep the channel open until we exit - root process uses open channels to keep @@ -208,12 +203,17 @@ def notify_root(port): atexit.register(lambda: channel.close()) if not response['incomingConnection']: - ptvsd.server.log.debug('No IDE connection is expected for this subprocess; unpausing.') - debugger = get_global_debugger() - while debugger is None: - time.sleep(0.1) - debugger = get_global_debugger() - debugger.ready_to_run = True + log.debug('No IDE connection is expected for this subprocess; unpausing.') + + # TODO: The code here exists to cancel any wait for attach in an indirect way. We need a cleaner + # way to cancel wait_for_attach. Ideally, a cancellable wait_for_attach, which ensures that it + # does not mess up the pydevd internal debugger states. + + # debugger = get_global_debugger() + # while debugger is None: + # time.sleep(0.1) + # debugger = get_global_debugger() + # debugger.ready_to_run = True def patch_args(args): @@ -228,11 +228,11 @@ def patch_args(args): python -R -Q warn .../ptvsd/__main__.py --host localhost --port 0 ... -m app """ - if not options.multiprocess: + if not server_opts.multiprocess: return args args = list(args) - ptvsd.server.log.debug('Patching subprocess command line: {0!r}', args) + log.debug('Patching subprocess command line: {0!r}', args) # First, let's find the target of the invocation. This is one of: # @@ -300,18 +300,18 @@ def patch_args(args): from ptvsd import __main__ ptvsd_args = [ __main__.__file__, - '--host', options.host, + '--host', server_opts.host, '--port', '0', '--wait', '--multiprocess', '--subprocess-of', str(os.getpid()), - '--subprocess-notify', str(options.subprocess_notify or subprocess_listener_port()), + '--subprocess-notify', str(server_opts.subprocess_notify or subprocess_listener_port()), ] - if ptvsd.common.options.log_dir: - ptvsd_args += ['--log-dir', ptvsd.common.options.log_dir] + if common_opts.log_dir: + ptvsd_args += ['--log-dir', common_opts.log_dir] args[i:i] = ptvsd_args - ptvsd.server.log.debug('Patched subprocess command line: {0!r}', args) + log.debug('Patched subprocess command line: {0!r}', args) return args diff --git a/src/ptvsd/server/options.py b/src/ptvsd/server/options.py index a88d6ddc..fc589a20 100644 --- a/src/ptvsd/server/options.py +++ b/src/ptvsd/server/options.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. See LICENSE in the project root # for license information. -from __future__ import print_function, with_statement, absolute_import +from __future__ import absolute_import, print_function, unicode_literals """ptvsd command-line options that need to be globally available. @@ -28,7 +28,7 @@ If target_kind is 'code', then target is the code to run. If target_kind is 'pid', then target is the process ID to attach to. """ -host = 'localhost' +host = '127.0.0.1' """Name or IP address of the network interface used by ptvsd.server. If runing in server mode, this is the interface on which it listens for incoming connections. If running in client mode, this is the interface to which it connects. diff --git a/src/ptvsd/server/pydevd_hooks.py b/src/ptvsd/server/pydevd_hooks.py deleted file mode 100644 index 18e913cb..00000000 --- a/src/ptvsd/server/pydevd_hooks.py +++ /dev/null @@ -1,156 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See LICENSE in the project root -# for license information. - -import os -import sys - -import pydevd -from _pydev_bundle import pydev_monkey -from _pydevd_bundle import pydevd_comm - -import ptvsd -import ptvsd.server.log -from ptvsd.server import multiproc -from ptvsd.server.socket import Address -from ptvsd.server.daemon import Daemon, DaemonStoppedError, DaemonClosedError -from ptvsd.server._util import new_hidden_thread -from ptvsd.server import options - - -@ptvsd.server.log.escaped_exceptions -def start_server(daemon, host, port, **kwargs): - """Return a socket to a (new) local pydevd-handling daemon. - - The daemon supports the pydevd client wire protocol, sending - requests and handling responses (and events). - - This is a replacement for _pydevd_bundle.pydevd_comm.start_server. - """ - sock, next_session = daemon.start_server((host, port)) - - def handle_next(): - try: - ptvsd.server.log.debug('Waiting for session...') - session = next_session(**kwargs) - ptvsd.server.log.debug('Got session') - return session - except (DaemonClosedError, DaemonStoppedError): - # Typically won't happen. - ptvsd.server.log.exception('Daemon stopped while waiting for session', level='debug') - raise - except Exception: - ptvsd.server.log.exception() - return None - - def serve_forever(): - ptvsd.server.log.debug('Waiting for initial connection...') - handle_next() - while True: - ptvsd.server.log.debug('Waiting for next connection...') - try: - handle_next() - except (DaemonClosedError, DaemonStoppedError): - break - ptvsd.server.log.debug('Done serving') - - t = new_hidden_thread( - target=serve_forever, - name='sessions', - ) - t.start() - return sock - - -@ptvsd.server.log.escaped_exceptions -def start_client(daemon, host, port, **kwargs): - """Return a socket to an existing "remote" pydevd-handling daemon. - - The daemon supports the pydevd client wire protocol, sending - requests and handling responses (and events). - - This is a replacement for _pydevd_bundle.pydevd_comm.start_client. - """ - sock, start_session = daemon.start_client((host, port)) - start_session(**kwargs) - return sock - - -# See pydevd/_vendored/pydevd/_pydev_bundle/pydev_monkey.py -@ptvsd.server.log.escaped_exceptions -def get_python_c_args(host, port, indC, args, setup): - runner = ''' -import sys -sys.path.append(r'{ptvsd_syspath}') -from ptvsd.server import multiproc -multiproc.init_subprocess( - {initial_pid}, - {initial_request}, - {parent_pid}, - {parent_port}, - {first_port}, - {last_port}, - {pydevd_setup}) -{rest} -''' - - first_port, last_port = multiproc.subprocess_port_range - - # __file__ will be .../ptvsd/__init__.py, and we want the ... - ptvsd_syspath = os.path.join(ptvsd.server.__file__, '../..') - - return runner.format( - initial_pid=multiproc.initial_pid, - initial_request=multiproc.initial_request, - parent_pid=os.getpid(), - parent_port=multiproc.listener_port, - first_port=first_port, - last_port=last_port, - ptvsd_syspath=ptvsd_syspath, - pydevd_setup=setup, - rest=args[indC + 1]) - - -def install(pydevd_module, address, - start_server=start_server, start_client=start_client, - **kwargs): - """Configure pydevd to use our wrapper. - - This is a bit of a hack to allow us to run our VSC debug adapter - in the same process as pydevd. Note that, as with most hacks, - this is somewhat fragile (since the monkeypatching sites may - change). - """ - - ptvsd.server.log.debug('Installing pydevd hooks.') - - addr = Address.from_raw(address) - daemon = Daemon(**kwargs) - - _start_server = (lambda p: start_server(daemon, addr.host, p)) - _start_server.orig = start_server - _start_client = (lambda h, p: start_client(daemon, h, p)) - _start_client.orig = start_client - - # These are the functions pydevd invokes to get a socket to the client. - pydevd_comm.start_server = _start_server - pydevd_comm.start_client = _start_client - - # This is invoked when a child process is spawned with multiproc debugging enabled. - pydev_monkey.patch_args = multiproc.patch_and_quote_args - if not options.multiprocess and not options.no_debug: - # This means '--multiprocess' flag was not passed via command line args. Patch the - # new process functions here to handle multiprocess being enabled via debug options. - ptvsd.server.log.debug('Monkey-patching multiprocess functions.') - pydev_monkey.patch_new_process_functions() - - # Ensure that pydevd is using our functions. - pydevd_module.start_server = _start_server - pydevd_module.start_client = _start_client - __main__ = sys.modules['__main__'] - if __main__ is not pydevd: - if getattr(__main__, '__file__', None) == pydevd.__file__: - __main__.start_server = _start_server - __main__.start_client = _start_client - - return daemon diff --git a/src/ptvsd/server/reraise.py b/src/ptvsd/server/reraise.py deleted file mode 100644 index fd8661cc..00000000 --- a/src/ptvsd/server/reraise.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See LICENSE in the project root -# for license information. - -# TODO: only absolute_import needed? -from __future__ import print_function, with_statement, absolute_import - -import sys - -if sys.version_info >= (3,): - from ptvsd.server.reraise3 import reraise # noqa: F401 -else: - from ptvsd.server.reraise2 import reraise # noqa: F401 diff --git a/src/ptvsd/server/reraise2.py b/src/ptvsd/server/reraise2.py deleted file mode 100644 index effb62a9..00000000 --- a/src/ptvsd/server/reraise2.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See LICENSE in the project root -# for license information. - - -def reraise(exc_info): - # TODO: docstring - raise exc_info[0], exc_info[1], exc_info[2] # noqa diff --git a/src/ptvsd/server/reraise3.py b/src/ptvsd/server/reraise3.py deleted file mode 100644 index dcb54153..00000000 --- a/src/ptvsd/server/reraise3.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See LICENSE in the project root -# for license information. - - -def reraise(exc_info): - # TODO: docstring - raise exc_info[1].with_traceback(exc_info[2]) diff --git a/src/ptvsd/server/runner.py b/src/ptvsd/server/runner.py deleted file mode 100644 index 60139a20..00000000 --- a/src/ptvsd/server/runner.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See LICENSE in the project root -# for license information. - -import pydevd -import threading - -from ptvsd.server.daemon import DaemonBase -from ptvsd.server.session import DebugSession -from ptvsd.server.wrapper import VSCLifecycleMsgProcessor -from pydevd import init_stdout_redirect, init_stderr_redirect - - -HOSTNAME = 'localhost' - - -def run(address, filename, is_module, *args, **kwargs): - # TODO: docstring - # TODO: client/server -> address - daemon = Daemon() - if not daemon.wait_for_launch(address): - return - - debugger = pydevd.PyDB() - # We do not want some internal methods to get executed in non-debug mode. - debugger.init_matplotlib_support = lambda *arg: None - debugger.run( - file=filename, - globals=None, - locals=None, - is_module=is_module, - set_trace=False) - - -class Daemon(DaemonBase): - """The process-level manager for the VSC protocol debug adapter.""" - - LAUNCH_TIMEOUT = 10000 # seconds - - class SESSION(DebugSession): - class MESSAGE_PROCESSOR(VSCLifecycleMsgProcessor): - - def on_setBreakpoints(self, request, args): - # Note: breakpoints is required (vscode will terminate - # the debugger if that's not the case). - # See: https://github.com/microsoft/ptvsd/issues/1408 - self.send_response( - request, - success=True, - breakpoints=( - [{'verified': False}] * len(args.get('breakpoints', ())) - ) - ) - - def on_invalid_request(self, request, args): - self.send_response(request, success=True) - - def wait_for_launch(self, addr, timeout=LAUNCH_TIMEOUT): - # TODO: docstring - launched = threading.Event() - _, start_session = self.start_client(addr) - start_session( - notify_launch=launched.set, - ) - return launched.wait(timeout) - - def _start(self): - import weakref - weak_self = weakref.ref(self) # Avoid cyclic ref - - def on_stdout(msg): - self = weak_self() - if self is not None: - self._send_output('stdout', msg) - - def on_stderr(msg): - self = weak_self() - if self is not None: - self._send_output('stderr', msg) - - init_stdout_redirect(on_stdout) - init_stderr_redirect(on_stderr) - return NoSocket() - - def _close(self): - super(Daemon, self)._close() - - def _send_output(self, category, output): - if self.session is None: - return - self.session._msgprocessor.send_event('output', - category=category, - output=output) - - -class NoSocket(object): - """A object with a noop socket lifecycle.""" - - def shutdown(self, *args, **kwargs): - pass - - def close(self): - pass diff --git a/src/ptvsd/server/session.py b/src/ptvsd/server/session.py deleted file mode 100644 index 57abc555..00000000 --- a/src/ptvsd/server/session.py +++ /dev/null @@ -1,204 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See LICENSE in the project root -# for license information. - -import ptvsd.server.log -from .socket import is_socket, close_socket -from .wrapper import VSCodeMessageProcessor -from ._util import TimeoutError, ClosedError, Closeable, Startable - - -class DebugSession(Startable, Closeable): - """A single DAP session for a network client socket.""" - - MESSAGE_PROCESSOR = None - - NAME = 'debug session' - FAIL_ON_ALREADY_CLOSED = False - FAIL_ON_ALREADY_STOPPED = False - - @classmethod - def from_raw(cls, raw, **kwargs): - """Return a session for the given data.""" - if isinstance(raw, cls): - return raw - if not is_socket(raw): - # TODO: Create a new client socket from a remote address? - # addr = Address.from_raw(raw) - raise NotImplementedError - client = raw - return cls(client, **kwargs) - - @classmethod - def from_server_socket(cls, server, **kwargs): - """Return a session for the next connection to the given socket.""" - client, _ = server.accept() - return cls(client, ownsock=True, **kwargs) - - def __init__(self, sock, - notify_closing=None, - notify_disconnecting=None, - ownsock=False): - super(DebugSession, self).__init__() - - if notify_closing is not None: - - def handle_closing(before): - if before: - notify_closing(self) - - self.add_close_handler(handle_closing) - - if notify_disconnecting is None: - notify_disconnecting = (lambda _: None) - self._notify_disconnecting = notify_disconnecting - - self._sock = sock - self._pre_socket_close = None - if ownsock: - - # Close the socket *after* calling sys.exit() (via notify_closing). - def handle_closing(before): - if before: - return - ptvsd.server.log.debug('Closing session socket') - proc = self._msgprocessor - if self._pre_socket_close is not None: - self._pre_socket_close() - if proc is not None: - try: - proc.wait_while_connected(10) # seconds - except TimeoutError: - ptvsd.server.log.exception('timed out waiting for disconnect', level='debug') - close_socket(self._sock) - - self.add_close_handler(handle_closing) - - self._msgprocessor = None - - @property - def socket(self): - return self._sock - - @property - def msgprocessor(self): - return self._msgprocessor - - def handle_debugger_stopped(self, wait=None): - """Deal with the debugger exiting.""" - proc = self._msgprocessor - if proc is None: - return - proc.handle_debugger_stopped(wait) - - def handle_exiting(self, exitcode=None, wait=None): - """Deal with the debuggee exiting.""" - proc = self._msgprocessor - if proc is None: - return - proc.handle_exiting(exitcode, wait) - - def wait_until_stopped(self): - """Block until all resources (e.g. message processor) have stopped.""" - proc = self._msgprocessor - if proc is None: - return - # TODO: Do this in VSCodeMessageProcessor.close()? - proc._wait_for_server_thread() - - # internal methods - - def _new_msg_processor(self, **kwargs): - return self.MESSAGE_PROCESSOR( - self._sock, - notify_disconnecting=self._handle_vsc_disconnect, - notify_closing=self._handle_vsc_close, - **kwargs - ) - - def _start(self, threadname, **kwargs): - """Start the message handling for the session.""" - self._msgprocessor = self._new_msg_processor(**kwargs) - self.add_resource_to_close(self._msgprocessor) - self._msgprocessor.start(threadname) - return self._msgprocessor_running - - def _stop(self): - proc = self._msgprocessor - if proc is None: - return - - ptvsd.server.log.debug('Message processor is stopping.') - # TODO: We should not need to wait if not exiting. - # The editor will send a "disconnect" request at this point. - proc._wait_for_disconnect() - proc.close() - self._msgprocessor = None - - def _close(self): - ptvsd.server.log.debug('Session is closing.') - - def _msgprocessor_running(self): - if self._msgprocessor is None: - return False - # TODO: Return self._msgprocessor.is_running(). - return True - - # internal methods for VSCodeMessageProcessor - - def _handle_vsc_disconnect(self, pre_socket_close=None): - ptvsd.server.log.debug('Disconnecting.') - self._pre_socket_close = pre_socket_close # TODO: Fail if already set? - self._notify_disconnecting(self) - - def _handle_vsc_close(self): - ptvsd.server.log.debug('Message processor is closing.') - try: - self.close() - except ClosedError: - pass - - -class PyDevdDebugSession(DebugSession): - """A single DAP session for a network client socket.""" - - MESSAGE_PROCESSOR = VSCodeMessageProcessor - - def __init__(self, sock, - notify_debugger_ready=None, - **kwargs): - super(PyDevdDebugSession, self).__init__(sock, **kwargs) - - def notify_debugger_ready(session, _notify=notify_debugger_ready): - if self._notified_debugger_ready: - return - self._notified_debugger_ready = True - if _notify is not None: - _notify(session) - - self._notified_debugger_ready = False - self._notify_debugger_ready = notify_debugger_ready - - def handle_pydevd_message(self, cmdid, seq, text): - if self._msgprocessor is None: - # TODO: Do more than ignore? - return - try: - return self._msgprocessor.on_pydevd_event(cmdid, seq, text) - except: - ptvsd.server.log.exception('Error handling pydevd message: {0}', text) - raise - - # internal methods - - def _new_msg_processor(self, **kwargs): - return super(PyDevdDebugSession, self)._new_msg_processor( - notify_debugger_ready=self._handle_vsc_debugger_ready, - **kwargs - ) - - # internal methods for VSCodeMessageProcessor - - def _handle_vsc_debugger_ready(self): - ptvsd.server.log.debug('Ready to debug') - self._notify_debugger_ready(self) diff --git a/src/ptvsd/server/wrapper.py b/src/ptvsd/server/wrapper.py deleted file mode 100644 index 72b81fe4..00000000 --- a/src/ptvsd/server/wrapper.py +++ /dev/null @@ -1,1528 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See LICENSE in the project root -# for license information. - -from __future__ import print_function, absolute_import, unicode_literals - -import copy -import errno -import json -import os -import re -import platform -import socket -import sys -import threading - -try: - import urllib - urllib.unquote -except Exception: - import urllib.parse as urllib - -try: - import queue -except ImportError: - import Queue as queue - -import warnings -import pydevd # noqa -import _pydevd_bundle.pydevd_comm as pydevd_comm # noqa -import _pydevd_bundle.pydevd_comm_constants as pydevd_comm_constants # noqa -import _pydevd_bundle.pydevd_extension_api as pydevd_extapi # noqa -import _pydevd_bundle.pydevd_extension_utils as pydevd_extutil # noqa -import _pydevd_bundle.pydevd_frame as pydevd_frame # noqa -import pydevd_file_utils -from _pydevd_bundle.pydevd_dont_trace_files import PYDEV_FILE # noqa - -import ptvsd -import ptvsd.server.log -from ptvsd.server import _util -from ptvsd.server import multiproc -from ptvsd.server import options -from ptvsd.server.compat import unicode -import ptvsd.server.ipcjson as ipcjson # noqa -import ptvsd.server.futures as futures # noqa -from ptvsd import __version__ # noqa -from ptvsd.server.socket import TimeoutError # noqa - -WAIT_FOR_THREAD_FINISH_TIMEOUT = 1 # seconds - -def NOOP(*args, **kwargs): - pass - - -def path_to_unicode(s): - return s if isinstance(s, unicode) else s.decode(sys.getfilesystemencoding()) - - -PTVSD_DIR_PATH = pydevd_file_utils.normcase(os.path.dirname(ptvsd.__file__) + os.path.sep) - - -class UnsupportedPyDevdCommandError(Exception): - - def __init__(self, cmdid): - msg = 'unsupported pydevd command ' + str(cmdid) - super(UnsupportedPyDevdCommandError, self).__init__(msg) - self.cmdid = cmdid - - -if sys.version_info >= (3,): - - def unquote(s): - return None if s is None else urllib.unquote(s) - -else: - - # In Python 2, urllib.unquote doesn't handle Unicode strings correctly, - # so we need to convert to ASCII first, unquote, and then decode. - def unquote(s): - if s is None: - return None - if not isinstance(s, bytes): - s = bytes(s) - s = urllib.unquote(s) - if isinstance(s, bytes): - s = s.decode('utf-8') - return s - - -class IDMap(object): - """Maps VSCode entities to corresponding pydevd entities by ID. - - VSCode entity IDs are generated here when necessary. - - For VSCode, entity IDs are always integers, and uniquely identify - the entity among all other entities of the same type - e.g. all - frames across all threads have unique IDs. - - For pydevd, IDs can be integer or strings, and are usually specific - to some scope - for example, a frame ID is only unique within a - given thread. To produce a truly unique ID, the IDs of all the outer - scopes have to be combined into a tuple. Thus, for example, a pydevd - frame ID is (thread_id, frame_id). - - Variables (evaluation results) technically don't have IDs in pydevd, - as it doesn't have evaluation persistence. However, for a given - frame, any child can be identified by the path one needs to walk - from the root of the frame to get to that child - and that path, - represented as a sequence of its constituent components, is used by - pydevd commands to identify the variable. So we use the tuple - representation of the same as its pydevd ID. For example, for - something like foo[1].bar, its ID is: - (thread_id, frame_id, 'FRAME', 'foo', 1, 'bar') - - For pydevd breakpoints, the ID has to be specified by the caller - when creating, so we can just reuse the ID that was generated for - VSC. However, when referencing the pydevd breakpoint later (e.g. to - remove it), its ID must be specified together with path to file in - which that breakpoint is set - i.e. pydevd treats those IDs as - scoped to a file. So, even though breakpoint IDs are unique across - files, use (path, bp_id) as pydevd ID. - """ - - def __init__(self): - self._vscode_to_pydevd = {} - self._pydevd_to_vscode = {} - self._next_id = 1 - self._lock = threading.Lock() - - def pairs(self): - # TODO: docstring - with self._lock: - return list(self._pydevd_to_vscode.items()) - - def add(self, pydevd_id): - # TODO: docstring - with self._lock: - vscode_id = self._next_id - if callable(pydevd_id): - pydevd_id = pydevd_id(vscode_id) - self._next_id += 1 - self._vscode_to_pydevd[vscode_id] = pydevd_id - self._pydevd_to_vscode[pydevd_id] = vscode_id - return vscode_id - - def remove(self, pydevd_id=None, vscode_id=None): - # TODO: docstring - with self._lock: - if pydevd_id is None: - pydevd_id = self._vscode_to_pydevd[vscode_id] - elif vscode_id is None: - vscode_id = self._pydevd_to_vscode[pydevd_id] - del self._vscode_to_pydevd[vscode_id] - del self._pydevd_to_vscode[pydevd_id] - - def to_pydevd(self, vscode_id): - # TODO: docstring - return self._vscode_to_pydevd[vscode_id] - - def to_vscode(self, pydevd_id, autogen): - # TODO: docstring - try: - return self._pydevd_to_vscode[pydevd_id] - except KeyError: - if autogen: - return self.add(pydevd_id) - else: - raise - - def pydevd_ids(self): - # TODO: docstring - with self._lock: - ids = list(self._pydevd_to_vscode.keys()) - return ids - - def vscode_ids(self): - # TODO: docstring - with self._lock: - ids = list(self._vscode_to_pydevd.keys()) - return ids - - -class PydevdSocket(object): - """A dummy socket-like object for communicating with pydevd. - - It parses pydevd messages and redirects them to the provided handler - callback. It also provides an interface to send notifications and - requests to pydevd; for requests, the reply can be asynchronously - awaited. - """ - - def __init__(self, handle_msg, handle_close, getpeername, getsockname): - self._handle_msg = handle_msg - self._handle_close = handle_close - self._getpeername = getpeername - self._getsockname = getsockname - - self.lock = threading.Lock() - self.seq = 1000000000 - self.pipe_r, self.pipe_w = os.pipe() - self.requests = {} - - self._closed = False - self._closing = False - - def close(self): - """Mark the socket as closed and release any resources.""" - if self._closing: - return - - with self.lock: - if self._closed: - return - self._closing = True - - if self.pipe_w is not None: - pipe_w = self.pipe_w - self.pipe_w = None - try: - os.close(pipe_w) - except OSError as exc: - if exc.errno != errno.EBADF: - raise - if self.pipe_r is not None: - pipe_r = self.pipe_r - self.pipe_r = None - try: - os.close(pipe_r) - except OSError as exc: - if exc.errno != errno.EBADF: - raise - self._handle_close() - self._closed = True - self._closing = False - - def shutdown(self, mode): - """Called when pydevd has stopped.""" - # noop - - def getpeername(self): - """Return the remote address to which the socket is connected.""" - return self._getpeername() - - def getsockname(self): - """Return the socket's own address.""" - return self._getsockname() - - def recv(self, count): - """Return the requested number of bytes. - - This is where the "socket" sends requests to pydevd. The data - must follow the pydevd line protocol. - """ - pipe_r = self.pipe_r - if pipe_r is None: - return b'' - data = os.read(pipe_r, count) - return data - - def recv_into(self, buf): - pipe_r = self.pipe_r - if pipe_r is None: - return 0 - return os.readv(pipe_r, [buf]) - - # In Python 2, we must unquote before we decode, because UTF-8 codepoints - # are encoded first and then quoted as individual bytes. In Python 3, - # however, we just get a properly UTF-8-encoded string. - if sys.version_info < (3,): - - @staticmethod - def _decode_and_unquote(data): - return unquote(data).decode('utf8') - - else: - - @staticmethod - def _decode_and_unquote(data): - return unquote(data.decode('utf8')) - - def send(self, data): - """Handle the given bytes. - - This is where pydevd sends responses and events. The data will - follow the pydevd line protocol. - - Note that the data is always a full message received from pydevd - (sent from _pydevd_bundle.pydevd_comm.WriterThread), so, there's - no need to actually treat received bytes as a stream of bytes. - """ - result = len(data) - - # Defer logging until we have as much information about the message - # as possible - after decoding it, parsing it, determining whether - # it's a response etc. This way it can be logged in the most readable - # representation and with the most context. - # - # However, if something fails at any of those stages, we want to log - # as much as we have by then. Thus, the format string for for trace() - # is also constructed dynamically, reflecting the available info. - - trace_prefix = 'PYD --> ' - trace_fmt = '{data}' - - try: - if data.startswith(b'{'): # JSON - data = data.decode('utf-8') - data = json.loads(data) - trace_fmt = '{data!j}' - cmd_id = data['pydevd_cmd_id'] - if 'request_seq' in data: - seq = data['request_seq'] - else: - seq = data['seq'] - args = data - else: - assert data.endswith(b'\n') - data = self._decode_and_unquote(data[:-1]) - cmd_id, seq, args = data.split('\t', 2) - if ptvsd.server.log.file: - trace_fmt = '{cmd_name} {seq}\n{args}' - except: - ptvsd.server.log.exception(trace_prefix + trace_fmt, data=data) - raise - - seq = int(seq) - cmd_id = int(cmd_id) - try: - cmd_name = pydevd_comm.ID_TO_MEANING[str(cmd_id)] - except KeyError: - cmd_name = cmd_id - - with self.lock: - loop, fut, requesting_handler = self.requests.pop(seq, (None, None, None)) - - if requesting_handler is not None: - trace_prefix = '(requested while handling {requesting_handler})\n' + trace_prefix - ptvsd.server.log.debug( - trace_prefix + trace_fmt, - data=data, - cmd_id=cmd_id, - cmd_name=cmd_name, - seq=seq, - args=args, - requesting_handler=requesting_handler, - ) - - if fut is None: - with ptvsd.server.log.handling((cmd_name, seq)): - self._handle_msg(cmd_id, seq, args) - else: - loop.call_soon_threadsafe(fut.set_result, (cmd_id, seq, args)) - - return result - - sendall = send - - def makefile(self, *args, **kwargs): - """Return a file-like wrapper around the socket.""" - return os.fdopen(self.pipe_r) - - def make_packet(self, cmd_id, args): - assert not isinstance(args, bytes) - with self.lock: - seq = self.seq - self.seq += 1 - - if ptvsd.server.log.file: - try: - cmd_name = pydevd_comm.ID_TO_MEANING[str(cmd_id)] - except KeyError: - cmd_name = cmd_id - ptvsd.server.log.debug('PYD <-- {0} {1} {2}', cmd_name, seq, args) - - s = '{}\t{}\t{}\n'.format(cmd_id, seq, args) - return seq, s - - def make_json_packet(self, cmd_id, args): - assert isinstance(args, dict) - with self.lock: - seq = self.seq - self.seq += 1 - args['seq'] = seq - - ptvsd.server.log.debug('PYD <-- {0!j}', args) - - s = json.dumps(args) - return seq, s - - def pydevd_notify(self, cmd_id, args, is_json=False): - if self.pipe_w is None: - raise EOFError - if is_json: - seq, s = self.make_json_packet(cmd_id, args) - else: - seq, s = self.make_packet(cmd_id, args) - with self.lock: - as_bytes = s - if not isinstance(as_bytes, bytes): - as_bytes = as_bytes.encode('utf-8') - if is_json: - os.write(self.pipe_w, ('Content-Length:%s\r\n\r\n' % (len(as_bytes),)).encode('ascii')) - os.write(self.pipe_w, as_bytes) - - def pydevd_request(self, loop, cmd_id, args, is_json=False): - ''' - If is_json == True the args are expected to be a dict to be - json-serialized with the request, otherwise it's expected - to be the text (to be concatenated with the command id and - seq in the pydevd line-based protocol). - ''' - if self.pipe_w is None: - raise EOFError - if is_json: - seq, s = self.make_json_packet(cmd_id, args) - else: - seq, s = self.make_packet(cmd_id, args) - fut = loop.create_future() - - with self.lock: - self.requests[seq] = loop, fut, ptvsd.server.log.current_handler() - as_bytes = s - if not isinstance(as_bytes, bytes): - as_bytes = as_bytes.encode('utf-8') - if is_json: - os.write(self.pipe_w, ('Content-Length:%s\r\n\r\n' % (len(as_bytes),)).encode('ascii')) - os.write(self.pipe_w, as_bytes) - - return fut - - -class VariablesSorter(object): - - def __init__(self): - self.variables = [] # variables that do not begin with underscores - self.single_underscore = [] # variables beginning with underscores - self.double_underscore = [] # variables beginning with two underscores - self.dunder = [] # variables that begin & end with double underscores - - def append(self, var): - var_name = var['name'] - if var_name.startswith('__'): - if var_name.endswith('__'): - self.dunder.append(var) - else: - self.double_underscore.append(var) - elif var_name.startswith('_'): - self.single_underscore.append(var) - else: - self.variables.append(var) - - def get_sorted_variables(self): - - def get_sort_key(o): - return o['name'] - - self.variables.sort(key=get_sort_key) - self.single_underscore.sort(key=get_sort_key) - self.double_underscore.sort(key=get_sort_key) - self.dunder.sort(key=get_sort_key) - return self.variables + self.single_underscore + self.double_underscore + self.dunder # noqa - - -######################## -# the debug config - - -def bool_parser(str): - return str in ("True", "true", "1") - - -DEBUG_OPTIONS_PARSER = { - 'WAIT_ON_ABNORMAL_EXIT': bool_parser, - 'WAIT_ON_NORMAL_EXIT': bool_parser, - 'BREAK_SYSTEMEXIT_ZERO': bool_parser, - 'REDIRECT_OUTPUT': bool_parser, - 'VERSION': unquote, - 'INTERPRETER_OPTIONS': unquote, - 'WEB_BROWSER_URL': unquote, - 'DJANGO_DEBUG': bool_parser, - 'FLASK_DEBUG': bool_parser, - 'FIX_FILE_PATH_CASE': bool_parser, - 'CLIENT_OS_TYPE': unquote, - 'DEBUG_STDLIB': bool_parser, - 'STOP_ON_ENTRY': bool_parser, - 'SHOW_RETURN_VALUE': bool_parser, - 'MULTIPROCESS': bool_parser, -} - -DEBUG_OPTIONS_BY_FLAG = { - 'RedirectOutput': 'REDIRECT_OUTPUT=True', - 'WaitOnNormalExit': 'WAIT_ON_NORMAL_EXIT=True', - 'WaitOnAbnormalExit': 'WAIT_ON_ABNORMAL_EXIT=True', - 'BreakOnSystemExitZero': 'BREAK_SYSTEMEXIT_ZERO=True', - 'Django': 'DJANGO_DEBUG=True', - 'Flask': 'FLASK_DEBUG=True', - 'Jinja': 'FLASK_DEBUG=True', - 'FixFilePathCase': 'FIX_FILE_PATH_CASE=True', - 'DebugStdLib': 'DEBUG_STDLIB=True', - 'WindowsClient': 'CLIENT_OS_TYPE=WINDOWS', - 'UnixClient': 'CLIENT_OS_TYPE=UNIX', - 'StopOnEntry': 'STOP_ON_ENTRY=True', - 'ShowReturnValue': 'SHOW_RETURN_VALUE=True', - 'Multiprocess': 'MULTIPROCESS=True', -} - - -def _extract_debug_options(opts, flags=None): - """Return the debug options encoded in the given value. - - "opts" is a semicolon-separated string of "key=value" pairs. - "flags" is a list of strings. - - If flags is provided then it is used as a fallback. - - The values come from the launch config: - - { - type:'python', - request:'launch'|'attach', - name:'friendly name for debug config', - debugOptions:[ - 'RedirectOutput', 'Django' - ], - options:'REDIRECT_OUTPUT=True;DJANGO_DEBUG=True' - } - - Further information can be found here: - - https://code.visualstudio.com/docs/editor/debugging#_launchjson-attributes - """ - if not opts: - opts = _build_debug_options(flags) - return _parse_debug_options(opts) - - -def _build_debug_options(flags): - """Build string representation of debug options from the launch config.""" - return ';'.join(DEBUG_OPTIONS_BY_FLAG[flag] - for flag in flags or [] - if flag in DEBUG_OPTIONS_BY_FLAG) - - -def _parse_debug_options(opts): - """Debug options are semicolon separated key=value pairs - WAIT_ON_ABNORMAL_EXIT=True|False - WAIT_ON_NORMAL_EXIT=True|False - BREAK_SYSTEMEXIT_ZERO=True|False - REDIRECT_OUTPUT=True|False - VERSION=string - INTERPRETER_OPTIONS=string - WEB_BROWSER_URL=string url - DJANGO_DEBUG=True|False - CLIENT_OS_TYPE=WINDOWS|UNIX - DEBUG_STDLIB=True|False - """ - options = {} - if not opts: - return options - - for opt in opts.split(';'): - try: - key, value = opt.split('=') - except ValueError: - continue - try: - options[key] = DEBUG_OPTIONS_PARSER[key](value) - except KeyError: - continue - - return options - -######################## -# the message processor - -# TODO: Embed instead of extend (inheritance -> composition). - - -class VSCodeMessageProcessorBase(ipcjson.SocketIO, ipcjson.IpcChannel): - """The base class for VSC message processors.""" - - def __init__(self, socket, notify_closing, timeout=None, own_socket=False): - super(VSCodeMessageProcessorBase, self).__init__( - socket=socket, - own_socket=False, - timeout=timeout, - ) - self.socket = socket - self._own_socket = own_socket - self._notify_closing = notify_closing - - self.server_thread = None - self._closing = False - self._closed = False - self.readylock = threading.Lock() - self.readylock.acquire() # Unlock at the end of start(). - - self._connected = threading.Lock() - self._listening = None - self._connlock = threading.Lock() - - @property - def connected(self): # may send responses/events - with self._connlock: - return _util.is_locked(self._connected) - - @property - def closed(self): - return self._closed or self._closing - - @property - def listening(self): - # TODO: must be disconnected? - with self._connlock: - if self._listening is None: - return False - return _util.is_locked(self._listening) - - def wait_while_connected(self, timeout=None): - """Wait until the client socket is disconnected.""" - with self._connlock: - lock = self._listening - _util.lock_wait(lock, timeout) # Wait until no longer connected. - - def wait_while_listening(self, timeout=None): - """Wait until no longer listening for incoming messages.""" - with self._connlock: - lock = self._listening - if lock is None: - raise RuntimeError('not listening yet') - _util.lock_wait(lock, timeout) # Wait until no longer listening. - - def start(self, threadname): - # event loop - self._start_event_loop() - - # VSC msg processing loop - def process_messages(): - self.readylock.acquire() - with self._connlock: - self._listening = threading.Lock() - try: - self.process_messages() - except (EOFError, TimeoutError): - ptvsd.server.log.exception('Client socket closed', level='info') - with self._connlock: - _util.lock_release(self._listening) - _util.lock_release(self._connected) - self.close() - except socket.error as exc: - if exc.errno == errno.ECONNRESET: - ptvsd.server.log.exception('Client socket forcibly closed', level='info') - with self._connlock: - _util.lock_release(self._listening) - _util.lock_release(self._connected) - self.close() - else: - raise exc - - self.server_thread = _util.new_hidden_thread( - target=process_messages, - name=threadname, - ) - self.server_thread.start() - - # special initialization - self.send_event( - 'output', - category='telemetry', - output='ptvsd', - data={'version': __version__}, - ) - self.readylock.release() - - def close(self): - """Stop the message processor and release its resources.""" - if self.closed: - return - self._closing = True - ptvsd.server.log.debug('Raw closing') - - self._notify_closing() - # Close the editor-side socket. - self._stop_vsc_message_loop() - - # Ensure that the connection is marked as closed. - with self._connlock: - _util.lock_release(self._listening) - _util.lock_release(self._connected) - self._closed = True - - # VSC protocol handlers - - def send_error_response(self, request, message=None): - self.send_response( - request, - success=False, - message=message - ) - - # internal methods - - def _set_disconnected(self): - with self._connlock: - _util.lock_release(self._connected) - - def _wait_for_server_thread(self): - if self.server_thread is None: - return - if not self.server_thread.is_alive(): - return - self.server_thread.join(WAIT_FOR_THREAD_FINISH_TIMEOUT) - - def _stop_vsc_message_loop(self): - self.set_exit() - self._stop_event_loop() - if self.socket is not None and self._own_socket: - try: - self.socket.shutdown(socket.SHUT_RDWR) - self.socket.close() - self._set_disconnected() - except Exception: - ptvsd.server.log.exception('Error on socket shutdown') - - # methods for subclasses to override - - def _start_event_loop(self): - pass - - def _stop_event_loop(self): - pass - - -INITIALIZE_RESPONSE = dict( - supportsCompletionsRequest=True, - supportsConditionalBreakpoints=True, - supportsConfigurationDoneRequest=True, - supportsDebuggerProperties=True, - supportsDelayedStackTraceLoading=True, - supportsEvaluateForHovers=True, - supportsExceptionInfoRequest=True, - supportsExceptionOptions=True, - supportsHitConditionalBreakpoints=True, - supportsLogPoints=True, - supportsModulesRequest=True, - supportsSetExpression=True, - supportsSetVariable=True, - supportsValueFormattingOptions=True, - supportTerminateDebuggee=True, - supportsGotoTargetsRequest=True, - exceptionBreakpointFilters=[ - { - 'filter': 'raised', - 'label': 'Raised Exceptions', - 'default': False - }, - { - 'filter': 'uncaught', - 'label': 'Uncaught Exceptions', - 'default': True - }, - ], -) - - -class VSCLifecycleMsgProcessor(VSCodeMessageProcessorBase): - """Handles adapter lifecycle messages of the VSC debugger protocol.""" - - EXITWAIT = 1 - - def __init__( - self, socket, notify_disconnecting, notify_closing, - notify_launch=None, notify_ready=None, timeout=None, debugging=True, - ): - super(VSCLifecycleMsgProcessor, self).__init__( - socket=socket, - notify_closing=notify_closing, - timeout=timeout, - ) - self._notify_launch = notify_launch or NOOP - self._notify_ready = notify_ready or NOOP - self._notify_disconnecting = notify_disconnecting - - self._stopped = False - - # These are needed to handle behavioral differences between VS and VSC - # https://github.com/Microsoft/VSDebugAdapterHost/wiki/Differences-between-Visual-Studio-Code-and-the-Visual-Studio-Debug-Adapter-Host # noqa - # VS expects a single stopped event in a multi-threaded scenario. - self._client_id = None - - # adapter state - self.start_reason = None - self.debug_options = {} - self._statelock = threading.Lock() - self._debugging = debugging - self._debuggerstopped = False - self._restart_debugger = False - self._exitlock = threading.Lock() - self._exitlock.acquire() # released in handle_exiting() - self._exiting = False - - def handle_debugger_stopped(self, wait=None): - """Deal with the debugger exiting.""" - # Notify the editor that the debugger has stopped. - if not self._debugging: - # TODO: Fail? If this gets called then we must be debugging. - return - - # We run this in a thread to allow handle_exiting() to be called - # at the same time. - def stop(): - if wait is not None: - wait() - # Since pydevd is embedded in the debuggee process, always - # make sure the exited event is sent first. - self._wait_until_exiting(self.EXITWAIT) - self._ensure_debugger_stopped() - - t = _util.new_hidden_thread( - target=stop, - name='stopping', - daemon=False, - ) - t.start() - - def handle_exiting(self, exitcode=None, wait=None): - """Deal with the debuggee exiting.""" - with self._statelock: - if self._exiting: - return - self._exiting = True - - # Notify the editor that the "debuggee" (e.g. script, app) exited. - self.send_event('exited', exitCode=exitcode or 0) - - self._waiting = True - if wait is not None and self.start_reason == 'launch': - normal, abnormal = self._wait_options() - cfg = (normal and not exitcode) or (abnormal and exitcode) - # This must be done before we send a disconnect response - # (which implies before we close the client socket). - wait(cfg) - - # If we are exiting then pydevd must have stopped. - self._ensure_debugger_stopped() - - if self._exitlock is not None: - _util.lock_release(self._exitlock) - - # VSC protocol handlers - - def on_initialize(self, request, args): - self._client_id = args.get('clientID', None) - self._restart_debugger = False - self.send_response(request, **INITIALIZE_RESPONSE) - self.send_event('initialized') - - def on_attach(self, request, args): - multiproc.root_start_request = request - self.start_reason = 'attach' - self._set_debug_options(args) - self._handle_launch_or_attach(request, args) - self.send_response(request) - - def on_launch(self, request, args): - multiproc.root_start_request = request - self.start_reason = 'launch' - self._set_debug_options(args) - self._notify_launch() - self._handle_launch_or_attach(request, args) - self.send_response(request) - - def on_disconnect(self, request, args): - multiproc.kill_subprocesses() - - self._restart_debugger = args.get('restart', False) - - # TODO: docstring - if self._debuggerstopped: # A "terminated" event must have been sent. - self._wait_until_exiting(self.EXITWAIT) - - status = {'sent': False} - - def disconnect_response(): - if status['sent']: - return - self.send_response(request) - status['sent'] = True - - self._notify_disconnecting( - pre_socket_close=disconnect_response, - ) - disconnect_response() - - self._set_disconnected() - - if self.start_reason == 'attach': - if not self._debuggerstopped: - self._handle_detach() - # TODO: We should be able drop the "launch" branch. - elif self.start_reason == 'launch': - if not self.closed: - # Closing the socket causes pydevd to resume all threads, - # so just terminate the process altogether. - sys.exit(0) - - # internal methods - - def _set_debug_options(self, args): - self.debug_options = _extract_debug_options( - args.get('options'), - args.get('debugOptions'), - ) - - def _ensure_debugger_stopped(self): - if not self._debugging: - return - with self._statelock: - if self._debuggerstopped: - return - self._debuggerstopped = True - if not self._restart_debugger: - # multiproc.initial_request = None - self.send_event('terminated') - - def _wait_until_exiting(self, timeout): - lock = self._exitlock - if lock is None: - return - try: - _util.lock_wait(lock, timeout, 'waiting for process exit') - except _util.TimeoutError as exc: - warnings.warn(str(exc)) - - # methods related to shutdown - - def _wait_options(self): - normal = self.debug_options.get('WAIT_ON_NORMAL_EXIT', False) - abnormal = self.debug_options.get('WAIT_ON_ABNORMAL_EXIT', False) - return normal, abnormal - - # methods for subclasses to override - - def _process_debug_options(self, opts): - pass - - def _handle_configurationDone(self, request, args): - pass - - def _handle_launch_or_attach(self, request, args): - pass - - def _handle_detach(self): - pass - - -class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): - """IPC JSON message processor for VSC debugger protocol. - - This translates between the VSC debugger protocol and the pydevd - protocol. - """ - - def __init__( - self, socket, pydevd_notify, pydevd_request, - notify_debugger_ready, - notify_disconnecting, notify_closing, - timeout=None, - ): - super(VSCodeMessageProcessor, self).__init__( - socket=socket, - notify_disconnecting=notify_disconnecting, - notify_closing=notify_closing, - timeout=timeout, - ) - self._pydevd_notify = pydevd_notify - self._pydevd_request = pydevd_request - self._notify_debugger_ready = notify_debugger_ready - - self._client_os_type = 'WINDOWS' if platform.system() == 'Windows' else 'UNIX' - - self.loop = None - self.event_loop_thread = None - - # debugger state - self._success_exitcodes = [] - - # adapter state - self._detached = False - self._path_mappings_received = False - self._path_mappings_applied = False - - def _start_event_loop(self): - self.loop = futures.EventLoop() - self.event_loop_thread = _util.new_hidden_thread( - target=self.loop.run_forever, - name='EventLoop', - ) - self.event_loop_thread.start() - - def _stop_event_loop(self): - self.loop.stop() - self.event_loop_thread.join(WAIT_FOR_THREAD_FINISH_TIMEOUT) - - def start(self, threadname): - super(VSCodeMessageProcessor, self).start(threadname) - if options.multiprocess: - self.start_subprocess_notifier() - - def start_subprocess_notifier(self): - self._subprocess_notifier_thread = _util.new_hidden_thread('SubprocessNotifier', self._subprocess_notifier) - self._subprocess_notifier_thread.start() - - def close(self): - super(VSCodeMessageProcessor, self).close() - if options.multiprocess: - self._subprocess_notifier_thread.join() - - def _subprocess_notifier(self): - while not self.closed: - try: - subprocess_request, subprocess_response = multiproc.subprocess_queue.get(timeout=0.1) - except queue.Empty: - continue - - try: - self.send_event('ptvsd_subprocess', **subprocess_request) - except Exception: - pass - else: - subprocess_response['incomingConnection'] = True - - multiproc.subprocess_queue.task_done() - - # async helpers - - def async_method(m): - """Converts a generator method into an async one.""" - m = futures.wrap_async(m) - - def f(self, *args, **kwargs): - return m(self, self.loop, *args, **kwargs) - - return f - - def async_handler(m): - """Converts a generator method into a fire-and-forget async one.""" - m = futures.wrap_async(m) - - def f(self, *args, **kwargs): - return m(self, self.loop, *args, **kwargs) - - return f - - def sleep(self): - fut = futures.Future(self.loop) - self.loop.call_soon(lambda: fut.set_result(None)) - return fut - - # PyDevd "socket" entry points (and related helpers) - - def pydevd_notify(self, cmd_id, args, is_json=False): - return self._pydevd_notify(cmd_id, args, is_json=is_json) - - def pydevd_request(self, cmd_id, args, is_json=False): - return self._pydevd_request(self.loop, cmd_id, args, is_json=is_json) - - # Instances of this class provide decorators to mark methods as - # handlers for various # pydevd messages - a decorated method is - # added to the map with the corresponding message ID, and is - # looked up there by pydevd event handler below. - class EventHandlers(dict): - - def handler(self, cmd_id): - - def decorate(f): - self[cmd_id] = f - return f - - return decorate - - pydevd_events = EventHandlers() - - def on_pydevd_event(self, cmd_id, seq, args): - if not self._detached: - try: - try: - f = self.pydevd_events[cmd_id] - except KeyError: - raise UnsupportedPyDevdCommandError(cmd_id) - return f(self, seq, args) - except: - ptvsd.server.log.exception('Error handling pydevd event: {0}', args) - raise - else: - return None - - def _wait_for_pydevd_ready(self): - pass - - def _ensure_pydevd_requests_handled(self): - pass - - # VSC protocol handlers - - @async_handler - def on_configurationDone(self, request, args): - self._process_debug_options(self.debug_options) - - self._forward_request_to_pydevd(request, args, send_response=False) - - self._notify_debugger_ready() - - self._notify_ready() - self.send_response(request) - - def _process_debug_options(self, opts): - """Process the launch arguments to configure the debugger.""" - if opts.get('MULTIPROCESS', False): - if not options.multiprocess: - options.multiprocess = True - multiproc.listen_for_subprocesses() - self.start_subprocess_notifier() - - def _send_cmd_version_command(self): - cmd = pydevd_comm.CMD_VERSION - - msg = '1.1\t{}\tID'.format(self._client_os_type) - return self.pydevd_request(cmd, msg) - - def _get_new_setDebuggerProperty_request(self, **kwargs): - return { - "command": "setDebuggerProperty", - "arguments": kwargs, - "type": "request", - # "seq": seq_id, # A new seq should be created for pydevd. - } - - @async_handler - def _handle_launch_or_attach(self, request, args): - self._path_mappings_received = True - - client_os_type = self.debug_options.get('CLIENT_OS_TYPE', '').upper().strip() - if client_os_type and client_os_type not in ('WINDOWS', 'UNIX'): - ptvsd.server.log.warning('Invalid CLIENT_OS_TYPE passed: %s (must be either "WINDOWS" or "UNIX").' % (client_os_type,)) - client_os_type = '' - - if not client_os_type: - for pathMapping in args.get('pathMappings', []): - localRoot = pathMapping.get('localRoot', '') - if localRoot: - if localRoot.startswith('/'): - client_os_type = 'UNIX' - break - - if re.match('^([a-zA-Z]):', localRoot): # Match drive letter - client_os_type = 'WINDOWS' - break - - if not client_os_type: - client_os_type = 'WINDOWS' if platform.system() == 'Windows' else 'UNIX' - - self._client_os_type = client_os_type - - self.pydevd_request(pydevd_comm.CMD_SET_PROTOCOL, 'json') - dont_trace_request = self._get_new_setDebuggerProperty_request( - dontTraceStartPatterns=[PTVSD_DIR_PATH], - dontTraceEndPatterns=['ptvsd_launcher.py'], - skipSuspendOnBreakpointException=('BaseException',), - skipPrintBreakpointException=('NameError',), - multiThreadsSingleNotification=True, - ideOS=self._client_os_type, - ) - yield self.pydevd_request(-1, dont_trace_request, is_json=True) - - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - yield self.pydevd_request(-1, pydevd_request, is_json=True) - - self._path_mappings_applied = True - - default_success_exitcodes = [0] - if self.debug_options.get('DJANGO_DEBUG', False): - default_success_exitcodes += [3] - self._success_exitcodes = args.get('successExitCodes', default_success_exitcodes) - - def _handle_detach(self): - ptvsd.server.log.info('Detaching ...') - # TODO: Skip if already detached? - self._detached = True - - # No related pydevd command id (removes all breaks and resumes threads). - self.pydevd_request( - -1, - {"command": "disconnect", "arguments": {}, "type": "request"}, - is_json=True - ) - - @async_handler - def _resume_all_threads(self): - request = { - "command": "continue", - "arguments": {"threadId": "*"}, - "type": "request" - } - yield self.pydevd_request(-1, request, is_json=True) - - @async_handler - def _forward_request_to_pydevd(self, request, args, send_response=True): - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - cmd_id = -1 # It's actually unused on json requests. - _, _, resp_args = yield self.pydevd_request(cmd_id, pydevd_request, is_json=True) - - if send_response: - if not resp_args.get('success'): - self.send_error_response(request, message=resp_args.get('message', '')) - else: - body = resp_args.get('body') - if body is None: - body = {} - self.send_response(request, **body) - - def on_threads(self, request, args): - self._forward_request_to_pydevd(request, args) - - def on_source(self, request, args): - self._forward_request_to_pydevd(request, args) - - def on_stackTrace(self, request, args): - self._forward_request_to_pydevd(request, args) - - def on_scopes(self, request, args): - self._forward_request_to_pydevd(request, args) - - def on_variables(self, request, args): - self._forward_request_to_pydevd(request, args) - - @async_handler - def on_setVariable(self, request, args): - """Handles DAP SetVariableRequest.""" - var_name = args['name'] - - if var_name.startswith('(return) '): - self.send_error_response( - request, - 'Cannot change return value') - return - - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - _, _, resp_args = yield self.pydevd_request( - pydevd_comm.CMD_CHANGE_VARIABLE, - pydevd_request, - is_json=True) - - body = resp_args['body'] - self.send_response(request, **body) - - @async_handler - def on_evaluate(self, request, args): - """Handles DAP EvaluateRequest.""" - - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - _, _, resp_args = yield self.pydevd_request( - pydevd_comm.CMD_EVALUATE_EXPRESSION, - pydevd_request, - is_json=True) - - body = resp_args['body'] - - # Currently there is an issue in VSC where returning success=false for a eval request, - # in repl context. VSC does not show the error response in the debug console. - if args.get('context', None) == 'repl' and not resp_args['success']: - body['result'] = resp_args['message'] - self.send_response(request, **body) - - def on_setExpression(self, request, args): - self._forward_request_to_pydevd(request, args) - - def on_modules(self, request, args): - self._forward_request_to_pydevd(request, args) - - @async_handler - def on_pause(self, request, args): - # Pause requests cannot be serviced until pydevd is fully initialized. - if not ptvsd.is_attached(): - self.send_response( - request, - success=False, - message='Cannot pause while debugger is initializing', - ) - return - - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - try: - pydevd_request['arguments']['threadId'] = '*' - except KeyError: - pydevd_request['arguments'] = {'threadId': '*'} - - # Always suspend all threads. - self.pydevd_request(pydevd_comm.CMD_THREAD_SUSPEND, - pydevd_request, is_json=True) - self.send_response(request) - - @async_handler - def on_continue(self, request, args): - - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - try: - pydevd_request['arguments']['threadId'] = '*' - except KeyError: - pydevd_request['arguments'] = {'threadId': '*'} - - # Always continue all threads. - self.pydevd_request(pydevd_comm.CMD_THREAD_RUN, - pydevd_request, is_json=True) - self.send_response(request, allThreadsContinued=True) - - def on_next(self, request, args): - self._forward_request_to_pydevd(request, args) - - def on_stepIn(self, request, args): - self._forward_request_to_pydevd(request, args) - - def on_stepOut(self, request, args): - self._forward_request_to_pydevd(request, args) - - def on_gotoTargets(self, request, args): - self._forward_request_to_pydevd(request, args) - - def on_goto(self, request, args): - self._forward_request_to_pydevd(request, args) - - @async_handler - def on_setBreakpoints(self, request, args): - if not self._path_mappings_received: - self.send_error_response(request, "'setBreakpoints' request must be issued after 'launch' or 'attach' request.") - return - - # There might be a concurrent 'launch' or 'attach' request in flight that hasn't - # gotten to processing path mappings yet. If so, spin until it finishes that. - while not self._path_mappings_applied: - yield self.sleep() - - pydevd_request = copy.deepcopy(request) - del pydevd_request['seq'] # A new seq should be created for pydevd. - - _, _, resp_args = yield self.pydevd_request( - pydevd_comm.CMD_SET_BREAK, - pydevd_request, - is_json=True) - - breakpoints = resp_args['body']['breakpoints'] - self.send_response(request, breakpoints=breakpoints) - - def on_setExceptionBreakpoints(self, request, args): - self._forward_request_to_pydevd(request, args) - - def on_exceptionInfo(self, request, args): - self._forward_request_to_pydevd(request, args) - - def on_completions(self, request, args): - self._forward_request_to_pydevd(request, args) - - # Custom ptvsd message - def on_ptvsd_systemInfo(self, request, args): - try: - pid = os.getpid() - except AttributeError: - pid = None - - try: - impl_desc = platform.python_implementation() - except AttributeError: - try: - impl_desc = sys.implementation.name - except AttributeError: - impl_desc = None - - def version_str(v): - return '{}.{}.{}{}{}'.format( - v.major, - v.minor, - v.micro, - v.releaselevel, - v.serial) - - try: - impl_name = sys.implementation.name - except AttributeError: - impl_name = None - - try: - impl_version = version_str(sys.implementation.version) - except AttributeError: - impl_version = None - - sys_info = { - 'ptvsd': { - 'version': __version__, - }, - 'python': { - 'version': version_str(sys.version_info), - 'implementation': { - 'name': impl_name, - 'version': impl_version, - 'description': impl_desc, - }, - }, - 'platform': { - 'name': sys.platform, - }, - 'process': { - 'pid': pid, - 'executable': sys.executable, - 'bitness': 64 if sys.maxsize > 2 ** 32 else 32, - }, - } - self.send_response(request, **sys_info) - - # VS specific custom message handlers - - def on_setDebuggerProperty(self, request, args): - if 'JustMyCodeStepping' in args: - jmc = int(args.get('JustMyCodeStepping', 0)) > 0 - self.debug_options['DEBUG_STDLIB'] = not jmc - - # TODO: Replace the line below with _forward_request_to_pydevd - # after fixing https://github.com/Microsoft/ptvsd/issues/1355 - self.send_response(request) - - # PyDevd protocol event handlers - - @pydevd_events.handler(pydevd_comm.CMD_MODULE_EVENT) - def on_pydevd_module_event(self, seq, args): - body = args.get('body', {}) - self.send_event('module', **body) - - @pydevd_events.handler(pydevd_comm.CMD_INPUT_REQUESTED) - def on_pydevd_input_requested(self, seq, args): - ''' - no-op: if stdin is requested, right now the user is expected to enter - text in the terminal and the debug adapter doesn't really do anything - (although in the future it could see that stdin is being requested and - redirect any evaluation request to stdin). - ''' - - @pydevd_events.handler(pydevd_comm.CMD_THREAD_CREATE) - def on_pydevd_thread_create(self, seq, args): - ''' - :param args: dict. - i.e.: - { - 'type': 'event', - 'event': 'thread', - 'seq': 4, - 'pydevd_cmd_id': 103 - 'body': {'reason': 'started', 'threadId': 'pid_9236_id_2714288164368'}, - } - ''' - # When we receive the thread on tests (due to a threads request), it's possible - # that the `is_attached` return false (but we should report about the - # thread creation anyways). - tid = args['body']['threadId'] - self.send_event('thread', reason='started', threadId=tid) - - - @pydevd_events.handler(pydevd_comm.CMD_THREAD_KILL) - def on_pydevd_thread_kill(self, seq, args): - tid = args['body']['threadId'] - self.send_event('thread', reason='exited', threadId=tid) - - @pydevd_events.handler(pydevd_comm.CMD_PROCESS_EVENT) - def on_pydevd_process_event(self, seq, args): - body = args['body'] - self.send_event('process', **body) - - @pydevd_events.handler(pydevd_comm_constants.CMD_THREAD_SUSPEND_SINGLE_NOTIFICATION) - def on_pydevd_thread_suspend_single_notification(self, seq, args): - # NOTE: We should add the thread to VSC thread map only if the - # thread is seen here for the first time in 'attach' scenario. - # If we are here in 'launch' scenario and we get KeyError then - # there is an issue in reporting of thread creation. - body = args['body'] - - reason = body['reason'] - if reason == 'exception': - exc_name = body['text'] - exc_desc = body['description'] - - if not self.debug_options.get('BREAK_SYSTEMEXIT_ZERO', False) and exc_name == 'SystemExit': - ptvsd.server.log.info('{0}({1!r})', exc_name, exc_desc) - try: - exit_code = int(exc_desc) - except ValueError: - # It is legal to invoke exit() with a non-integer argument, and SystemExit will - # pass that through. It's considered an error exit, same as non-zero integer. - ptvsd.server.log.info('Exit code {0!r} cannot be converted to int, treating as failure', exc_desc) - ignore = False - else: - ignore = exit_code in self._success_exitcodes - ptvsd.server.log.info( - 'Process exiting with {0} exit code {1}', - 'success' if ignore else 'failure', - exc_desc, - ) - if ignore: - self._resume_all_threads() - return - - self.send_event('stopped', **body) - - @pydevd_events.handler(pydevd_comm_constants.CMD_THREAD_RESUME_SINGLE_NOTIFICATION) - def on_pydevd_thread_resume_single_notification(self, seq, args): - tid = args['body']['threadId'] - - if os.getenv('PTVSD_USE_CONTINUED'): - self.send_event('continued', threadId=tid) - - @pydevd_events.handler(pydevd_comm.CMD_WRITE_TO_CONSOLE) - def on_pydevd_cmd_write_to_console2(self, seq, args): - """Handle console output""" - body = args.get('body', {}) - self.send_event('output', **body) diff --git a/tests/ptvsd/common/test_socket.py b/tests/ptvsd/common/test_socket.py index db569e1d..d34203ff 100644 --- a/tests/ptvsd/common/test_socket.py +++ b/tests/ptvsd/common/test_socket.py @@ -4,7 +4,6 @@ import platform import pytest -from ptvsd.common.socket import Address from ptvsd.common.socket import create_server, close_socket @@ -43,118 +42,3 @@ class TestSocketServerReuse(object): close_socket(sock1) if sock2 is not None: close_socket(sock2) - - -class TestAddress(object): - def test_from_raw(self): - serverlocal = Address.as_server("localhost", 9876) - serverremote = Address.as_server("1.2.3.4", 9876) - clientlocal = Address.as_client("localhost", 9876) - clientremote = Address.as_client("1.2.3.4", 9876) - default = Address(None, 1111) - external = Address("", 1111) - values = [ - (serverlocal, serverlocal), - (serverremote, serverremote), - (clientlocal, clientlocal), - (clientremote, clientremote), - (None, default), - ("", external), - ([], default), - ({}, default), - (9876, serverlocal), - ("localhost:9876", clientlocal), - ("1.2.3.4:9876", clientremote), - ("*:9876", Address.as_server("", 9876)), - ("*", external), - (":9876", Address.as_server("", 9876)), - ("localhost", Address("localhost", 1111)), - (":", external), - (dict(host="localhost"), Address("localhost", 1111)), - (dict(port=9876), serverlocal), - (dict(host=None, port=9876), serverlocal), - (dict(host="localhost", port=9876), clientlocal), - (dict(host="localhost", port="9876"), clientlocal), - ] - for value, expected in values: - addr = Address.from_raw(value, defaultport=1111) - assert addr == expected - - @pytest.mark.parametrize("host", ["localhost", "127.0.0.1", "::", "1.2.3.4"]) - def test_as_server_valid_address(self, host): - addr = Address.as_server(host, 9786) - assert addr == Address(host, 9786, isserver=True) - - def test_as_server_public_host(self): - addr = Address.as_server("", 9786) - assert addr == Address("", 9786, isserver=True) - - def test_as_server_default_host(self): - addr = Address.as_server(None, 9786) - assert addr == Address("localhost", 9786, isserver=True) - - @pytest.mark.parametrize("host", [None, "", "localhost", "1.2.3.4"]) - def test_as_server_bad_port(self, host): - port = None - with pytest.raises(TypeError): - Address.as_server(host, port) - - @pytest.mark.parametrize("host", [None, "", "localhost", "1.2.3.4"]) - @pytest.mark.parametrize("port", ["", -1, 65536]) - def test_as_server_bad_port2(self, host, port): - with pytest.raises(ValueError): - Address.as_server(host, port) - - @pytest.mark.parametrize("host", ["localhost", "127.0.0.1", "::", "1.2.3.4"]) - def test_as_client_valid_address(self, host): - addr = Address.as_client(host, 9786) - assert addr == Address(host, 9786, isserver=False) - - def test_as_client_public_host(self): - addr = Address.as_client("", 9786) - assert addr == Address("", 9786, isserver=False) - - def test_as_client_default_host(self): - addr = Address.as_client(None, 9786) - assert addr == Address("localhost", 9786, isserver=False) - - @pytest.mark.parametrize("host", [None, "", "localhost", "1.2.3.4"]) - def test_as_client_bad_port(self, host): - port = None - with pytest.raises(TypeError): - Address.as_client(host, port) - - @pytest.mark.parametrize("host", [None, "", "localhost", "1.2.3.4"]) - @pytest.mark.parametrize("port", ["", -1, 65536]) - def test_as_client_bad_port2(self, host, port): - with pytest.raises(ValueError): - Address.as_client(host, port) - - @pytest.mark.parametrize("host", ["localhost", "127.0.0.1", "::", "1.2.3.4"]) - def test_new_valid_address(self, host): - addr = Address(host, 9786) - assert addr == Address(host, 9786, isserver=False) - - def test_new_public_host(self): - addr = Address("", 9786) - assert addr == Address("", 9786, isserver=True) - - def test_new_default_host(self): - addr = Address(None, 9786) - assert addr == Address("localhost", 9786, isserver=True) - - def test_new_wildcard_host(self): - addr = Address("*", 9786) - assert addr == Address("", 9786, isserver=True) - - @pytest.mark.parametrize("host", [None, "", "localhost", "1.2.3.4"]) - def test_new_bad_port(self, host): - port = None - with pytest.raises(TypeError): - Address(host, port) - - @pytest.mark.parametrize("host", [None, "", "localhost", "1.2.3.4"]) - @pytest.mark.parametrize("port", ["", -1, 65536]) - def test_new_bad_port2(self, host, port): - with pytest.raises(ValueError): - Address(host, port)