diff --git a/ptvsd/ipcjson.py b/ptvsd/ipcjson.py index 4759c37eb..1ff29941c 100644 --- a/ptvsd/ipcjson.py +++ b/ptvsd/ipcjson.py @@ -100,6 +100,7 @@ def _send(self, **payload): try: self.__socket.send(headers) self.__socket.send(content) + _trace('Sent content', content) except BrokenPipeError: pass except OSError as exc: @@ -191,6 +192,7 @@ def _wait_for_message(self): # read content, utf-8 encoded content = self._buffered_read_as_utf8(length) try: + _trace('Received content', content) msg = json.loads(content) self._receive_message(msg) except ValueError: diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py index 95803b234..fb4762ab7 100644 --- a/ptvsd/wrapper.py +++ b/ptvsd/wrapper.py @@ -234,7 +234,26 @@ def __init__(self, name, description, stack, source): self.source = source -class PydevdSocket(object): +class Observable(object): + def __init__(self): + self._observers = [] + + def register_observer(self, observer): + self._observers.append(observer) + + def un_register_observer(self, observer): + self._observers.remove(observer) + + def notify_observers(self, *args, **kwargs): + for observer in self._observers: + observer.notify(*args, **kwargs) + + @property + def observer_count(self): + return len(self._observers) + + +class PydevdSocket(Observable): """A dummy socket-like object for communicating with pydevd. It parses pydevd messages and redirects them to the provided handler @@ -245,9 +264,10 @@ class PydevdSocket(object): _vscprocessor = None - def __init__(self, event_handler): + def __init__(self): + super(PydevdSocket, self).__init__() #self.log = open('pydevd.log', 'w') - self.event_handler = event_handler + self.event_handler = self.notify_observers self.lock = threading.Lock() self.seq = 1000000000 self.pipe_r, self.pipe_w = os.pipe() @@ -621,6 +641,7 @@ def __init__(self, socket, pydevd, logfile=None, super(VSCodeMessageProcessor, self).__init__(socket=socket, own_socket=False, logfile=logfile) + pydevd.register_observer(self) self.socket = socket self.pydevd = pydevd self.killonclose = killonclose @@ -659,16 +680,24 @@ def __init__(self, socket, pydevd, logfile=None, # closing the adapter - def close(self): + def close(self, exit=True): """Stop the message processor and release its resources.""" if self._closed: 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.pydevd.un_register_observer(self) + if exit: + # Stop the PyDevd message handler first. + self._stop_pydevd_message_loop() + # Treat PyDevd as effectively exited. + self._handle_pydevd_stopped() + else: + # 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() + # Close the editor-side socket. self._stop_vsc_message_loop() @@ -689,6 +718,9 @@ def _stop_vsc_message_loop(self): except Exception: pass + def notify(self, *args, **kwargs): + self._on_pydevd_event(*args) + def _handle_pydevd_stopped(self): wait_on_normal_exit = self.debug_options.get( 'WAIT_ON_NORMAL_EXIT', False) @@ -723,13 +755,13 @@ def _wait_for_disconnect(self, timeout=None): self.send_response(self.disconnect_request) self.disconnect_request = None - def _handle_disconnect(self, request): + def _handle_disconnect(self, request, exit=True): self.disconnect_request = request self.disconnect_request_event.set() killProcess = not self._closed - self.close() + self.close(exit=exit) # TODO: Move killing the process to close()? - if killProcess and self.killonclose: + if exit and killProcess and self.killonclose: os.kill(os.getpid(), signal.SIGTERM) # async helpers @@ -792,7 +824,7 @@ def decorate(f): pydevd_events = EventHandlers() - def on_pydevd_event(self, cmd_id, seq, args): + def _on_pydevd_event(self, cmd_id, seq, args): # TODO: docstring try: f = self.pydevd_events[cmd_id] @@ -937,6 +969,15 @@ def on_attach(self, request, args): options = self.build_debug_options(args.get('debugOptions', [])) self.debug_options = self._parse_debug_options( args.get('options', options)) + + if self.pydevd.observer_count > 1: + self.send_response( + request, + success=False, + message='A debugger is already attached to this process.', + ) + return + self.send_response(request) @async_handler @@ -953,7 +994,7 @@ def on_disconnect(self, request, args): if self.start_reason == 'launch': self._handle_disconnect(request) else: - self.send_response(request) + self._handle_disconnect(request, exit=False) def send_process_event(self, start_method): # TODO: docstring @@ -1412,7 +1453,7 @@ def on_setBreakpoints(self, request, args): for src_bp in src_bps: line = src_bp['line'] vsc_bpid = self.bp_map.add( - lambda vsc_bpid: (path, vsc_bpid)) + lambda vsc_bpid: (path, vsc_bpid)) self.path_casing.track_file_path_case(path) msg = msgfmt.format(vsc_bpid, bp_type, path, line, src_bp.get('condition', None)) @@ -1561,9 +1602,9 @@ def on_pydevd_thread_suspend(self, seq, args): pyd_tid = xml.thread['id'] reason = int(xml.thread['stop_reason']) STEP_REASONS = { - pydevd_comm.CMD_STEP_INTO, - pydevd_comm.CMD_STEP_OVER, - pydevd_comm.CMD_STEP_RETURN, + pydevd_comm.CMD_STEP_INTO, + pydevd_comm.CMD_STEP_OVER, + pydevd_comm.CMD_STEP_RETURN, } EXCEPTION_REASONS = { pydevd_comm.CMD_STEP_CAUGHT_EXCEPTION, @@ -1715,22 +1756,28 @@ def _new_sock(): 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) +def _add_pydevd_event_handler(client, + pydevd, + name, + killonclose=True, + addhandlers=True): + proc = VSCodeMessageProcessor(client, pydevd, killonclose=killonclose) - server_thread = threading.Thread(target=proc.process_messages, - name=name) + 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) + _add_signal_handler(proc) + +def _start(client, server, killonclose=True, addhandlers=True): + name = 'ptvsd.Client' if server is None else 'ptvsd.Server' + if addhandlers: + _set_signal_handler() + pydevd = PydevdSocket() + _add_pydevd_event_handler(client, pydevd, name, killonclose, addhandlers) return pydevd @@ -1742,14 +1789,25 @@ def handler(): atexit.register(handler) -def _set_signal_handlers(proc): +signal_handler = Observable() + + +def _set_signal_handler(): if platform.system() == 'Windows': return None + signal.signal(signal.SIGHUP, signal_sighup_handler) + + +def signal_sighup_handler(signum, frame): + signal_handler.notify_observers() + sys.exit(0) - def handler(signum, frame): - proc.close() - sys.exit(0) - signal.signal(signal.SIGHUP, handler) + +def _add_signal_handler(proc): + if platform.system() == 'Windows': + return None + + signal_handler.register_observer({"notify": lambda: proc.close()}) ######################## @@ -1766,6 +1824,16 @@ def start_server(port, addhandlers=True): server = _create_server(port) client, _ = server.accept() pydevd = _start(client, server) + + def _wait_for_more_connections(): + while True: + client, _ = server.accept() + _add_pydevd_event_handler(client, pydevd, name='ptvsd.Server') + + connection_thread = threading.Thread(target=_wait_for_more_connections, + name='ptvsd.Client.Connection') + connection_thread.daemon = True + connection_thread.start() return pydevd diff --git a/tests/highlevel/test_lifecycle.py b/tests/highlevel/test_lifecycle.py index a170fdf00..702e8dc7c 100644 --- a/tests/highlevel/test_lifecycle.py +++ b/tests/highlevel/test_lifecycle.py @@ -54,7 +54,7 @@ def test_attach(self): # end req_disconnect = self.send_request('disconnect') finally: - with self._fix.wait_for_events(['exited', 'terminated']): + with self._fix.wait_for_events(['terminated']): self.fix.close_ptvsd() daemon.close() @@ -90,15 +90,8 @@ def test_attach(self): self.new_event('initialized'), self.new_response(req_attach), self.new_response(req_config), - #self.new_event('process', **dict( - # name=sys.argv[0], - # systemProcessId=os.getpid(), - # isLocalProcess=True, - # startMethod='attach', - #)), - self.new_response(req_disconnect), - self.new_event('exited', exitCode=0), self.new_event('terminated'), + self.new_response(req_disconnect), ]) self.assert_received(self.debugger, [ self.debugger_msgs.new_request(CMD_VERSION,