Remove code duplication in runner.py. (#469)

(fixes gh-464)

The code in ptvsd/runner.py was originally copied from elsewhere and slightly adapted. Over time the APIs diverged, which lead to the failure in gh-464. This PR fixes the problem by getting rid of the code duplication.
This commit is contained in:
Eric Snow 2018-06-12 17:32:47 -06:00 committed by GitHub
parent d635e22d33
commit d498fd3582
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 547 additions and 559 deletions

View file

@ -8,7 +8,7 @@ from ptvsd.socket import (
from .exit_handlers import (
ExitHandlers, UnsupportedSignalError,
kill_current_proc)
from .session import DebugSession
from .session import PyDevdDebugSession
from ._util import ignore_errors, debug
@ -49,8 +49,10 @@ class DaemonStoppedError(DaemonError):
# TODO: Inherit from Closeable.
# TODO: Inherit from Startable?
class Daemon(object):
"""The process-level manager for the VSC protocol debug adapter."""
class DaemonBase(object):
"""The base class for DAP daemons."""
SESSION = None
exitcode = 0
@ -61,13 +63,15 @@ class Daemon(object):
self._started = False
self._closed = False
self._pydevd = None # set when started
# socket-related
self._sock = None # set when started
self._server = None
# session-related
self._singlesession = singlesession
self._server = None
self._session = None
self._numsessions = 0
self._sessionlock = None
@ -84,10 +88,6 @@ class Daemon(object):
if addhandlers:
self._install_exit_handlers()
@property
def pydevd(self):
return self._pydevd
@property
def session(self):
"""The current session."""
@ -118,7 +118,9 @@ class Daemon(object):
def is_running(self):
"""Return True if the daemon is running."""
if self._pydevd is None:
if self._closed:
return False
if self._sock is None:
return False
return True
@ -130,27 +132,29 @@ class Daemon(object):
raise RuntimeError('already started')
self._started = True
return self._start()
sock = self._start()
self._sock = sock
return sock
def start_server(self, addr, hidebadsessions=True):
"""Return (pydevd "socket", next_session) with a new server socket."""
"""Return ("socket", next_session) with a new server socket."""
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)
self._sessionlock = threading.Lock()
pydevd = self._pydevd
sock = self._sock
def check_ready():
self._check_ready_for_session()
def check_ready(**kwargs):
self._check_ready_for_session(**kwargs)
if self._server is None:
raise DaemonStoppedError()
def next_session(**kwargs):
def next_session(timeout=None, **kwargs):
server = self._server
sessionlock = self._sessionlock
check_ready()
check_ready(checksession=False)
debug('getting next session')
sessionlock.acquire() # Released in _finish_session().
@ -164,7 +168,7 @@ class Daemon(object):
client = connect(server, None, **kwargs)
self._bind_session(client)
debug('starting session')
self._start_session('ptvsd.Server', timeout)
self._start_session_safely('ptvsd.Server', timeout=timeout)
debug('session started')
return self._session
except Exception as exc:
@ -178,18 +182,18 @@ class Daemon(object):
self._stop_quietly()
raise
return pydevd, next_session
return sock, next_session
def start_client(self, addr):
"""Return (pydevd "socket", start_session) with a new client socket."""
"""Return ("socket", start_session) with a new client socket."""
addr = Address.from_raw(addr)
with self.started():
assert self.session is None
client = create_client()
connect(client, addr)
pydevd = self._pydevd
sock = self._sock
def start_session():
def start_session(**kwargs):
self._check_ready_for_session()
if self._server is not None:
raise RuntimeError('running as server')
@ -198,15 +202,15 @@ class Daemon(object):
try:
self._bind_session(client)
self._start_session('ptvsd.Client', None)
self._start_session_safely('ptvsd.Client', **kwargs)
return self._session
except Exception:
self._stop_quietly()
raise
return pydevd, start_session
return sock, start_session
def start_session(self, session, threadname, timeout=None):
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
@ -217,7 +221,7 @@ class Daemon(object):
raise RuntimeError('running as server')
self._bind_session(session)
self._start_session(threadname, timeout)
self._start_session_safely(threadname, **kwargs)
return self.session
def close(self):
@ -228,41 +232,26 @@ class Daemon(object):
self._close()
def re_build_breakpoints(self):
"""Restore the breakpoints to their last values."""
if self.session is None:
return
return self.session.re_build_breakpoints()
# internal methods
def _check_ready_for_session(self):
def _check_ready_for_session(self, checksession=True):
if self._closed:
raise DaemonClosedError()
if not self._started:
raise DaemonStoppedError('never started')
if self._pydevd is None:
if self._sock is None:
raise DaemonStoppedError()
if self.session is not None:
if checksession and self.session is not None:
raise RuntimeError('session already started')
def _close(self):
self._stop()
self._pydevd = None
self._sock = None
if self._wait_on_exit(self.exitcode):
self._wait_for_user()
def _start(self):
self._pydevd = wrapper.PydevdSocket(
self._handle_pydevd_message,
self._handle_pydevd_close,
self._getpeername,
self._getsockname,
)
return self._pydevd
def _stop(self):
sessionlock = self._sessionlock
self._sessionlock = None
@ -282,9 +271,9 @@ class Daemon(object):
with ignore_errors():
close_socket(server)
if self._pydevd is not None:
if self._sock is not None:
with ignore_errors():
close_socket(self._pydevd)
close_socket(self._sock)
def _stop_quietly(self):
if self._closed: # XXX wrong?
@ -307,7 +296,7 @@ class Daemon(object):
# internal session-related methods
def _bind_session(self, session):
session = DebugSession.from_raw(
session = self.SESSION.from_raw(
session,
notify_closing=self._handle_session_closing,
ownsock=True,
@ -315,14 +304,9 @@ class Daemon(object):
self._session = session
self._numsessions += 1
def _start_session(self, threadname, timeout):
def _start_session_safely(self, threadname, **kwargs):
try:
self.session.start(
threadname,
self._pydevd.pydevd_notify,
self._pydevd.pydevd_request,
timeout=timeout,
)
self._start_session(threadname, **kwargs)
except Exception:
with ignore_errors():
self._finish_session()
@ -398,6 +382,52 @@ class Daemon(object):
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
)
class Daemon(DaemonBase):
"""The process-level manager for the VSC protocol debug adapter."""
SESSION = PyDevdDebugSession
@property
def pydevd(self):
return self._sock
def re_build_breakpoints(self):
"""Restore the breakpoints to their last values."""
if self.session is None:
return
return self.session.re_build_breakpoints()
# 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
)
# internal methods for PyDevdSocket().
def _handle_pydevd_message(self, cmdid, seq, text):

View file

@ -8,7 +8,7 @@ from ptvsd.daemon import Daemon, DaemonStoppedError, DaemonClosedError
from ptvsd._util import debug
def start_server(daemon, host, port):
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
@ -16,11 +16,11 @@ def start_server(daemon, host, port):
This is a replacement for _pydevd_bundle.pydevd_comm.start_server.
"""
pydevd, next_session = daemon.start_server((host, port))
sock, next_session = daemon.start_server((host, port))
def handle_next():
try:
session = next_session()
session = next_session(**kwargs)
debug('done waiting')
return session
except (DaemonClosedError, DaemonStoppedError):
@ -54,10 +54,10 @@ def start_server(daemon, host, port):
t.is_pydev_daemon_thread = True
t.daemon = True
t.start()
return pydevd
return sock
def start_client(daemon, host, port):
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
@ -65,9 +65,9 @@ def start_client(daemon, host, port):
This is a replacement for _pydevd_bundle.pydevd_comm.start_client.
"""
pydevd, start_session = daemon.start_client((host, port))
start_session()
return pydevd
sock, start_session = daemon.start_client((host, port))
start_session(**kwargs)
return sock
def install(pydevd, address,

View file

@ -2,33 +2,28 @@
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
import atexit
import os
import platform
import pydevd
import signal
import socket
import sys
import time
import threading
import traceback
import warnings
from ptvsd import ipcjson, __version__
from ptvsd.daemon import DaemonClosedError
from ptvsd.pydevd_hooks import start_client
from ptvsd.socket import close_socket
from ptvsd.wrapper import WAIT_FOR_DISCONNECT_REQUEST_TIMEOUT, WAIT_FOR_THREAD_FINISH_TIMEOUT, INITIALIZE_RESPONSE # noqa
from ptvsd.daemon import DaemonBase
from ptvsd.session import DebugSession
from ptvsd.wrapper import (
WAIT_FOR_THREAD_FINISH_TIMEOUT, VSCLifecycleMsgProcessor)
from pydevd import init_stdout_redirect, init_stderr_redirect
HOSTNAME = 'localhost'
WAIT_FOR_LAUNCH_REQUEST_TIMEOUT = 10000
OUTPUT_POLL_PERIOD = 0.3
def run(address, filename, is_module, *args, **kwargs):
# TODO: docstring
# TODO: client/server -> address
if not start_message_processor(*address):
daemon = Daemon()
if not daemon.wait_for_launch(address):
return
debugger = pydevd.PyDB()
@ -45,18 +40,8 @@ def run(address, filename, is_module, *args, **kwargs):
time.sleep(OUTPUT_POLL_PERIOD + 0.1)
def start_message_processor(host, port_num):
launch_notification = threading.Event()
daemon = Daemon(
notify_launch=launch_notification.set,
addhandlers=True, killonclose=True)
start_client(daemon, host, port_num)
return launch_notification.wait(WAIT_FOR_LAUNCH_REQUEST_TIMEOUT)
class OutputRedirection(object):
# TODO: docstring
def __init__(self, on_output=lambda category, output: None):
self._on_output = on_output
@ -64,6 +49,7 @@ class OutputRedirection(object):
self._thread = None
def start(self):
# TODO: docstring
init_stdout_redirect()
init_stderr_redirect()
self._thread = threading.Thread(
@ -74,6 +60,7 @@ class OutputRedirection(object):
self._thread.start()
def stop(self):
# TODO: docstring
if self._stopped:
return
@ -81,7 +68,6 @@ class OutputRedirection(object):
self._thread.join(WAIT_FOR_THREAD_FINISH_TIMEOUT)
def _run(self):
import sys
while not self._stopped:
self._check_output(sys.stdoutBuf, 'stdout')
self._check_output(sys.stderrBuf, 'stderr')
@ -104,243 +90,47 @@ class OutputRedirection(object):
traceback.print_exc()
# TODO: Inherit from ptvsd.daemon.Daemon.
class Daemon(object):
class Daemon(DaemonBase):
"""The process-level manager for the VSC protocol debug adapter."""
def __init__(self,
notify_launch=lambda: None,
addhandlers=True,
killonclose=True):
LAUNCH_TIMEOUT = 10000 # seconds
self.exitcode = 0
self.exiting_via_exit_handler = False
class SESSION(DebugSession):
class MESSAGE_PROCESSOR(VSCLifecycleMsgProcessor):
def on_invalid_request(self, request, args):
self.send_response(request, success=True)
self.addhandlers = addhandlers
self.killonclose = killonclose
self._notify_launch = notify_launch
self._closed = False
self._client = None
self._adapter = None
def start(self):
if self._closed:
raise DaemonClosedError()
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):
self._output_monitor = OutputRedirection(self._send_output)
self._output_monitor.start()
return NoSocket()
return None
def start_session(self, client):
"""Set the client socket to use for the debug adapter.
A VSC message loop is started for the client.
"""
if self._closed:
raise DaemonClosedError()
if self._client is not None:
raise RuntimeError('connection already set')
self._client = client
self._adapter = VSCodeMessageProcessor(
client,
self._notify_launch,
self._handle_vsc_disconnect,
self._handle_vsc_close,
)
self._adapter.start()
if self.addhandlers:
self._add_atexit_handler()
self._set_signal_handlers()
return self._adapter
def close(self):
"""Stop all loops and release all resources."""
def _close(self):
self._output_monitor.stop()
if self._closed:
raise DaemonClosedError('already closed')
self._closed = True
if self._client is not None:
self._release_connection()
# internal methods
def _add_atexit_handler(self):
def handler():
self.exiting_via_exit_handler = True
if not self._closed:
self.close()
if self._adapter is not None:
self._adapter._wait_for_server_thread()
atexit.register(handler)
def _set_signal_handlers(self):
if platform.system() == 'Windows':
return None
def handler(signum, frame):
if not self._closed:
self.close()
sys.exit(0)
signal.signal(signal.SIGHUP, handler)
def _release_connection(self):
if self._adapter is not None:
self._adapter.handle_stopped(self.exitcode)
self._adapter.close()
close_socket(self._client)
# internal methods for VSCodeMessageProcessor
def _handle_vsc_disconnect(self, kill=False):
if not self._closed:
self.close()
if kill and self.killonclose and not self.exiting_via_exit_handler:
os.kill(os.getpid(), signal.SIGTERM)
def _handle_vsc_close(self):
if self._closed:
return
self.close()
super(Daemon, self)._close()
def _send_output(self, category, output):
self._adapter.send_event('output', category=category, output=output)
if self.session is None:
return
self.session._msgprocessor.send_event('output',
category=category,
output=output)
class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel):
"""IPC JSON message processor for VSC debugger protocol.
class NoSocket(object):
"""A object with a noop socket lifecycle."""
This translates between the VSC debugger protocol and the pydevd
protocol.
"""
def __init__(
self,
socket,
notify_launch=lambda: None,
notify_disconnecting=lambda: None,
notify_closing=lambda: None,
logfile=None,
):
super(VSCodeMessageProcessor, self).__init__(
socket=socket, own_socket=False, logfile=logfile)
self._socket = socket
self._notify_launch = notify_launch
self._notify_disconnecting = notify_disconnecting
self._notify_closing = notify_closing
self.server_thread = None
self._closed = False
# adapter state
self.disconnect_request = None
self.disconnect_request_event = threading.Event()
self._exited = False
def start(self):
# VSC msg processing loop
self.server_thread = threading.Thread(
target=self.process_messages,
name='ptvsd.Client',
)
self.server_thread.pydev_do_not_trace = True
self.server_thread.is_pydev_daemon_thread = True
self.server_thread.daemon = True
self.server_thread.start()
# special initialization
self.send_event(
'output',
category='telemetry',
output='ptvsd',
data={
'version': __version__,
'nodebug': True
},
)
# closing the adapter
def shutdown(self, *args, **kwargs):
pass
def close(self):
"""Stop the message processor and release its resources."""
if self._closed:
return
self._closed = True
self._notify_closing()
# Close the editor-side socket.
self._stop_vsc_message_loop()
def _stop_vsc_message_loop(self):
self.set_exit()
if self._socket:
try:
self._socket.shutdown(socket.SHUT_RDWR)
self._socket.close()
except Exception:
pass
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 handle_stopped(self, exitcode):
"""Finalize the protocol connection."""
if self._exited:
return
self._exited = True
# Notify the editor that the "debuggee" (e.g. script, app) exited.
self.send_event('exited', exitCode=exitcode)
# Notify the editor that the debugger has stopped.
self.send_event('terminated')
# The editor will send a "disconnect" request at this point.
self._wait_for_disconnect()
def _wait_for_disconnect(self, timeout=None):
if timeout is None:
timeout = WAIT_FOR_DISCONNECT_REQUEST_TIMEOUT
if not self.disconnect_request_event.wait(timeout):
warnings.warn('timed out waiting for disconnect request')
if self.disconnect_request is not None:
self.send_response(self.disconnect_request)
self.disconnect_request = None
def _handle_disconnect(self, request):
self.disconnect_request = request
self.disconnect_request_event.set()
self._notify_disconnecting(not self._closed)
if not self._closed:
self.close()
# VSC protocol handlers
def on_initialize(self, request, args):
self.send_response(request, **INITIALIZE_RESPONSE)
self.send_event('initialized')
def on_configurationDone(self, request, args):
self.send_response(request)
def on_launch(self, request, args):
self._notify_launch()
self.send_response(request)
def on_disconnect(self, request, args):
self._handle_disconnect(request)
def on_invalid_request(self, request, args):
self.send_response(request, success=True)
pass

View file

@ -6,6 +6,8 @@ from ._util import Closeable, Startable, debug
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
@ -57,18 +59,6 @@ class DebugSession(Startable, Closeable):
def msgprocessor(self):
return self._msgprocessor
def handle_pydevd_message(self, cmdid, seq, text):
if self._msgprocessor is None:
# TODO: Do more than ignore?
return
return self._msgprocessor.on_pydevd_event(cmdid, seq, text)
def re_build_breakpoints(self):
"""Restore the breakpoints to their last values."""
if self._msgprocessor is None:
return
return self._msgprocessor.re_build_breakpoints()
def wait_options(self):
"""Return (normal, abnormal) based on the session's launch config."""
if self._msgprocessor is None:
@ -96,19 +86,17 @@ class DebugSession(Startable, Closeable):
# internal methods
def _start(self, threadname, pydevd_notify, pydevd_request, timeout=None):
"""Start the message handling for the session.
A VSC message loop is started.
"""
self._msgprocessor = VSCodeMessageProcessor(
def _new_msg_processor(self, **kwargs):
return self.MESSAGE_PROCESSOR(
self._sock,
pydevd_notify,
pydevd_request,
notify_disconnecting=self._handle_vsc_disconnect,
notify_closing=self._handle_vsc_close,
timeout=timeout,
**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
@ -142,3 +130,21 @@ class DebugSession(Startable, Closeable):
def _handle_vsc_close(self):
debug('processor closing')
self.close()
class PyDevdDebugSession(DebugSession):
"""A single DAP session for a network client socket."""
MESSAGE_PROCESSOR = VSCodeMessageProcessor
def handle_pydevd_message(self, cmdid, seq, text):
if self._msgprocessor is None:
# TODO: Do more than ignore?
return
return self._msgprocessor.on_pydevd_event(cmdid, seq, text)
def re_build_breakpoints(self):
"""Restore the breakpoints to their last values."""
if self._msgprocessor is None:
return
return self._msgprocessor.re_build_breakpoints()

View file

@ -48,35 +48,19 @@ from ptvsd.socket import TimeoutError # noqa
# print(s)
#ipcjson._TRACE = ipcjson_trace
WAIT_FOR_DISCONNECT_REQUEST_TIMEOUT = 2
WAIT_FOR_THREAD_FINISH_TIMEOUT = 1
INITIALIZE_RESPONSE = dict(
supportsExceptionInfoRequest=True,
supportsConfigurationDoneRequest=True,
supportsConditionalBreakpoints=True,
supportsHitConditionalBreakpoints=True,
supportsSetVariable=True,
supportsExceptionOptions=True,
supportsEvaluateForHovers=True,
supportsValueFormattingOptions=True,
supportsSetExpression=True,
supportsModulesRequest=True,
supportsLogPoints=True,
supportTerminateDebuggee=True,
exceptionBreakpointFilters=[
{
'filter': 'raised',
'label': 'Raised Exceptions',
'default': False
},
{
'filter': 'uncaught',
'label': 'Uncaught Exceptions',
'default': True
},
],
)
def is_debugger_internal_thread(thread_name):
# TODO: docstring
if thread_name:
if thread_name.startswith('pydevd.'):
return True
elif thread_name.startswith('ptvsd.'):
return True
return False
class SafeReprPresentationProvider(pydevd_extapi.StrPresentationProvider):
@ -702,72 +686,137 @@ class InternalsFilter(object):
return False
class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel):
"""IPC JSON message processor for VSC debugger protocol.
########################
# the debug config
This translates between the VSC debugger protocol and the pydevd
protocol.
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,
'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,
'WINDOWS_CLIENT': bool_parser,
'DEBUG_STDLIB': bool_parser,
}
DEBUG_OPTIONS_BY_FLAG = {
'RedirectOutput': 'REDIRECT_OUTPUT=True',
'WaitOnNormalExit': 'WAIT_ON_NORMAL_EXIT=True',
'WaitOnAbnormalExit': 'WAIT_ON_ABNORMAL_EXIT=True',
'Django': 'DJANGO_DEBUG=True',
'Flask': 'FLASK_DEBUG=True',
'Jinja': 'FLASK_DEBUG=True',
'FixFilePathCase': 'FIX_FILE_PATH_CASE=True',
'DebugStdLib': 'DEBUG_STDLIB=True',
'WindowsClient': 'WINDOWS_CLIENT=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 __init__(self, socket, pydevd_notify, pydevd_request,
notify_disconnecting, notify_closing,
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
REDIRECT_OUTPUT=True|False
VERSION=string
INTERPRETER_OPTIONS=string
WEB_BROWSER_URL=string url
DJANGO_DEBUG=True|False
WINDOWS_CLIENT=True|False
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
if 'WINDOWS_CLIENT' not in options:
options['WINDOWS_CLIENT'] = platform.system() == 'Windows' # noqa
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, logfile=None,
):
super(VSCodeMessageProcessor, self).__init__(socket=socket,
own_socket=False,
timeout=timeout,
logfile=logfile)
super(VSCodeMessageProcessorBase, self).__init__(
socket=socket,
own_socket=False,
timeout=timeout,
logfile=logfile,
)
self.socket = socket
self._pydevd_notify = pydevd_notify
self._pydevd_request = pydevd_request
self._notify_disconnecting = notify_disconnecting
self._notify_closing = notify_closing
self.loop = None
self.event_loop_thread = None
self.server_thread = None
self._closed = False
self.bkpoints = None
# debugger state
self.is_process_created = False
self.is_process_created_lock = threading.Lock()
self.stack_traces = {}
self.stack_traces_lock = threading.Lock()
self.active_exceptions = {}
self.active_exceptions_lock = threading.Lock()
self.thread_map = IDMap()
self.frame_map = IDMap()
self.var_map = IDMap()
self.bp_map = IDMap()
self.source_map = IDMap()
self.enable_source_references = False
self.next_var_ref = 0
self.exceptions_mgr = ExceptionsManager(self)
self.modules_mgr = ModulesManager(self)
self.internals_filter = InternalsFilter()
# adapter state
self.readylock = threading.Lock()
self.readylock.acquire() # Unlock at the end of start().
self.disconnect_request = None
self.debug_options = {}
self.disconnect_request_event = threading.Event()
self._exited = False
self.path_casing = PathUnNormcase()
self.start_reason = None
def start(self, threadname):
# event loop
self.loop = futures.EventLoop()
self.event_loop_thread = threading.Thread(
target=self.loop.run_forever,
name='ptvsd.EventLoop',
)
self.event_loop_thread.pydev_do_not_trace = True
self.event_loop_thread.is_pydev_daemon_thread = True
self.event_loop_thread.daemon = True
self.event_loop_thread.start()
self._start_event_loop()
# VSC msg processing loop
def process_messages():
@ -797,8 +846,6 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel):
debug('output sent')
self.readylock.release()
# closing the adapter
def close(self):
"""Stop the message processor and release its resources."""
debug('raw closing')
@ -810,10 +857,27 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel):
# Close the editor-side socket.
self._stop_vsc_message_loop()
# VSC protocol handlers
def send_error_response(self, request, message=None):
self.send_response(
request,
success=False,
message=message
)
# internal methods
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.loop.stop()
self.event_loop_thread.join(WAIT_FOR_THREAD_FINISH_TIMEOUT)
self._stop_event_loop()
if self.socket:
try:
self.socket.shutdown(socket.SHUT_RDWR)
@ -822,16 +886,66 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel):
# TODO: log the error
pass
def _wait_options(self):
# In attach scenarios, we can't assume that the process is actually
# interactive and has a console, so ignore these options.
# In launch scenarios, we only want "press any key" to show up when
# program terminates by itself, not when user explicitly stops it.
if self.disconnect_request or self.start_reason != 'launch':
return False, False
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 _start_event_loop(self):
pass
def _stop_event_loop(self):
pass
INITIALIZE_RESPONSE = dict(
supportsExceptionInfoRequest=True,
supportsConfigurationDoneRequest=True,
supportsConditionalBreakpoints=True,
supportsHitConditionalBreakpoints=True,
supportsSetVariable=True,
supportsExceptionOptions=True,
supportsEvaluateForHovers=True,
supportsValueFormattingOptions=True,
supportsSetExpression=True,
supportsModulesRequest=True,
supportsLogPoints=True,
supportTerminateDebuggee=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."""
def __init__(self, socket,
notify_disconnecting, notify_closing, notify_launch=None,
timeout=None, logfile=None,
):
super(VSCLifecycleMsgProcessor, self).__init__(
socket=socket,
notify_closing=notify_closing,
timeout=timeout,
logfile=logfile,
)
self._notify_launch = notify_launch or (lambda: None)
self._notify_disconnecting = notify_disconnecting
self._exited = False
# adapter state
self.disconnect_request = None
self.debug_options = {}
self.disconnect_request_event = threading.Event()
self.start_reason = None
def handle_session_stopped(self, exitcode=None):
"""Finalize the protocol connection."""
@ -848,6 +962,52 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel):
# The editor will send a "disconnect" request at this point.
self._wait_for_disconnect()
# VSC protocol handlers
def on_initialize(self, request, args):
# TODO: docstring
self.send_response(request, **INITIALIZE_RESPONSE)
self.send_event('initialized')
def on_configurationDone(self, request, args):
# TODO: docstring
self.send_response(request)
self._process_debug_options(self.debug_options)
self._handle_configurationDone(args)
def on_attach(self, request, args):
# TODO: docstring
self.start_reason = 'attach'
self._set_debug_options(args)
self._handle_attach(args)
self.send_response(request)
def on_launch(self, request, args):
# TODO: docstring
self.start_reason = 'launch'
self._set_debug_options(args)
self._notify_launch()
self._handle_launch(args)
self.send_response(request)
def on_disconnect(self, request, args):
# TODO: docstring
if self.start_reason == 'launch':
self._handle_disconnect(request)
else:
self.send_response(request)
self._notify_disconnecting(kill=False)
# internal methods
def _set_debug_options(self, args):
self.debug_options = _extract_debug_options(
args.get('options'),
args.get('debugOptions'),
)
# methods related to shutdown
def _wait_for_disconnect(self, timeout=None):
if timeout is None:
timeout = WAIT_FOR_DISCONNECT_REQUEST_TIMEOUT
@ -859,6 +1019,7 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel):
self.disconnect_request = None
def _handle_disconnect(self, request):
assert self.start_reason == 'launch'
self.disconnect_request = request
self.disconnect_request_event.set()
self._notify_disconnecting(kill=not self._closed)
@ -867,12 +1028,92 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel):
# so just terminate the process altogether.
sys.exit(0)
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 _wait_options(self):
# In attach scenarios, we can't assume that the process is actually
# interactive and has a console, so ignore these options.
# In launch scenarios, we only want "press any key" to show up when
# program terminates by itself, not when user explicitly stops it.
if self.disconnect_request or self.start_reason != 'launch':
return False, False
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, args):
pass
def _handle_attach(self, args):
pass
def _handle_launch(self, args):
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_disconnecting, notify_closing,
timeout=None, logfile=None,
):
super(VSCodeMessageProcessor, self).__init__(
socket=socket,
notify_disconnecting=notify_disconnecting,
notify_closing=notify_closing,
timeout=timeout,
logfile=logfile,
)
self._pydevd_notify = pydevd_notify
self._pydevd_request = pydevd_request
self.loop = None
self.event_loop_thread = None
self.bkpoints = None
# debugger state
self.is_process_created = False
self.is_process_created_lock = threading.Lock()
self.stack_traces = {}
self.stack_traces_lock = threading.Lock()
self.active_exceptions = {}
self.active_exceptions_lock = threading.Lock()
self.thread_map = IDMap()
self.frame_map = IDMap()
self.var_map = IDMap()
self.bp_map = IDMap()
self.source_map = IDMap()
self.enable_source_references = False
self.next_var_ref = 0
self.exceptions_mgr = ExceptionsManager(self)
self.modules_mgr = ModulesManager(self)
self.internals_filter = InternalsFilter()
# adapter state
self.path_casing = PathUnNormcase()
def _start_event_loop(self):
self.loop = futures.EventLoop()
self.event_loop_thread = threading.Thread(
target=self.loop.run_forever,
name='ptvsd.EventLoop',
)
self.event_loop_thread.pydev_do_not_trace = True
self.event_loop_thread.is_pydev_daemon_thread = True
self.event_loop_thread.daemon = True
self.event_loop_thread.start()
def _stop_event_loop(self):
self.loop.stop()
self.event_loop_thread.join(WAIT_FOR_THREAD_FINISH_TIMEOUT)
# async helpers
@ -961,17 +1202,7 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel):
# VSC protocol handlers
@async_handler
def on_initialize(self, request, args):
# TODO: docstring
self.send_response(request, **INITIALIZE_RESPONSE)
self.send_event('initialized')
@async_handler
def on_configurationDone(self, request, args):
# TODO: docstring
self.send_response(request)
self.process_debug_options()
def _handle_configurationDone(self, args):
self.pydevd_request(pydevd_comm.CMD_RUN, '')
if self.start_reason == 'attach':
@ -983,92 +1214,17 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel):
self.is_process_created = True
self.send_process_event(self.start_reason)
def process_debug_options(self):
"""
Process the launch arguments to configure the debugger.
""" # noqa
if self.debug_options.get('FIX_FILE_PATH_CASE', False):
def _process_debug_options(self, opts):
"""Process the launch arguments to configure the debugger."""
if opts.get('FIX_FILE_PATH_CASE', False):
self.path_casing.enable()
if self.debug_options.get('REDIRECT_OUTPUT', False):
if opts.get('REDIRECT_OUTPUT', False):
redirect_output = 'STDOUT\tSTDERR'
else:
redirect_output = ''
self.pydevd_request(pydevd_comm.CMD_REDIRECT_OUTPUT, redirect_output)
def build_debug_options(self, debug_options):
"""
Build string representation of debug options from launch config (as provided by VSC)
Further information can be found here https://code.visualstudio.com/docs/editor/debugging#_launchjson-attributes
{
type:'python',
request:'launch'|'attach',
name:'friendly name for debug config',
debugOptions:[
'RedirectOutput', 'Django'
]
}
""" # noqa
debug_option_mapping = {
'RedirectOutput': 'REDIRECT_OUTPUT=True',
'WaitOnNormalExit': 'WAIT_ON_NORMAL_EXIT=True',
'WaitOnAbnormalExit': 'WAIT_ON_ABNORMAL_EXIT=True',
'Django': 'DJANGO_DEBUG=True',
'Flask': 'FLASK_DEBUG=True',
'Jinja': 'FLASK_DEBUG=True',
'FixFilePathCase': 'FIX_FILE_PATH_CASE=True',
'DebugStdLib': 'DEBUG_STDLIB=True',
'WindowsClient': 'WINDOWS_CLIENT=True',
}
return ';'.join(debug_option_mapping[option]
for option in debug_options
if option in debug_option_mapping)
def _parse_debug_options(self, debug_options):
"""Debug options are semicolon separated key=value pairs
WAIT_ON_ABNORMAL_EXIT=True|False
WAIT_ON_NORMAL_EXIT=True|False
REDIRECT_OUTPUT=True|False
VERSION=string
INTERPRETER_OPTIONS=string
WEB_BROWSER_URL=string url
DJANGO_DEBUG=True|False
WINDOWS_CLIENT=True|False
DEBUG_STDLIB=True|False
"""
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,
'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,
'WINDOWS_CLIENT': bool_parser,
'DEBUG_STDLIB': bool_parser,
}
options = {}
for opt in debug_options.split(';'):
try:
key, value = opt.split('=')
except ValueError:
continue
try:
options[key] = DEBUG_OPTIONS_PARSER[key](value)
except KeyError:
continue
if 'WINDOWS_CLIENT' not in options:
options['WINDOWS_CLIENT'] = platform.system() == 'Windows' # noqa
return options
def _initialize_path_maps(self, args):
pathMaps = []
for pathMapping in args.get('pathMappings', []):
@ -1090,34 +1246,14 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel):
return self.pydevd_request(cmd, msg)
@async_handler
def on_attach(self, request, args):
# TODO: docstring
self.start_reason = 'attach'
def _handle_attach(self, args):
self._initialize_path_maps(args)
options = self.build_debug_options(args.get('debugOptions', []))
self.debug_options = self._parse_debug_options(
args.get('options', options))
yield self._send_cmd_version_command()
self.send_response(request)
@async_handler
def on_launch(self, request, args):
# TODO: docstring
self.start_reason = 'launch'
def _handle_launch(self, args):
self._initialize_path_maps(args)
options = self.build_debug_options(args.get('debugOptions', []))
self.debug_options = self._parse_debug_options(
args.get('options', options))
yield self._send_cmd_version_command()
self.send_response(request)
def on_disconnect(self, request, args):
# TODO: docstring
if self.start_reason == 'launch':
self._handle_disconnect(request)
else:
self.send_response(request)
self._notify_disconnecting(kill=False)
def send_process_event(self, start_method):
# TODO: docstring
@ -1129,21 +1265,6 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel):
}
self.send_event('process', **evt)
def send_error_response(self, request, message=None):
self.send_response(
request,
success=False,
message=message
)
def is_debugger_internal_thread(self, thread_name):
if thread_name:
if thread_name.startswith('pydevd.'):
return True
elif thread_name.startswith('ptvsd.'):
return True
return False
@async_handler
def on_threads(self, request, args):
# TODO: docstring
@ -1168,7 +1289,7 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel):
except KeyError:
name = None
if not self.is_debugger_internal_thread(name):
if not is_debugger_internal_thread(name):
pyd_tid = xthread['id']
try:
vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=False)
@ -1880,7 +2001,7 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel):
name = unquote(xml.thread['name'])
except KeyError:
name = None
if not self.is_debugger_internal_thread(name):
if not is_debugger_internal_thread(name):
# Any internal pydevd or ptvsd threads will be ignored everywhere
tid = self.thread_map.to_vscode(xml.thread['id'], autogen=True)
self.send_event('thread', reason='started', threadId=tid)

