From 12f91361dab2692f1cff787833ea099f68fc4baf Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 3 Apr 2018 01:09:28 +0000 Subject: [PATCH 1/9] Factor out Daemon. --- ptvsd/__main__.py | 4 +- ptvsd/wrapper.py | 320 ++++++++++++++++++++++---------- tests/helpers/pydevd/_binder.py | 35 ++-- 3 files changed, 242 insertions(+), 117 deletions(-) diff --git a/ptvsd/__main__.py b/ptvsd/__main__.py index f3d97fd51..76955b21e 100644 --- a/ptvsd/__main__.py +++ b/ptvsd/__main__.py @@ -80,12 +80,12 @@ def _run(argv, _pydevd=pydevd, _install=ptvsd.wrapper.install, **kwargs): sys.modules['__main___orig'] = sys.modules['__main__'] sys.modules['__main__'] = _pydevd - _install(_pydevd, **kwargs) + daemon = _install(_pydevd, **kwargs) sys.argv[:] = argv try: _pydevd.main() except SystemExit as ex: - ptvsd.wrapper.ptvsd_sys_exit_code = int(ex.code) + daemon.exitcode = int(ex.code) raise diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py index 95803b234..b466d42e0 100644 --- a/ptvsd/wrapper.py +++ b/ptvsd/wrapper.py @@ -49,7 +49,6 @@ # print(s) #ipcjson._TRACE = ipcjson_trace -ptvsd_sys_exit_code = 0 WAIT_FOR_DISCONNECT_REQUEST_TIMEOUT = 2 WAIT_FOR_THREAD_FINISH_TIMEOUT = 1 @@ -243,11 +242,13 @@ class PydevdSocket(object): awaited. """ - _vscprocessor = None - - def __init__(self, event_handler): + def __init__(self, handle_msg, handle_close, getpeername, getsockname): #self.log = open('pydevd.log', 'w') - self.event_handler = event_handler + 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() @@ -282,27 +283,21 @@ def close(self): except OSError as exc: if exc.errno != errno.EBADF: raise - if self._vscprocessor is not None: - proc = self._vscprocessor - self._vscprocessor = None - proc.close() + 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.""" - if self._vscprocessor is None: - raise NotImplementedError - return self._vscprocessor.socket.getpeername() + return self._getpeername() def getsockname(self): """Return the socket's own address.""" - if self._vscprocessor is None: - raise NotImplementedError - return self._vscprocessor.socket.getsockname() + return self._getsockname() def recv(self, count): """Return the requested number of bytes. @@ -352,7 +347,7 @@ def send(self, data): with self.lock: loop, fut = self.requests.pop(seq, (None, None)) if fut is None: - self.event_handler(cmd_id, seq, args) + self._handle_msg(cmd_id, seq, args) else: loop.call_soon_threadsafe(fut.set_result, (cmd_id, seq, args)) return result @@ -615,15 +610,23 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): protocol. """ - def __init__(self, socket, pydevd, logfile=None, - killonclose=True, - waitonexitfunc=_wait_on_exit): + def __init__(self, socket, pydevd, notify_disconnecting, notify_closing, + logfile=None, + ): super(VSCodeMessageProcessor, self).__init__(socket=socket, own_socket=False, logfile=logfile) self.socket = socket self.pydevd = pydevd - self.killonclose = killonclose + 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 + + # debugger state self.is_process_created = False self.is_process_created_lock = threading.Lock() self.stack_traces = {} @@ -635,27 +638,41 @@ def __init__(self, socket, pydevd, logfile=None, self.var_map = IDMap() self.bp_map = IDMap() self.next_var_ref = 0 - self.loop = futures.EventLoop() self.exceptions_mgr = ExceptionsManager(self) self.modules_mgr = ModulesManager(self) + + # adapter state self.disconnect_request = None self.debug_options = {} self.disconnect_request_event = threading.Event() - pydevd._vscprocessor = self - self._closed = False self._exited = False self.path_casing = PathUnNormcase() - self.event_loop_thread = threading.Thread(target=self.loop.run_forever, - name='ptvsd.EventLoop') + + 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.daemon = True self.event_loop_thread.start() - self.wait_on_exit_func = waitonexitfunc + # VSC msg processing loop + self.server_thread = threading.Thread( + target=self.process_messages, + name=threadname, + ) + self.server_thread.daemon = True + self.server_thread.start() + + # special initialization self.send_event( 'output', category='telemetry', output='ptvsd', - data={'version': __version__}) + data={'version': __version__}, + ) # closing the adapter @@ -665,19 +682,10 @@ def close(self): return self._closed = True - # Stop the PyDevd message handler first. - self._stop_pydevd_message_loop() - # Treat PyDevd as effectively exited. - self._handle_pydevd_stopped() + self._notify_closing() # Close the editor-side socket. self._stop_vsc_message_loop() - def _stop_pydevd_message_loop(self): - pydevd = self.pydevd - self.pydevd = None - pydevd.shutdown(socket.SHUT_RDWR) - pydevd.close() - def _stop_vsc_message_loop(self): self.set_exit() self.loop.stop() @@ -687,26 +695,22 @@ def _stop_vsc_message_loop(self): self.socket.shutdown(socket.SHUT_RDWR) self.socket.close() except Exception: + # TODO: log the error pass - def _handle_pydevd_stopped(self): - wait_on_normal_exit = self.debug_options.get( - 'WAIT_ON_NORMAL_EXIT', False) - wait_on_abnormal_exit = self.debug_options.get( - 'WAIT_ON_ABNORMAL_EXIT', False) - - if (wait_on_normal_exit and not ptvsd_sys_exit_code) \ - or (wait_on_abnormal_exit and ptvsd_sys_exit_code): - self.wait_on_exit_func() - else: - pass + 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 + def handle_pydevd_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=ptvsd_sys_exit_code) + self.send_event('exited', exitCode=exitcode) # Notify the editor that the debugger has stopped. self.send_event('terminated') @@ -726,11 +730,9 @@ def _wait_for_disconnect(self, timeout=None): def _handle_disconnect(self, request): self.disconnect_request = request self.disconnect_request_event.set() - killProcess = not self._closed - self.close() - # TODO: Move killing the process to close()? - if killProcess and self.killonclose: - os.kill(os.getpid(), signal.SIGTERM) + self._notify_disconnecting(not self._closed) + if not self._closed: + self.close() # async helpers @@ -1693,69 +1695,178 @@ def on_pydevd_cmd_write_to_console2(self, seq, args): self.send_event('output', category=category, output=content) -######################## -# lifecycle +class Daemon(object): -def _create_server(port): - server = _new_sock() - server.bind(('127.0.0.1', port)) - server.listen(1) - return server + exitcode = 0 + def __init__(self, wait_on_exit=_wait_on_exit, + addhandlers=True, killonclose=True): + self.wait_on_exit = wait_on_exit + self.addhandlers = addhandlers + self.killonclose = killonclose -def _create_client(): - return _new_sock() + self._closed = False + self._pydevd = None + self._server = None + self._client = None + self._adapter = None -def _new_sock(): - sock = socket.socket(socket.AF_INET, - socket.SOCK_STREAM, - socket.IPPROTO_TCP) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - return sock + @property + def pydevd(self): + return self._pydevd + + @property + def server(self): + return self._server + + @property + def client(self): + return self._client + + @property + def adapter(self): + return self._adapter + + def start(self, server=None): + if self._closed: + raise RuntimeError('closed') + if self._pydevd is not None: + raise RuntimeError('already started') + self._pydevd = PydevdSocket( + self._handle_pydevd_message, + self._handle_pydevd_close, + self._getpeername, + self._getsockname, + ) + self._server = server + return self._pydevd + def set_connection(self, client): + if self._closed: + raise RuntimeError('closed') + if self._pydevd is None: + raise RuntimeError('not started yet') + if self._client is not None: + raise RuntimeError('connection already set') + self._client = client + + self._adapter = VSCodeMessageProcessor( + client, + self._pydevd, + self._handle_vsc_disconnect, + self._handle_vsc_close, + ) + name = 'ptvsd.Client' if self._server is None else 'ptvsd.Server' + self._adapter.start(name) + if self.addhandlers: + self._add_atexit_handler() + self._set_signal_handlers() + return self._adapter -def _start(client, server, killonclose=True, addhandlers=True): - name = 'ptvsd.Client' if server is None else 'ptvsd.Server' + def close(self): + if self._closed: + raise RuntimeError('already closed') + self._closed = True - pydevd = PydevdSocket(lambda *args: proc.on_pydevd_event(*args)) - proc = VSCodeMessageProcessor(client, pydevd, - killonclose=killonclose) + if self._adapter is not None: + normal, abnormal = self._adapter._wait_options() + if (normal and not self.exitcode) or (abnormal and self.exitcode): + self.wait_on_exit_func() + + if self._pydevd is not None: + self._pydevd.shutdown(socket.SHUT_RDWR) + self._pydevd.close() + if self._client is not None: + self._release_connection() + + # internal methods + + def _add_atexit_handler(self): + def handler(): + if not self._closed: + self.close() + if self._adapter.server_thread.is_alive(): + self._adapter.server_thread.join( + WAIT_FOR_THREAD_FINISH_TIMEOUT) + atexit.register(handler) + + def _set_signal_handlers(self): + if platform.system() == 'Windows': + return None - server_thread = threading.Thread(target=proc.process_messages, - name=name) - server_thread.daemon = True - server_thread.start() + def handler(signum, frame): + if not self._closed: + self.close() + sys.exit(0) + signal.signal(signal.SIGHUP, handler) - if addhandlers: - _add_atexit_handler(proc, server_thread) - _set_signal_handlers(proc) + def _release_connection(self): + if self._adapter is not None: + # TODO: This is not correct in the "attach" case. + self._adapter.handle_pydevd_stopped(self.exitcode) + self._adapter.close() + self._client.shutdown(socket.SHUT_RDWR) + self._client.close() - return pydevd + # internal methods for PyDevdSocket(). + + def _handle_pydevd_message(self, cmdid, seq, text): + if self._adapter is not None: + self._adapter.on_pydevd_event(cmdid, seq, text) + + def _handle_pydevd_close(self): + if self._closed: + return + self.close() + def _getpeername(self): + if self._client is None: + raise NotImplementedError + return self._client.getpeername() -def _add_atexit_handler(proc, server_thread): - def handler(): - proc.close() - if server_thread.is_alive(): - server_thread.join(WAIT_FOR_THREAD_FINISH_TIMEOUT) - atexit.register(handler) + def _getsockname(self): + if self._client is None: + raise NotImplementedError + return self._client.getsockname() + # internal methods for VSCodeMessageProcessor -def _set_signal_handlers(proc): - if platform.system() == 'Windows': - return None + def _handle_vsc_disconnect(self, kill=False): + if not self._closed: + self.close() + if kill and self.killonclose: + os.kill(os.getpid(), signal.SIGTERM) - def handler(signum, frame): - proc.close() - sys.exit(0) - signal.signal(signal.SIGHUP, handler) + def _handle_vsc_close(self): + if self._closed: + return + self.close() ######################## # pydevd hooks -def start_server(port, addhandlers=True): +def _create_server(port): + server = _new_sock() + server.bind(('127.0.0.1', port)) + server.listen(1) + return server + + +def _create_client(): + return _new_sock() + + +def _new_sock(): + sock = socket.socket(socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + return sock + + +def start_server(daemon, port): """Return a socket to a (new) local pydevd-handling daemon. The daemon supports the pydevd client wire protocol, sending @@ -1765,11 +1876,13 @@ def start_server(port, addhandlers=True): """ server = _create_server(port) client, _ = server.accept() - pydevd = _start(client, server) + + pydevd = daemon.start(server) + daemon.set_connection(client) return pydevd -def start_client(host, port, addhandlers=True): +def start_client(daemon, host, port): """Return a socket to an existing "remote" pydevd-handling daemon. The daemon supports the pydevd client wire protocol, sending @@ -1779,7 +1892,9 @@ def start_client(host, port, addhandlers=True): """ client = _create_client() client.connect((host, port)) - pydevd = _start(client, None) + + pydevd = daemon.start() + daemon.set_connection(client) return pydevd @@ -1791,9 +1906,11 @@ def install(pydevd, start_server=start_server, start_client=start_client): this is somewhat fragile (since the monkeypatching sites may change). """ + daemon = Daemon() + # 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 + pydevd_comm.start_server = (lambda p: start_server(daemon, p)) + pydevd_comm.start_client = (lambda h, p: start_client(daemon, h, p)) # Ensure that pydevd is using our functions. pydevd.start_server = start_server @@ -1802,3 +1919,4 @@ def install(pydevd, start_server=start_server, start_client=start_client): if __main__ is not pydevd and __main__.__file__ == pydevd.__file__: __main__.start_server = start_server __main__.start_client = start_client + return daemon diff --git a/tests/helpers/pydevd/_binder.py b/tests/helpers/pydevd/_binder.py index ff0ec68fc..85784fd6e 100644 --- a/tests/helpers/pydevd/_binder.py +++ b/tests/helpers/pydevd/_binder.py @@ -1,4 +1,3 @@ -from collections import namedtuple import threading import time @@ -6,7 +5,7 @@ from tests.helpers import socket -class PTVSD(namedtuple('PTVSD', 'client server proc fakesock')): +class PTVSD(wrapper.Daemon): """A wrapper around a running "instance" of PTVSD. "client" and "server" are the two ends of socket that PTVSD uses @@ -20,16 +19,24 @@ class PTVSD(namedtuple('PTVSD', 'client server proc fakesock')): @classmethod def from_connect_func(cls, connect): """Return a new instance using the socket returned by connect().""" - client, server = connect() - fakesock = wrapper._start( - client, - server, - killonclose=False, + self = cls( + wait_on_exit=(lambda: None), addhandlers=False, + killonclose=False, ) - proc = fakesock._vscprocessor - proc._exit_on_unknown_command = False - return cls(client, server, proc, fakesock) + client, server = connect() + self.start(server) + proc = self.set_connection(client) + proc._exit_on_unknown_command = False # TODO: hack alert! + return self + + @property + def fakesock(self): + return self.pydevd + + @property + def proc(self): + return self.adapter def close(self): """Stop PTVSD and clean up. @@ -40,7 +47,7 @@ def close(self): the editor (e.g. a "disconnect" request). PTVSD also closes all of its background threads and closes any sockets it controls. """ - self.proc.close() + super(PTVSD, self).close() class BinderBase(object): @@ -141,10 +148,10 @@ def _run(self): try: self._run_debugger() except SystemExit as exc: - wrapper.ptvsd_sys_exit_code = int(exc.code) + self.ptvsd.exitcode = int(exc.code) raise - wrapper.ptvsd_sys_exit_code = 0 - self.ptvsd.proc.close() + self.ptvsd.exitcode = 0 + self.ptvsd.close() class Binder(BinderBase): From c9350ce254e933f2112a3863463449ef3fd5beb7 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 3 Apr 2018 01:23:28 +0000 Subject: [PATCH 2/9] Move Daemon to its own module. --- ptvsd/__main__.py | 4 +- ptvsd/daemon.py | 251 ++++++++++++++++++++++++++++++++ ptvsd/wrapper.py | 243 ------------------------------- tests/helpers/pydevd/_binder.py | 4 +- tests/helpers/socket.py | 2 +- 5 files changed, 256 insertions(+), 248 deletions(-) create mode 100644 ptvsd/daemon.py diff --git a/ptvsd/__main__.py b/ptvsd/__main__.py index 76955b21e..d9c178932 100644 --- a/ptvsd/__main__.py +++ b/ptvsd/__main__.py @@ -8,7 +8,7 @@ import pydevd -import ptvsd.wrapper +import ptvsd.daemon __author__ = "Microsoft Corporation " @@ -58,7 +58,7 @@ def _run_argv(address, filename, extra, _prog=sys.argv[0]): ] + extra -def _run(argv, _pydevd=pydevd, _install=ptvsd.wrapper.install, **kwargs): +def _run(argv, _pydevd=pydevd, _install=ptvsd.daemon.install, **kwargs): """Start pydevd with the given commandline args.""" #print(' '.join(argv)) diff --git a/ptvsd/daemon.py b/ptvsd/daemon.py new file mode 100644 index 000000000..f59d17bd9 --- /dev/null +++ b/ptvsd/daemon.py @@ -0,0 +1,251 @@ +import atexit +import os +import platform +import signal +import socket +import sys + +from _pydevd_bundle import pydevd_comm + +from ptvsd import wrapper + + +def _wait_on_exit(): + 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 Daemon(object): + + exitcode = 0 + + def __init__(self, wait_on_exit=_wait_on_exit, + addhandlers=True, killonclose=True): + self.wait_on_exit = wait_on_exit + self.addhandlers = addhandlers + self.killonclose = killonclose + + self._closed = False + + self._pydevd = None + self._server = None + self._client = None + self._adapter = None + + @property + def pydevd(self): + return self._pydevd + + @property + def server(self): + return self._server + + @property + def client(self): + return self._client + + @property + def adapter(self): + return self._adapter + + def start(self, server=None): + if self._closed: + raise RuntimeError('closed') + if self._pydevd is not None: + raise RuntimeError('already started') + self._pydevd = wrapper.PydevdSocket( + self._handle_pydevd_message, + self._handle_pydevd_close, + self._getpeername, + self._getsockname, + ) + self._server = server + return self._pydevd + + def set_connection(self, client): + if self._closed: + raise RuntimeError('closed') + if self._pydevd is None: + raise RuntimeError('not started yet') + if self._client is not None: + raise RuntimeError('connection already set') + self._client = client + + self._adapter = wrapper.VSCodeMessageProcessor( + client, + self._pydevd, + self._handle_vsc_disconnect, + self._handle_vsc_close, + ) + name = 'ptvsd.Client' if self._server is None else 'ptvsd.Server' + self._adapter.start(name) + if self.addhandlers: + self._add_atexit_handler() + self._set_signal_handlers() + return self._adapter + + def close(self): + if self._closed: + raise RuntimeError('already closed') + self._closed = True + + if self._adapter is not None: + normal, abnormal = self._adapter._wait_options() + if (normal and not self.exitcode) or (abnormal and self.exitcode): + self.wait_on_exit_func() + + if self._pydevd is not None: + self._pydevd.shutdown(socket.SHUT_RDWR) + self._pydevd.close() + if self._client is not None: + self._release_connection() + + # internal methods + + def _add_atexit_handler(self): + def handler(): + if not self._closed: + self.close() + if self._adapter.server_thread.is_alive(): + self._adapter.server_thread.join( + wrapper.WAIT_FOR_THREAD_FINISH_TIMEOUT) + 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: + # TODO: This is not correct in the "attach" case. + self._adapter.handle_pydevd_stopped(self.exitcode) + self._adapter.close() + self._client.shutdown(socket.SHUT_RDWR) + self._client.close() + + # internal methods for PyDevdSocket(). + + def _handle_pydevd_message(self, cmdid, seq, text): + if self._adapter is not None: + self._adapter.on_pydevd_event(cmdid, seq, text) + + def _handle_pydevd_close(self): + if self._closed: + return + self.close() + + def _getpeername(self): + if self._client is None: + raise NotImplementedError + return self._client.getpeername() + + def _getsockname(self): + if self._client is None: + raise NotImplementedError + return self._client.getsockname() + + # internal methods for VSCodeMessageProcessor + + def _handle_vsc_disconnect(self, kill=False): + if not self._closed: + self.close() + if kill and self.killonclose: + os.kill(os.getpid(), signal.SIGTERM) + + def _handle_vsc_close(self): + if self._closed: + return + self.close() + + +######################## +# pydevd hooks + +def _create_server(port): + server = _new_sock() + server.bind(('127.0.0.1', port)) + server.listen(1) + return server + + +def _create_client(): + return _new_sock() + + +def _new_sock(): + sock = socket.socket(socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + return sock + + +def start_server(daemon, port): + """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. + """ + server = _create_server(port) + client, _ = server.accept() + + pydevd = daemon.start(server) + daemon.set_connection(client) + return pydevd + + +def start_client(daemon, host, port): + """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. + """ + client = _create_client() + client.connect((host, port)) + + pydevd = daemon.start() + daemon.set_connection(client) + return pydevd + + +def install(pydevd, start_server=start_server, start_client=start_client): + """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). + """ + daemon = Daemon() + + # These are the functions pydevd invokes to get a socket to the client. + pydevd_comm.start_server = (lambda p: start_server(daemon, p)) + pydevd_comm.start_client = (lambda h, p: start_client(daemon, h, p)) + + # Ensure that pydevd is using our functions. + pydevd.start_server = start_server + pydevd.start_client = start_client + __main__ = sys.modules['__main__'] + if __main__ is not pydevd and __main__.__file__ == pydevd.__file__: + __main__.start_server = start_server + __main__.start_client = start_client + return daemon diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py index b466d42e0..a647cdae4 100644 --- a/ptvsd/wrapper.py +++ b/ptvsd/wrapper.py @@ -4,14 +4,12 @@ from __future__ import print_function, absolute_import -import atexit import contextlib import errno import io import os import platform import re -import signal import socket import sys import threading @@ -53,20 +51,6 @@ WAIT_FOR_THREAD_FINISH_TIMEOUT = 1 -def _wait_on_exit(): - 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 SafeReprPresentationProvider(pydevd_extapi.StrPresentationProvider): """ Computes string representation of Python values by delegating them @@ -1693,230 +1677,3 @@ def on_pydevd_cmd_write_to_console2(self, seq, args): category = 'stdout' if ctx == '1' else 'stderr' content = unquote(xml.io['s']) self.send_event('output', category=category, output=content) - - -class Daemon(object): - - exitcode = 0 - - def __init__(self, wait_on_exit=_wait_on_exit, - addhandlers=True, killonclose=True): - self.wait_on_exit = wait_on_exit - self.addhandlers = addhandlers - self.killonclose = killonclose - - self._closed = False - - self._pydevd = None - self._server = None - self._client = None - self._adapter = None - - @property - def pydevd(self): - return self._pydevd - - @property - def server(self): - return self._server - - @property - def client(self): - return self._client - - @property - def adapter(self): - return self._adapter - - def start(self, server=None): - if self._closed: - raise RuntimeError('closed') - if self._pydevd is not None: - raise RuntimeError('already started') - self._pydevd = PydevdSocket( - self._handle_pydevd_message, - self._handle_pydevd_close, - self._getpeername, - self._getsockname, - ) - self._server = server - return self._pydevd - - def set_connection(self, client): - if self._closed: - raise RuntimeError('closed') - if self._pydevd is None: - raise RuntimeError('not started yet') - if self._client is not None: - raise RuntimeError('connection already set') - self._client = client - - self._adapter = VSCodeMessageProcessor( - client, - self._pydevd, - self._handle_vsc_disconnect, - self._handle_vsc_close, - ) - name = 'ptvsd.Client' if self._server is None else 'ptvsd.Server' - self._adapter.start(name) - if self.addhandlers: - self._add_atexit_handler() - self._set_signal_handlers() - return self._adapter - - def close(self): - if self._closed: - raise RuntimeError('already closed') - self._closed = True - - if self._adapter is not None: - normal, abnormal = self._adapter._wait_options() - if (normal and not self.exitcode) or (abnormal and self.exitcode): - self.wait_on_exit_func() - - if self._pydevd is not None: - self._pydevd.shutdown(socket.SHUT_RDWR) - self._pydevd.close() - if self._client is not None: - self._release_connection() - - # internal methods - - def _add_atexit_handler(self): - def handler(): - if not self._closed: - self.close() - if self._adapter.server_thread.is_alive(): - self._adapter.server_thread.join( - WAIT_FOR_THREAD_FINISH_TIMEOUT) - 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: - # TODO: This is not correct in the "attach" case. - self._adapter.handle_pydevd_stopped(self.exitcode) - self._adapter.close() - self._client.shutdown(socket.SHUT_RDWR) - self._client.close() - - # internal methods for PyDevdSocket(). - - def _handle_pydevd_message(self, cmdid, seq, text): - if self._adapter is not None: - self._adapter.on_pydevd_event(cmdid, seq, text) - - def _handle_pydevd_close(self): - if self._closed: - return - self.close() - - def _getpeername(self): - if self._client is None: - raise NotImplementedError - return self._client.getpeername() - - def _getsockname(self): - if self._client is None: - raise NotImplementedError - return self._client.getsockname() - - # internal methods for VSCodeMessageProcessor - - def _handle_vsc_disconnect(self, kill=False): - if not self._closed: - self.close() - if kill and self.killonclose: - os.kill(os.getpid(), signal.SIGTERM) - - def _handle_vsc_close(self): - if self._closed: - return - self.close() - - -######################## -# pydevd hooks - -def _create_server(port): - server = _new_sock() - server.bind(('127.0.0.1', port)) - server.listen(1) - return server - - -def _create_client(): - return _new_sock() - - -def _new_sock(): - sock = socket.socket(socket.AF_INET, - socket.SOCK_STREAM, - socket.IPPROTO_TCP) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - return sock - - -def start_server(daemon, port): - """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. - """ - server = _create_server(port) - client, _ = server.accept() - - pydevd = daemon.start(server) - daemon.set_connection(client) - return pydevd - - -def start_client(daemon, host, port): - """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. - """ - client = _create_client() - client.connect((host, port)) - - pydevd = daemon.start() - daemon.set_connection(client) - return pydevd - - -def install(pydevd, start_server=start_server, start_client=start_client): - """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). - """ - daemon = Daemon() - - # These are the functions pydevd invokes to get a socket to the client. - pydevd_comm.start_server = (lambda p: start_server(daemon, p)) - pydevd_comm.start_client = (lambda h, p: start_client(daemon, h, p)) - - # Ensure that pydevd is using our functions. - pydevd.start_server = start_server - pydevd.start_client = start_client - __main__ = sys.modules['__main__'] - if __main__ is not pydevd and __main__.__file__ == pydevd.__file__: - __main__.start_server = start_server - __main__.start_client = start_client - return daemon diff --git a/tests/helpers/pydevd/_binder.py b/tests/helpers/pydevd/_binder.py index 85784fd6e..8154e212c 100644 --- a/tests/helpers/pydevd/_binder.py +++ b/tests/helpers/pydevd/_binder.py @@ -1,11 +1,11 @@ import threading import time -from ptvsd import wrapper +import ptvsd.daemon from tests.helpers import socket -class PTVSD(wrapper.Daemon): +class PTVSD(ptvsd.daemon.Daemon): """A wrapper around a running "instance" of PTVSD. "client" and "server" are the two ends of socket that PTVSD uses diff --git a/tests/helpers/socket.py b/tests/helpers/socket.py index 5092e20b1..371d0b3e8 100644 --- a/tests/helpers/socket.py +++ b/tests/helpers/socket.py @@ -4,7 +4,7 @@ import errno import socket -import ptvsd.wrapper as _ptvsd +import ptvsd.daemon as _ptvsd def create_server(address): From 95ce9ed3b42d30128aa808b592f4da26cf2bf6fd Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 3 Apr 2018 01:35:44 +0000 Subject: [PATCH 3/9] Avoid multiple close attempts. --- ptvsd/daemon.py | 18 ++++++++++++++---- tests/helpers/pydevd/_binder.py | 5 ++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/ptvsd/daemon.py b/ptvsd/daemon.py index f59d17bd9..92855abd2 100644 --- a/ptvsd/daemon.py +++ b/ptvsd/daemon.py @@ -1,4 +1,5 @@ import atexit +import errno import os import platform import signal @@ -24,6 +25,11 @@ def _wait_on_exit(): msvcrt.getch() +class DaemonClosedError(RuntimeError): + def __init__(self, msg='closed'): + super(DaemonClosedError, self).__init__(msg) + + class Daemon(object): exitcode = 0 @@ -59,7 +65,7 @@ def adapter(self): def start(self, server=None): if self._closed: - raise RuntimeError('closed') + raise DaemonClosedError() if self._pydevd is not None: raise RuntimeError('already started') self._pydevd = wrapper.PydevdSocket( @@ -73,7 +79,7 @@ def start(self, server=None): def set_connection(self, client): if self._closed: - raise RuntimeError('closed') + raise DaemonClosedError() if self._pydevd is None: raise RuntimeError('not started yet') if self._client is not None: @@ -95,7 +101,7 @@ def set_connection(self, client): def close(self): if self._closed: - raise RuntimeError('already closed') + raise DaemonClosedError('already closed') self._closed = True if self._adapter is not None: @@ -135,7 +141,11 @@ def _release_connection(self): # TODO: This is not correct in the "attach" case. self._adapter.handle_pydevd_stopped(self.exitcode) self._adapter.close() - self._client.shutdown(socket.SHUT_RDWR) + try: + self._client.shutdown(socket.SHUT_RDWR) + except OSError as exc: + if exc.errno not in (errno.ENOTCONN, errno.EBADF): + raise self._client.close() # internal methods for PyDevdSocket(). diff --git a/tests/helpers/pydevd/_binder.py b/tests/helpers/pydevd/_binder.py index 8154e212c..a5e98ef23 100644 --- a/tests/helpers/pydevd/_binder.py +++ b/tests/helpers/pydevd/_binder.py @@ -47,7 +47,10 @@ def close(self): the editor (e.g. a "disconnect" request). PTVSD also closes all of its background threads and closes any sockets it controls. """ - super(PTVSD, self).close() + try: + super(PTVSD, self).close() + except ptvsd.daemon.DaemonClosedError: + pass class BinderBase(object): From ca335238d3e8ad89636d555834a70dde33bb8cc5 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 3 Apr 2018 01:42:08 +0000 Subject: [PATCH 4/9] Add docstrings. --- ptvsd/daemon.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ptvsd/daemon.py b/ptvsd/daemon.py index 92855abd2..66d56d944 100644 --- a/ptvsd/daemon.py +++ b/ptvsd/daemon.py @@ -26,11 +26,13 @@ def _wait_on_exit(): class DaemonClosedError(RuntimeError): + """Indicates that a Daemon was unexpectedly closed.""" def __init__(self, msg='closed'): super(DaemonClosedError, self).__init__(msg) class Daemon(object): + """The process-level manager for the VSC protocol debug adapter.""" exitcode = 0 @@ -64,6 +66,7 @@ def adapter(self): return self._adapter def start(self, server=None): + """Return the "socket" to use for pydevd after setting it up.""" if self._closed: raise DaemonClosedError() if self._pydevd is not None: @@ -78,6 +81,10 @@ def start(self, server=None): return self._pydevd def set_connection(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._pydevd is None: @@ -100,6 +107,7 @@ def set_connection(self, client): return self._adapter def close(self): + """Stop all loops and release all resources.""" if self._closed: raise DaemonClosedError('already closed') self._closed = True From 859dc0ee0c2eb0d0e6ee85a99b9a0e83963683cd Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 3 Apr 2018 01:46:48 +0000 Subject: [PATCH 5/9] Wait for the adapter to finish only if there is one. --- ptvsd/daemon.py | 6 +++--- ptvsd/wrapper.py | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/ptvsd/daemon.py b/ptvsd/daemon.py index 66d56d944..8ed424e0c 100644 --- a/ptvsd/daemon.py +++ b/ptvsd/daemon.py @@ -129,9 +129,9 @@ def _add_atexit_handler(self): def handler(): if not self._closed: self.close() - if self._adapter.server_thread.is_alive(): - self._adapter.server_thread.join( - wrapper.WAIT_FOR_THREAD_FINISH_TIMEOUT) + if self._adapter is not None: + # TODO: Do this in VSCodeMessageProcessor.close()? + self._adapter._wait_for_server_thread() atexit.register(handler) def _set_signal_handlers(self): diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py index a647cdae4..24d6288f0 100644 --- a/ptvsd/wrapper.py +++ b/ptvsd/wrapper.py @@ -718,6 +718,13 @@ def _handle_disconnect(self, request): if not self._closed: self.close() + 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) + # async helpers def async_method(m): From 1d8a948dc820107ae83c2b46b02aae94b05dac59 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 3 Apr 2018 02:16:47 +0000 Subject: [PATCH 6/9] Move the pydevd hooks to their own module. --- ptvsd/__main__.py | 4 +- ptvsd/daemon.py | 93 ++--------------------------------------- ptvsd/pydevd_hooks.py | 62 +++++++++++++++++++++++++++ ptvsd/socket.py | 57 +++++++++++++++++++++++++ tests/helpers/socket.py | 26 +++--------- 5 files changed, 130 insertions(+), 112 deletions(-) create mode 100644 ptvsd/pydevd_hooks.py create mode 100644 ptvsd/socket.py diff --git a/ptvsd/__main__.py b/ptvsd/__main__.py index d9c178932..51ed31ee7 100644 --- a/ptvsd/__main__.py +++ b/ptvsd/__main__.py @@ -8,7 +8,7 @@ import pydevd -import ptvsd.daemon +from ptvsd.pydevd_hooks import install __author__ = "Microsoft Corporation " @@ -58,7 +58,7 @@ def _run_argv(address, filename, extra, _prog=sys.argv[0]): ] + extra -def _run(argv, _pydevd=pydevd, _install=ptvsd.daemon.install, **kwargs): +def _run(argv, _pydevd=pydevd, _install=install, **kwargs): """Start pydevd with the given commandline args.""" #print(' '.join(argv)) diff --git a/ptvsd/daemon.py b/ptvsd/daemon.py index 8ed424e0c..5c7c1deef 100644 --- a/ptvsd/daemon.py +++ b/ptvsd/daemon.py @@ -1,14 +1,11 @@ import atexit -import errno import os import platform import signal -import socket import sys -from _pydevd_bundle import pydevd_comm - from ptvsd import wrapper +from ptvsd.socket import close_socket def _wait_on_exit(): @@ -118,8 +115,7 @@ def close(self): self.wait_on_exit_func() if self._pydevd is not None: - self._pydevd.shutdown(socket.SHUT_RDWR) - self._pydevd.close() + close_socket(self._pydevd) if self._client is not None: self._release_connection() @@ -149,12 +145,7 @@ def _release_connection(self): # TODO: This is not correct in the "attach" case. self._adapter.handle_pydevd_stopped(self.exitcode) self._adapter.close() - try: - self._client.shutdown(socket.SHUT_RDWR) - except OSError as exc: - if exc.errno not in (errno.ENOTCONN, errno.EBADF): - raise - self._client.close() + close_socket(self._client) # internal methods for PyDevdSocket(). @@ -189,81 +180,3 @@ def _handle_vsc_close(self): if self._closed: return self.close() - - -######################## -# pydevd hooks - -def _create_server(port): - server = _new_sock() - server.bind(('127.0.0.1', port)) - server.listen(1) - return server - - -def _create_client(): - return _new_sock() - - -def _new_sock(): - sock = socket.socket(socket.AF_INET, - socket.SOCK_STREAM, - socket.IPPROTO_TCP) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - return sock - - -def start_server(daemon, port): - """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. - """ - server = _create_server(port) - client, _ = server.accept() - - pydevd = daemon.start(server) - daemon.set_connection(client) - return pydevd - - -def start_client(daemon, host, port): - """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. - """ - client = _create_client() - client.connect((host, port)) - - pydevd = daemon.start() - daemon.set_connection(client) - return pydevd - - -def install(pydevd, start_server=start_server, start_client=start_client): - """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). - """ - daemon = Daemon() - - # These are the functions pydevd invokes to get a socket to the client. - pydevd_comm.start_server = (lambda p: start_server(daemon, p)) - pydevd_comm.start_client = (lambda h, p: start_client(daemon, h, p)) - - # Ensure that pydevd is using our functions. - pydevd.start_server = start_server - pydevd.start_client = start_client - __main__ = sys.modules['__main__'] - if __main__ is not pydevd and __main__.__file__ == pydevd.__file__: - __main__.start_server = start_server - __main__.start_client = start_client - return daemon diff --git a/ptvsd/pydevd_hooks.py b/ptvsd/pydevd_hooks.py new file mode 100644 index 000000000..2bfec6b7b --- /dev/null +++ b/ptvsd/pydevd_hooks.py @@ -0,0 +1,62 @@ +import sys + +from _pydevd_bundle import pydevd_comm + +from ptvsd.socket import create_server, create_client +from ptvsd.daemon import Daemon + + +def start_server(daemon, port): + """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. + """ + server = create_server(port) + client, _ = server.accept() + + pydevd = daemon.start(server) + daemon.set_connection(client) + return pydevd + + +def start_client(daemon, host, port): + """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. + """ + client = create_client() + client.connect((host, port)) + + pydevd = daemon.start() + daemon.set_connection(client) + return pydevd + + +def install(pydevd, start_server=start_server, start_client=start_client): + """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). + """ + daemon = Daemon() + + # These are the functions pydevd invokes to get a socket to the client. + pydevd_comm.start_server = (lambda p: start_server(daemon, p)) + pydevd_comm.start_client = (lambda h, p: start_client(daemon, h, p)) + + # Ensure that pydevd is using our functions. + pydevd.start_server = start_server + pydevd.start_client = start_client + __main__ = sys.modules['__main__'] + if __main__ is not pydevd and __main__.__file__ == pydevd.__file__: + __main__.start_server = start_server + __main__.start_client = start_client + return daemon diff --git a/ptvsd/socket.py b/ptvsd/socket.py new file mode 100644 index 000000000..fa02c6b44 --- /dev/null +++ b/ptvsd/socket.py @@ -0,0 +1,57 @@ +from __future__ import absolute_import + +import contextlib +import errno +import socket + + +NOT_CONNECTED = ( + errno.ENOTCONN, + errno.EBADF, +) + + +def create_server(port): + """Return a local server socket listening on the given port.""" + server = _new_sock() + server.bind(('127.0.0.1', port)) + server.listen(1) + return server + + +def create_client(): + """Return a client socket that may be connected to a remote address.""" + return _new_sock() + + +def _new_sock(): + sock = socket.socket(socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP) + 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 + + +def shut_down(sock, how=socket.SHUT_RDWR, ignored=NOT_CONNECTED): + """Shut down the given socket.""" + with ignored_errno(*ignored or ()): + sock.shutdown(how) + + +def close_socket(sock): + """Shutdown and close the socket.""" + try: + shut_down(sock) + except Exception: + pass + sock.close() diff --git a/tests/helpers/socket.py b/tests/helpers/socket.py index 371d0b3e8..94463d230 100644 --- a/tests/helpers/socket.py +++ b/tests/helpers/socket.py @@ -1,21 +1,19 @@ from __future__ import absolute_import from collections import namedtuple -import errno -import socket -import ptvsd.daemon as _ptvsd +import ptvsd.socket as _ptvsd def create_server(address): """Return a server socket after binding.""" host, port = address - return _ptvsd._create_server(port) + return _ptvsd.create_server(port) def create_client(): """Return a new (unconnected) client socket.""" - return _ptvsd._create_client() + return _ptvsd.create_client() def connect(sock, address): @@ -57,11 +55,7 @@ def connect(): def close(sock): """Shutdown and close the socket.""" - try: - sock.shutdown(socket.SHUT_RDWR) - except Exception: - pass - sock.close() + _ptvsd.close_socket(sock) class Connection(namedtuple('Connection', 'client server')): @@ -90,16 +84,8 @@ def makefile(self, *args, **kwargs): def shutdown(self, *args, **kwargs): if self.server is not None: - try: - self.server.shutdown(*args, **kwargs) - except OSError as exc: - if exc.errno not in (errno.ENOTCONN, errno.EBADF): - raise - try: - self.client.shutdown(*args, **kwargs) - except OSError as exc: - if exc.errno not in (errno.ENOTCONN, errno.EBADF): - raise + _ptvsd.shut_down(self.server, *args, **kwargs) + _ptvsd.shut_down(self.client, *args, **kwargs) def close(self): if self.server is not None: From 7fc36290794f53ad5e420cc8e8233c1f1dd36aac Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 3 Apr 2018 02:21:12 +0000 Subject: [PATCH 7/9] Do not pass the entire PydevdSocket object. --- ptvsd/daemon.py | 3 ++- ptvsd/wrapper.py | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ptvsd/daemon.py b/ptvsd/daemon.py index 5c7c1deef..c1443b0af 100644 --- a/ptvsd/daemon.py +++ b/ptvsd/daemon.py @@ -92,7 +92,8 @@ def set_connection(self, client): self._adapter = wrapper.VSCodeMessageProcessor( client, - self._pydevd, + self._pydevd.pydevd_notify, + self._pydevd.pydevd_request, self._handle_vsc_disconnect, self._handle_vsc_close, ) diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py index 24d6288f0..b305382b8 100644 --- a/ptvsd/wrapper.py +++ b/ptvsd/wrapper.py @@ -594,14 +594,16 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): protocol. """ - def __init__(self, socket, pydevd, notify_disconnecting, notify_closing, + def __init__(self, socket, pydevd_notify, pydevd_request, + notify_disconnecting, notify_closing, logfile=None, ): super(VSCodeMessageProcessor, self).__init__(socket=socket, own_socket=False, logfile=logfile) self.socket = socket - self.pydevd = pydevd + self._pydevd_notify = pydevd_notify + self._pydevd_request = pydevd_request self._notify_disconnecting = notify_disconnecting self._notify_closing = notify_closing @@ -763,14 +765,14 @@ def sleep(self): def pydevd_notify(self, cmd_id, args): # TODO: docstring try: - return self.pydevd.pydevd_notify(cmd_id, args) + return self._pydevd_notify(cmd_id, args) except BaseException: traceback.print_exc(file=sys.__stderr__) raise def pydevd_request(self, cmd_id, args): # TODO: docstring - return self.pydevd.pydevd_request(self.loop, cmd_id, args) + return self._pydevd_request(self.loop, cmd_id, args) # Instances of this class provide decorators to mark methods as # handlers for various # pydevd messages - a decorated method is From cfa9a1e4e57718dbcf8d589a5e1d47a47a621d04 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 4 Apr 2018 15:40:51 +0000 Subject: [PATCH 8/9] Use the wrapped functions in install(). --- ptvsd/pydevd_hooks.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ptvsd/pydevd_hooks.py b/ptvsd/pydevd_hooks.py index 2bfec6b7b..f42395605 100644 --- a/ptvsd/pydevd_hooks.py +++ b/ptvsd/pydevd_hooks.py @@ -48,15 +48,18 @@ def install(pydevd, start_server=start_server, start_client=start_client): """ daemon = Daemon() + _start_server = (lambda p: start_server(daemon, p)) + _start_client = (lambda h, p: start_client(daemon, h, p)) + # These are the functions pydevd invokes to get a socket to the client. - pydevd_comm.start_server = (lambda p: start_server(daemon, p)) - pydevd_comm.start_client = (lambda h, p: start_client(daemon, h, p)) + pydevd_comm.start_server = _start_server + pydevd_comm.start_client = _start_client # Ensure that pydevd is using our functions. - pydevd.start_server = start_server - pydevd.start_client = start_client + pydevd.start_server = _start_server + pydevd.start_client = _start_client __main__ = sys.modules['__main__'] if __main__ is not pydevd and __main__.__file__ == pydevd.__file__: - __main__.start_server = start_server - __main__.start_client = start_client + __main__.start_server = _start_server + __main__.start_client = _start_client return daemon From 7026e06b55cb4d3329df42b7e35754f05f7b3232 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 4 Apr 2018 15:41:11 +0000 Subject: [PATCH 9/9] Fix the tests. --- ptvsd/pydevd_hooks.py | 2 ++ tests/ptvsd/test___main__.py | 52 ++++++++++++++++++++++-------------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/ptvsd/pydevd_hooks.py b/ptvsd/pydevd_hooks.py index f42395605..cf7b293d3 100644 --- a/ptvsd/pydevd_hooks.py +++ b/ptvsd/pydevd_hooks.py @@ -49,7 +49,9 @@ def install(pydevd, start_server=start_server, start_client=start_client): daemon = Daemon() _start_server = (lambda p: start_server(daemon, 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 diff --git a/tests/ptvsd/test___main__.py b/tests/ptvsd/test___main__.py index ece74e6ae..0744062d2 100644 --- a/tests/ptvsd/test___main__.py +++ b/tests/ptvsd/test___main__.py @@ -5,7 +5,7 @@ from _pydevd_bundle import pydevd_comm -import ptvsd.wrapper +import ptvsd.pydevd_hooks from ptvsd.__main__ import run_module, run_file, parse_args if sys.version_info < (3,): @@ -43,6 +43,10 @@ def __init__(self, __file__, handle_main): self.__file__ = __file__ self.handle_main = handle_main + @property + def __name__(self): + return object.__repr__(self) + def main(self): self.handle_main() @@ -169,30 +173,30 @@ class IntegratedRunTests(unittest.TestCase): def setUp(self): super(IntegratedRunTests, self).setUp() - self.__main__ = sys.modules['__main__'] - self.argv = sys.argv - ptvsd.wrapper.ptvsd_sys_exit_code = 0 - self.start_server = pydevd_comm.start_server - self.start_client = pydevd_comm.start_client + self.___main__ = sys.modules['__main__'] + self._argv = sys.argv + self._start_server = pydevd_comm.start_server + self._start_client = pydevd_comm.start_client self.pydevd = None self.kwargs = None self.maincalls = 0 self.mainexc = None + self.exitcode = -1 def tearDown(self): - sys.argv[:] = self.argv - sys.modules['__main__'] = self.__main__ + sys.argv[:] = self._argv + sys.modules['__main__'] = self.___main__ sys.modules.pop('__main___orig', None) - ptvsd.wrapper.ptvsd_sys_exit_code = 0 - pydevd_comm.start_server = self.start_server - pydevd_comm.start_client = self.start_client + pydevd_comm.start_server = self._start_server + pydevd_comm.start_client = self._start_client # We shouldn't need to restore __main__.start_*. super(IntegratedRunTests, self).tearDown() def _install(self, pydevd, **kwargs): self.pydevd = pydevd self.kwargs = kwargs + return self def _main(self): self.maincalls += 1 @@ -212,7 +216,7 @@ def test_run(self): '--port', '8888', '--file', 'spam.py', ]) - self.assertEqual(ptvsd.wrapper.ptvsd_sys_exit_code, 0) + self.assertEqual(self.exitcode, -1) def test_failure(self): self.mainexc = RuntimeError('boom!') @@ -230,7 +234,7 @@ def test_failure(self): '--port', '8888', '--file', 'spam.py', ]) - self.assertEqual(ptvsd.wrapper.ptvsd_sys_exit_code, 0) + self.assertEqual(self.exitcode, -1) # TODO: Is this right? self.assertIs(exc, self.mainexc) def test_exit(self): @@ -248,20 +252,28 @@ def test_exit(self): '--port', '8888', '--file', 'spam.py', ]) - self.assertEqual(ptvsd.wrapper.ptvsd_sys_exit_code, 1) + self.assertEqual(self.exitcode, 1) def test_installed(self): pydevd = FakePyDevd('pydevd/pydevd.py', self._main) addr = (None, 8888) run_file(addr, 'spam.py', _pydevd=pydevd) - self.assertIs(pydevd_comm.start_server, ptvsd.wrapper.start_server) - self.assertIs(pydevd_comm.start_client, ptvsd.wrapper.start_client) - self.assertIs(pydevd.start_server, ptvsd.wrapper.start_server) - self.assertIs(pydevd.start_client, ptvsd.wrapper.start_client) __main__ = sys.modules['__main__'] - self.assertIs(__main__.start_server, ptvsd.wrapper.start_server) - self.assertIs(__main__.start_client, ptvsd.wrapper.start_client) + expected_server = ptvsd.pydevd_hooks.start_server + expected_client = ptvsd.pydevd_hooks.start_client + for mod in (pydevd_comm, pydevd, __main__): + start_server = getattr(mod, 'start_server') + if hasattr(start_server, 'orig'): + start_server = start_server.orig + start_client = getattr(mod, 'start_client') + if hasattr(start_client, 'orig'): + start_client = start_client.orig + + self.assertIs(start_server, expected_server, + '(module {})'.format(mod.__name__)) + self.assertIs(start_client, expected_client, + '(module {})'.format(mod.__name__)) class ParseArgsTests(unittest.TestCase):