mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
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:
parent
d635e22d33
commit
d498fd3582
7 changed files with 547 additions and 559 deletions
140
ptvsd/daemon.py
140
ptvsd/daemon.py
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
288
ptvsd/runner.py
288
ptvsd/runner.py
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
573
ptvsd/wrapper.py
573
ptvsd/wrapper.py
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue