diff --git a/ptvsd/__main__.py b/ptvsd/__main__.py index f3d97fd51..51ed31ee7 100644 --- a/ptvsd/__main__.py +++ b/ptvsd/__main__.py @@ -8,7 +8,7 @@ import pydevd -import ptvsd.wrapper +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.wrapper.install, **kwargs): +def _run(argv, _pydevd=pydevd, _install=install, **kwargs): """Start pydevd with the given commandline args.""" #print(' '.join(argv)) @@ -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/daemon.py b/ptvsd/daemon.py new file mode 100644 index 000000000..c1443b0af --- /dev/null +++ b/ptvsd/daemon.py @@ -0,0 +1,183 @@ +import atexit +import os +import platform +import signal +import sys + +from ptvsd import wrapper +from ptvsd.socket import close_socket + + +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 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 + + 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): + """Return the "socket" to use for pydevd after setting it up.""" + if self._closed: + raise DaemonClosedError() + 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): + """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: + 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.pydevd_notify, + self._pydevd.pydevd_request, + 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): + """Stop all loops and release all resources.""" + if self._closed: + raise DaemonClosedError('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: + close_socket(self._pydevd) + 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 is not None: + # TODO: Do this in VSCodeMessageProcessor.close()? + 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: + # TODO: This is not correct in the "attach" case. + self._adapter.handle_pydevd_stopped(self.exitcode) + self._adapter.close() + close_socket(self._client) + + # 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() diff --git a/ptvsd/pydevd_hooks.py b/ptvsd/pydevd_hooks.py new file mode 100644 index 000000000..cf7b293d3 --- /dev/null +++ b/ptvsd/pydevd_hooks.py @@ -0,0 +1,67 @@ +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() + + _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 + pydevd_comm.start_client = _start_client + + # 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/ptvsd/wrapper.py b/ptvsd/wrapper.py index 95803b234..b305382b8 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 @@ -49,25 +47,10 @@ # print(s) #ipcjson._TRACE = ipcjson_trace -ptvsd_sys_exit_code = 0 WAIT_FOR_DISCONNECT_REQUEST_TIMEOUT = 2 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 @@ -243,11 +226,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 +267,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 +331,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 +594,25 @@ 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, 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.killonclose = killonclose + 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 + + # debugger state self.is_process_created = False self.is_process_created_lock = threading.Lock() self.stack_traces = {} @@ -635,27 +624,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 +668,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 +681,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 +716,16 @@ 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() + + 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 @@ -770,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 @@ -1691,114 +1686,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) - - -######################## -# lifecycle - -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(client, server, killonclose=True, addhandlers=True): - name = 'ptvsd.Client' if server is None else 'ptvsd.Server' - - pydevd = PydevdSocket(lambda *args: proc.on_pydevd_event(*args)) - proc = VSCodeMessageProcessor(client, pydevd, - killonclose=killonclose) - - server_thread = threading.Thread(target=proc.process_messages, - name=name) - server_thread.daemon = True - server_thread.start() - - if addhandlers: - _add_atexit_handler(proc, server_thread) - _set_signal_handlers(proc) - - return pydevd - - -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 _set_signal_handlers(proc): - if platform.system() == 'Windows': - return None - - def handler(signum, frame): - proc.close() - sys.exit(0) - signal.signal(signal.SIGHUP, handler) - - -######################## -# pydevd hooks - -def start_server(port, addhandlers=True): - """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 = _start(client, server) - return pydevd - - -def start_client(host, port, addhandlers=True): - """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 = _start(client, None) - 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). - """ - # 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 - - # 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 diff --git a/tests/helpers/pydevd/_binder.py b/tests/helpers/pydevd/_binder.py index ff0ec68fc..a5e98ef23 100644 --- a/tests/helpers/pydevd/_binder.py +++ b/tests/helpers/pydevd/_binder.py @@ -1,12 +1,11 @@ -from collections import namedtuple import threading import time -from ptvsd import wrapper +import ptvsd.daemon from tests.helpers import socket -class PTVSD(namedtuple('PTVSD', 'client server proc fakesock')): +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 @@ -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,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. """ - self.proc.close() + try: + super(PTVSD, self).close() + except ptvsd.daemon.DaemonClosedError: + pass class BinderBase(object): @@ -141,10 +151,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): diff --git a/tests/helpers/socket.py b/tests/helpers/socket.py index 5092e20b1..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.wrapper 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: 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):