View file

@ -189,6 +189,8 @@ class EasyDebugClient(DebugClient):
argv = [
filename,
] + list(argv)
if kwargs.pop('nodebug', False):
argv.insert(0, '--nodebug')
self._launch(argv, **kwargs)
return self._adapter, self._session
@ -202,5 +204,7 @@ class EasyDebugClient(DebugClient):
argv = [
'-m', module,
] + list(argv)
if kwargs.pop('nodebug', False):
argv.insert(0, '--nodebug')
self._launch(argv, **kwargs)
return self._adapter, self._session

View file

@ -471,3 +471,40 @@ class LifecycleTests(TestsBase, unittest.TestCase):
self.new_event('exited', exitCode=0),
self.new_event('terminated'),
])
def test_nodebug(self):
lockfile = self.workspace.lockfile()
done, waitscript = lockfile.wait_in_script()
filename = self.write_script('spam.py', dedent("""
print('+ before')
{}
print('+ after')
""").format(waitscript))
with DebugClient(port=9876) as editor:
adapter, session = editor.host_local_debugger(
argv=[
'--nodebug',
filename,
],
)
(req_initialize, req_launch, req_config
) = lifecycle_handshake(session, 'launch')
done()
adapter.wait()
self.assert_received(session.received, [
self.new_version_event(session.received),
self.new_response(req_initialize, **INITIALIZE_RESPONSE),
self.new_event('initialized'),
self.new_response(req_launch),
self.new_response(req_config),
self.new_event('output',
output='+ before\n+ after\n',
category='stdout'),
self.new_event('exited', exitCode=0),
self.new_event('terminated'),
])