def test_events(self): EVENTS = [ { "seq": 1, "type": "event", "event": "stopped", "body": {"reason": "pause"}, }, { "seq": 2, "type": "event", "event": "unknown", "body": {"something": "else"}, }, ] recorder = MessageHandlerRecorder() class Handlers(object): @recorder def stopped_event(self, event): assert event.event == "stopped" @recorder def event(self, event): assert event.event == "unknown" stream = JsonMemoryStream(EVENTS, []) channel = messaging.JsonMessageChannel(stream, Handlers()) channel.start() channel.wait() recorder.expect(channel, EVENTS, ["stopped_event", "event"])
def _start_channel(self, stream): handlers = messaging.MessageHandlers( request=self._process_request, event=self._process_event, disconnect=self._process_disconnect, ) self.channel = messaging.JsonMessageChannel(stream, handlers) self.channel.start() telemetry = self.wait_for_next_event("output") assert telemetry == { "category": "telemetry", "output": "ptvsd", "data": { "packageVersion": some.str }, } self.request( "initialize", { "pathFormat": "path", "clientID": self.client_id, "adapterID": "test", "linesStartAt1": True, "columnsStartAt1": True, "supportsVariableType": True, "supportsRunInTerminalRequest": True, }, )
def __init__(self, session, stream=None, channel=None): assert (stream is None) ^ (channel is None) try: lock_held = session.lock.acquire(blocking=False) assert lock_held, "__init__ of a Component subclass must lock its Session" finally: session.lock.release() super(Component, self).__init__() self.session = session if channel is None: stream.name = str(self) channel = messaging.JsonMessageChannel(stream, self) channel.start() else: channel.name = channel.stream.name = str(self) channel.handlers = self self.channel = channel self.is_connected = True # Do this last to avoid triggering useless notifications for assignments above. self.observers += [lambda *_: self.session.notify_changed()]
def __init__(self, session, stream): super(Component, self).__init__() self.session = session stream.name = str(self) self.channel = messaging.JsonMessageChannel(stream, self) self.is_connected = True self.observers += [lambda *_: session.notify_changed()] self.channel.start()
def connect(session_id, launcher_port): global channel assert channel is None sock = sockets.create_client() sock.connect(("127.0.0.1", launcher_port)) stream = messaging.JsonIOStream.from_socket(sock, fmt("Adapter-{0}", session_id)) channel = messaging.JsonMessageChannel(stream, handlers=Handlers()) channel.start()
def connect(launcher_port): from ptvsd.common import messaging, sockets from ptvsd.launcher import handlers global channel assert channel is None sock = sockets.create_client() sock.connect(("127.0.0.1", launcher_port)) stream = messaging.JsonIOStream.from_socket(sock, "Adapter") channel = messaging.JsonMessageChannel(stream, handlers=handlers) channel.start()
def __init__(self, sock): from ptvsd.adapter import sessions self.server = None """The Server component, if this debug server belongs to Session. """ self.pid = None stream = messaging.JsonIOStream.from_socket(sock, str(self)) self.channel = messaging.JsonMessageChannel(stream, self) self.channel.start() try: info = self.channel.request("pydevdSystemInfo") process_info = info("process", json.object()) self.pid = process_info("pid", int) self.ppid = process_info("ppid", int, optional=True) if self.ppid == (): self.ppid = None self.channel.name = stream.name = str(self) with _lock: if any(conn.pid == self.pid for conn in _connections): raise KeyError( fmt("{0} is already connected to this adapter", self)) _connections.append(self) _connections_changed.set() except Exception: log.exception("Failed to accept incoming server connection:") # If we couldn't retrieve all the necessary info from the debug server, # or there's a PID clash, we don't want to track this debuggee anymore, # but we want to continue accepting connections. self.channel.close() return parent_session = sessions.get(self.ppid) if parent_session is None: log.info("No active debug session for parent process of {0}.", self) else: try: parent_session.ide.notify_of_subprocess(self) except Exception: # This might fail if the IDE concurrently disconnects from the parent # session. We still want to keep the connection around, in case the # IDE reconnects later. If the parent session was "launch", it'll take # care of closing the remaining server connections. log.exception("Failed to notify parent session about {0}:", self)
def __init__(self, sock): from ptvsd.adapter import sessions self.disconnected = False self.server = None """The Server component, if this debug server belongs to Session. """ self.pid = None stream = messaging.JsonIOStream.from_socket(sock, str(self)) self.channel = messaging.JsonMessageChannel(stream, self) self.channel.start() try: self.authenticate() info = self.channel.request("pydevdSystemInfo") process_info = info("process", json.object()) self.pid = process_info("pid", int) self.ppid = process_info("ppid", int, optional=True) if self.ppid == (): self.ppid = None self.channel.name = stream.name = str(self) ptvsd_dir = os.path.dirname(os.path.dirname(ptvsd.__file__)) # Note: we must check if 'ptvsd' is not already in sys.modules because the # evaluation of an import at the wrong time could deadlock Python due to # its import lock. # # So, in general this evaluation shouldn't do anything. It's only # important when pydevd attaches automatically to a subprocess. In this # case, we have to make sure that ptvsd is properly put back in the game # for users to be able to use it.v # # In this case (when the import is needed), this evaluation *must* be done # before the configurationDone request is sent -- if this is not respected # it's possible that pydevd already started secondary threads to handle # commands, in which case it's very likely that this command would be # evaluated at the wrong thread and the import could potentially deadlock # the program. # # Note 2: the sys module is guaranteed to be in the frame globals and # doesn't need to be imported. inject_ptvsd = """ if 'ptvsd' not in sys.modules: sys.path.insert(0, {ptvsd_dir!r}) try: import ptvsd finally: del sys.path[0] """ inject_ptvsd = fmt(inject_ptvsd, ptvsd_dir=ptvsd_dir) try: self.channel.request("evaluate", {"expression": inject_ptvsd}) except messaging.MessageHandlingError: # Failure to inject is not a fatal error - such a subprocess can # still be debugged, it just won't support "import ptvsd" in user # code - so don't terminate the session. log.exception("Failed to inject ptvsd into {0}:", self, level="warning") with _lock: # The server can disconnect concurrently before we get here, e.g. if # it was force-killed. If the disconnect() handler has already run, # don't register this server or report it, since there's nothing to # deregister it. if self.disconnected: return if any(conn.pid == self.pid for conn in _connections): raise KeyError( fmt("{0} is already connected to this adapter", self)) _connections.append(self) _connections_changed.set() except Exception: log.exception("Failed to accept incoming server connection:") self.channel.close() # If this was the first server to connect, and the main thread is inside # wait_until_disconnected(), we want to unblock it and allow it to exit. dont_wait_for_first_connection() # If we couldn't retrieve all the necessary info from the debug server, # or there's a PID clash, we don't want to track this debuggee anymore, # but we want to continue accepting connections. return parent_session = sessions.get(self.ppid) if parent_session is None: log.info("No active debug session for parent process of {0}.", self) else: try: parent_session.ide.notify_of_subprocess(self) except Exception: # This might fail if the IDE concurrently disconnects from the parent # session. We still want to keep the connection around, in case the # IDE reconnects later. If the parent session was "launch", it'll take # care of closing the remaining server connections. log.exception("Failed to notify parent session about {0}:", self)
def test_fuzz(self): # Set up two channels over the same stream that send messages to each other # asynchronously, and record everything that they send and receive. # All records should match at the end. class Fuzzer(object): def __init__(self, name): self.name = name self.lock = threading.Lock() self.sent = [] self.received = [] self.responses_sent = [] self.responses_received = [] self.done = False def start(self, channel): self._worker = threading.Thread( name=self.name, target=lambda: self._send_requests_and_events(channel), ) self._worker.daemon = True self._worker.start() def wait(self): self._worker.join() def done_event(self, event): with self.lock: self.done = True def fizz_event(self, event): assert event.event == "fizz" with self.lock: self.received.append(("event", "fizz", event.body)) def buzz_event(self, event): assert event.event == "buzz" with self.lock: self.received.append(("event", "buzz", event.body)) def event(self, event): with self.lock: self.received.append(("event", event.event, event.body)) def make_and_log_response(self, request): x = random.randint(-100, 100) if x < 0: exc_type = (messaging.InvalidMessageError if x % 2 else messaging.MessageHandlingError) x = exc_type(str(x), request) with self.lock: self.responses_sent.append((request.seq, x)) return x def fizz_request(self, request): assert request.command == "fizz" with self.lock: self.received.append( ("request", "fizz", request.arguments)) return self.make_and_log_response(request) def buzz_request(self, request): assert request.command == "buzz" with self.lock: self.received.append( ("request", "buzz", request.arguments)) return self.make_and_log_response(request) def request(self, request): with self.lock: self.received.append( ("request", request.command, request.arguments)) return self.make_and_log_response(request) def _got_response(self, response): with self.lock: self.responses_received.append( (response.request.seq, response.body)) def _send_requests_and_events(self, channel): types = [ random.choice(("event", "request")) for _ in range(0, 100) ] for typ in types: name = random.choice(("fizz", "buzz", "fizzbuzz")) body = random.randint(0, 100) with self.lock: self.sent.append((typ, name, body)) if typ == "event": channel.send_event(name, body) elif typ == "request": req = channel.send_request(name, body) req.on_response(self._got_response) channel.send_event("done") # Spin until we receive "done", and also get responses to all requests. requests_sent = types.count("request") log.info("{0} waiting for {1} responses...", self.name, requests_sent) while True: with self.lock: if self.done: if requests_sent == len(self.responses_received): break time.sleep(0.1) fuzzer1 = Fuzzer("fuzzer1") fuzzer2 = Fuzzer("fuzzer2") server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.bind(("localhost", 0)) _, port = server_socket.getsockname() server_socket.listen(0) socket1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) socket1_thread = threading.Thread( target=lambda: socket1.connect(("localhost", port))) socket1_thread.start() socket2, _ = server_socket.accept() socket1_thread.join() try: io1 = socket1.makefile("rwb", 0) io2 = socket2.makefile("rwb", 0) stream1 = messaging.JsonIOStream(io1, io1, "socket1") channel1 = messaging.JsonMessageChannel(stream1, fuzzer1) channel1.start() fuzzer1.start(channel1) stream2 = messaging.JsonIOStream(io2, io2, "socket2") channel2 = messaging.JsonMessageChannel(stream2, fuzzer2) channel2.start() fuzzer2.start(channel2) fuzzer1.wait() fuzzer2.wait() finally: socket1.close() socket2.close() assert fuzzer1.sent == fuzzer2.received assert fuzzer2.sent == fuzzer1.received assert fuzzer1.responses_sent == fuzzer2.responses_received assert fuzzer2.responses_sent == fuzzer1.responses_received
def test_invalid_request_handling(self): REQUESTS = [ { "seq": 1, "type": "request", "command": "stackTrace", "arguments": { "AAA": {} }, }, { "seq": 2, "type": "request", "command": "stackTrace", "arguments": {} }, { "seq": 3, "type": "request", "command": "unknown", "arguments": None }, { "seq": 4, "type": "request", "command": "pause" }, ] class Handlers(object): def stackTrace_request(self, request): request.arguments["AAA"] request.arguments["AAA"]["BBB"] def request(self, request): request.arguments["CCC"] def pause_request(self, request): request.arguments["DDD"] output = [] stream = JsonMemoryStream(REQUESTS, output) channel = messaging.JsonMessageChannel(stream, Handlers()) channel.start() channel.wait() def missing_property(name): return some.str.matching("Invalid message:.*" + re.escape(name) + ".*") assert output == [ { "seq": 1, "type": "response", "request_seq": 1, "command": "stackTrace", "success": False, "message": missing_property("BBB"), }, { "seq": 2, "type": "response", "request_seq": 2, "command": "stackTrace", "success": False, "message": missing_property("AAA"), }, { "seq": 3, "type": "response", "request_seq": 3, "command": "unknown", "success": False, "message": missing_property("CCC"), }, { "seq": 4, "type": "response", "request_seq": 4, "command": "pause", "success": False, "message": missing_property("DDD"), }, ]
def test_responses(self): request1_sent = threading.Event() request2_sent = threading.Event() request3_sent = threading.Event() request4_sent = threading.Event() def iter_responses(): request1_sent.wait() yield { "seq": 1, "type": "response", "request_seq": 1, "command": "next", "success": True, "body": { "threadId": 3 }, } request2_sent.wait() yield { "seq": 2, "type": "response", "request_seq": 2, "command": "pause", "success": False, "message": "Invalid message: pause not supported", } request3_sent.wait() yield { "seq": 3, "type": "response", "request_seq": 3, "command": "next", "success": True, "body": { "threadId": 5 }, } request4_sent.wait() stream = JsonMemoryStream(iter_responses(), []) channel = messaging.JsonMessageChannel(stream, None) channel.start() # Blocking wait. request1 = channel.send_request("next") request1_sent.set() log.info("Waiting for response...") response1_body = request1.wait_for_response() response1 = request1.response assert response1.success assert response1.request is request1 assert response1.body == response1_body assert response1.body == {"threadId": 3} # Async callback, registered before response is received. request2 = channel.send_request("pause") response2 = [] response2_received = threading.Event() def response2_handler(resp): response2.append(resp) response2_received.set() log.info("Registering callback") request2.on_response(response2_handler) request2_sent.set() log.info("Waiting for callback...") response2_received.wait() response2, = response2 assert not response2.success assert response2.request is request2 assert response2 is request2.response assert response2.body == messaging.InvalidMessageError( "pause not supported", request2) # Async callback, registered after response is received. request3 = channel.send_request("next") request3_sent.set() request3.wait_for_response() response3 = [] response3_received = threading.Event() def response3_handler(resp): response3.append(resp) response3_received.set() log.info("Registering callback") request3.on_response(response3_handler) log.info("Waiting for callback...") response3_received.wait() response3, = response3 assert response3.success assert response3.request is request3 assert response3 is request3.response assert response3.body == {"threadId": 5} # Async callback, registered after channel is closed. request4 = channel.send_request("next") request4_sent.set() channel.wait() response4 = [] response4_received = threading.Event() def response4_handler(resp): response4.append(resp) response4_received.set() log.info("Registering callback") request4.on_response(response4_handler) log.info("Waiting for callback...") response4_received.wait() response4, = response4 assert not response4.success assert response4.request is request4 assert response4 is request4.response assert isinstance(response4.body, messaging.NoMoreMessages)
def test_requests(self): REQUESTS = [ { "seq": 1, "type": "request", "command": "next", "arguments": { "threadId": 3 }, }, { "seq": 2, "type": "request", "command": "launch", "arguments": { "program": "main.py" }, }, { "seq": 3, "type": "request", "command": "unknown", "arguments": { "answer": 42 }, }, { "seq": 4, "type": "request", "command": "pause", "arguments": { "threadId": 5 }, }, ] recorder = MessageHandlerRecorder() class Handlers(object): @recorder def next_request(self, request): assert request.command == "next" return {"threadId": 7} @recorder def launch_request(self, request): assert request.command == "launch" self._launch = request return messaging.NO_RESPONSE @recorder def request(self, request): request.respond({}) @recorder def pause_request(self, request): assert request.command == "pause" self._launch.respond({"processId": 9}) raise request.cant_handle("pause error") stream = JsonMemoryStream(REQUESTS, []) channel = messaging.JsonMessageChannel(stream, Handlers()) channel.start() channel.wait() recorder.expect( channel, REQUESTS, ["next_request", "launch_request", "request", "pause_request"], ) assert stream.output == [ { "seq": 1, "type": "response", "request_seq": 1, "command": "next", "success": True, "body": { "threadId": 7 }, }, { "seq": 2, "type": "response", "request_seq": 3, "command": "unknown", "success": True, }, { "seq": 3, "type": "response", "request_seq": 2, "command": "launch", "success": True, "body": { "processId": 9 }, }, { "seq": 4, "type": "response", "request_seq": 4, "command": "pause", "success": False, "message": "pause error", }, ]