From 6beef3d20017c3085ba5263da571ae2b83356459 Mon Sep 17 00:00:00 2001 From: Michal Gregorczyk Date: Mon, 4 Dec 2023 12:27:15 +0100 Subject: [PATCH] Add jdwp streams implemented abstract class Codegen dataclassess (#79) * Wrote test to validate primitive type mapping * Added test for type alias definition * Built dataclasses code generator * Built test for dataclassess code generator * Changed to unittest * Updated struct dataclass function to support all types * feat: built dataclass generator * Added copyright * Added nested struct support * Added test for nested struct * Feat: update functions and put it in a class. * chore: update tests * Casted types to pass pyre check * Updated types * chore: Refactor typing.cast code * Added copyright * refactor: update tests to reflect new schema changes * fix: Fix pyre typing errors [jdwp] do not emit array length and union tag fields [jdwp] map union tags and array lengths to int [jdwp] fix type for union fields [jdwp] fix names of fields containing spaces [jdwp] generate projects.jdwp.runtime.structs [jdwp] add tests importing components of the runtime library implement async abstract methods, add TCP conn update files update conn and streams add buck deps add jvm connection and implement jvm streams add a base class for jdwp structs [jdwp] generate projects.jdwp.runtime.structs [jdwp] add tests importing components of the runtime library fix fail build --- projects/jdwp/BUCK | 3 +- projects/jdwp/main.py | 18 ++- projects/jdwp/runtime/async_streams.py | 12 +- projects/jdwp/runtime/jdwp_streams.py | 181 ++++++++++++++++++++++++ projects/jdwp/runtime/jvm_connection.py | 90 ++++++++++++ 5 files changed, 294 insertions(+), 10 deletions(-) create mode 100644 projects/jdwp/runtime/jdwp_streams.py create mode 100644 projects/jdwp/runtime/jvm_connection.py diff --git a/projects/jdwp/BUCK b/projects/jdwp/BUCK index bbad7db..a5527ed 100644 --- a/projects/jdwp/BUCK +++ b/projects/jdwp/BUCK @@ -3,7 +3,7 @@ python_binary( name = "main", main = "main.py", - deps = [], + deps = [":lib"], ) @@ -11,4 +11,5 @@ python_library( name = "lib", srcs = glob(["**/*.py"]), visibility = ["PUBLIC"], + deps = [ "//projects/jdwp/runtime:runtime"] ) diff --git a/projects/jdwp/main.py b/projects/jdwp/main.py index 6924f3d..6c84f89 100644 --- a/projects/jdwp/main.py +++ b/projects/jdwp/main.py @@ -1,9 +1,21 @@ # Copyright (c) Meta Platforms, Inc. and affiliates. +import asyncio +from projects.jdwp.runtime.jvm_connection import JVMConnection -def main(): - return None + +async def main(): + host = "localhost" + port = 8880 + + connection = JVMConnection(host, port) + + await connection.connect() + + await connection.handshake() + + await connection.close() if __name__ == "__main__": - main() + asyncio.run(main()) diff --git a/projects/jdwp/runtime/async_streams.py b/projects/jdwp/runtime/async_streams.py index f1f9ae8..66023bb 100644 --- a/projects/jdwp/runtime/async_streams.py +++ b/projects/jdwp/runtime/async_streams.py @@ -96,15 +96,15 @@ async def read_long( class JDWPOutputStreamBase(abc.ABC): # Methods for OpaqueType @abc.abstractmethod - def write_boolean(self, value: bool) -> None: + async def write_boolean(self, value: bool) -> None: pass @abc.abstractmethod - def write_location(self, value: typing.Any) -> None: + async def write_location(self, value: typing.Any) -> None: pass @abc.abstractmethod - def write_string(self, value: str) -> None: + async def write_string(self, value: str) -> None: pass # Methods for IdType @@ -166,15 +166,15 @@ def write_frame_id(self, value: FrameIDType) -> None: # Methods for IntegralType @abc.abstractmethod - def write_byte(self, value: int) -> None: + async def write_byte(self, value: int) -> None: pass @abc.abstractmethod - def write_int(self, value: int) -> None: + async def write_int(self, value: int) -> None: pass @abc.abstractmethod - def write_long( + async def write_long( self, value: int, ) -> None: diff --git a/projects/jdwp/runtime/jdwp_streams.py b/projects/jdwp/runtime/jdwp_streams.py new file mode 100644 index 0000000..d66fb58 --- /dev/null +++ b/projects/jdwp/runtime/jdwp_streams.py @@ -0,0 +1,181 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. + +import struct +import typing +import asyncio +from projects.jdwp.defs.schema import IdType +from projects.jdwp.runtime.type_aliases import * +from projects.jdwp.runtime.async_streams import ( + JDWPInputStreamBase, + JDWPOutputStreamBase, +) + + +class JDWPInputStream(JDWPInputStreamBase): + __tcp_connection: asyncio.StreamReader + + def __init__(self, stream_reader: asyncio.StreamReader): + super().__init__() + self.__tcp_connection = stream_reader + + async def read_boolean(self) -> bool: + data = await self._read_bytes(1) + return bool(data[0]) + + async def read_location(self) -> typing.Any: + pass + + async def read_string(self) -> str: + length = await self.read_int() + string_data = await self._read_bytes(length) + return string_data.decode("utf-8") + + async def read_object_id(self) -> ObjectIDType: + return await self._read_id(IdType.OBJECT_ID) + + async def read_thread_id(self) -> ThreadIDType: + return await self._read_id(IdType.THREAD_ID) + + async def read_thread_group_id(self) -> ThreadGroupIDType: + return await self._read_id(IdType.THREAD_GROUP_ID) + + async def read_string_id(self) -> StringIDType: + return await self._read_id(IdType.STRING_ID) + + async def read_class_loader_id(self) -> ClassLoaderIDType: + return await self._read_id(IdType.CLASS_LOADER_ID) + + async def read_class_object_id(self) -> ClassObjectIDType: + return await self._read_id(IdType.CLASS_OBJECT_ID) + + async def read_array_id(self) -> ArrayIDType: + return await self._read_id(IdType.ARRAY_ID) + + async def read_reference_type_id(self) -> ReferenceTypeIDType: + return await self._read_id(IdType.REFERENCE_TYPE_ID) + + async def read_class_id(self) -> ClassIDType: + return await self._read_id(IdType.CLASS_ID) + + async def read_interface_id(self) -> InterfaceIDType: + return await self._read_id(IdType.INTERFACE_ID) + + async def read_array_type_id(self) -> ArrayTypeIDType: + return await self._read_id(IdType.ARRAY_TYPE_ID) + + async def read_method_id(self) -> MethodIDType: + return await self._read_id(IdType.METHOD_ID) + + async def read_field_id(self) -> FieldIDType: + return await self._read_id(IdType.FIELD_ID) + + async def read_frame_id(self) -> FrameIDType: + return await self._read_id(IdType.FRAME_ID) + + async def read_byte(self) -> int: + data = await self._read_bytes(1) + return int.from_bytes(data, byteorder="big") + + async def read_int(self) -> int: + data = await self._read_bytes(4) + return struct.unpack("!I", data)[0] + + async def read_long(self) -> int: + data = await self._read_bytes(8) + return struct.unpack("!Q", data)[0] + + async def _read_bytes(self, size: int) -> bytes: + try: + return await self.__tcp_connection.readexactly(size) + except Exception as e: + print(f"Error during data receiving: {e}") + return b"" + + async def _read_id(self, id_type: IdType) -> typing.Any: + pass + + +class JDWPOutputStream(JDWPOutputStreamBase): + __tcp_connection: asyncio.StreamWriter + + def __init__(self, socket_connection: asyncio.StreamWriter): + super().__init__() + self.__tcp_connection = socket_connection + + async def write_boolean(self, value: bool) -> None: + await self._write_bytes(struct.pack("!B", int(value))) + + async def write_int(self, value: int) -> None: + await self._write_bytes(struct.pack("!I", value)) + + async def write_array_id(self, value: ArrayIDType) -> None: + await self._write_id(value) + + async def write_array_type_id(self, value: ArrayTypeIDType) -> None: + await self._write_id(value) + + async def write_byte(self, value: int) -> None: + await self._write_bytes(struct.pack("!B", value)) + + async def write_class_id(self, value: ClassIDType) -> None: + await self._write_id(value) + + async def write_class_loader_id(self, value: ClassLoaderIDType) -> None: + await self._write_id(value) + + async def write_class_object_id(self, value: ClassObjectIDType) -> None: + await self._write_id(value) + + async def write_field_id(self, value: FieldIDType) -> None: + await self._write_id(value) + + async def write_frame_id(self, value: FrameIDType) -> None: + await self._write_id(value) + + async def write_interface_id(self, value: InterfaceIDType) -> None: + await self._write_id(value) + + async def write_location(self, value: typing.Any) -> None: + pass + + async def write_method_id(self, value: MethodIDType) -> None: + await self._write_id(value) + + async def write_string_id(self, value: StringIDType) -> None: + await self._write_id(value) + + async def write_thread_group_id(self, value: ThreadGroupIDType) -> None: + await self._write_id(value) + + async def write_long(self, value: int) -> None: + await self._write_bytes(struct.pack("!Q", value)) + + async def write_object_id(self, value: typing.Any) -> None: + await self._write_id(value) + + async def write_thread_id(self, value: typing.Any) -> None: + await self._write_id(value) + + async def write_string(self, value: str) -> None: + value_bytes = value.encode("utf-8") + length = len(value_bytes) + + await self.write_int(length) + + await self._write_bytes(value_bytes) + + async def write_reference_type_id(self, value: ReferenceTypeIDType) -> None: + await self._write_id(value) + + async def _write_bytes(self, data: bytes) -> None: + try: + self.__tcp_connection.write(data) + await self.__tcp_connection.drain() + except Exception as e: + print(f"Error during data sending: {e}") + await self.__tcp_connection.drain() + + async def _write_id(self, value: typing.Any) -> None: + size = min(value.bit_length() // 8 + 1, 8) + await self._write_bytes(struct.pack("B", size)) + await self._write_bytes(value.to_bytes(size, byteorder="big")) diff --git a/projects/jdwp/runtime/jvm_connection.py b/projects/jdwp/runtime/jvm_connection.py new file mode 100644 index 0000000..00a7e2b --- /dev/null +++ b/projects/jdwp/runtime/jvm_connection.py @@ -0,0 +1,90 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. + +import asyncio +import typing +import struct +from projects.jdwp.runtime.jdwp_streams import JDWPInputStream, JDWPOutputStream +from projects.jdwp.runtime.jdwpstruct import JDWPStruct + + +class JVMConnection: + next_packet_id: int = 0 + + def __init__(self, host: str, port: int): + self.host: str = host + self.port: int = port + self.__reader: typing.Optional[asyncio.StreamReader] = None + self.__writer: typing.Optional[asyncio.StreamWriter] = None + self.__input_stream: typing.Optional[JDWPInputStream] = None + self.__output_stream: typing.Optional[JDWPOutputStream] = None + + async def connect(self) -> None: + try: + self.__reader, self.__writer = await asyncio.open_connection( + self.host, self.port + ) + if self.__writer is None: + raise Exception("Writer not initialized") + self.__output_stream = JDWPOutputStream(self.__writer) + except Exception as e: + print(f"Error during connection: {e}") + await self.close() + + async def close(self) -> None: + for stream in (self.__reader, self.__writer): + if stream is not None: + if isinstance(stream, asyncio.streams.StreamWriter): + stream.close() + self.__input_stream, self.__output_stream = None, None + + async def handshake(self) -> None: + try: + handshake_bytes = b"JDWP-Handshake" + if self.__output_stream is not None: + await self.__output_stream._write_bytes(handshake_bytes) + + if self.__input_stream is not None and hasattr( + self.__input_stream, "read_packet_data" + ): + response_bytes = await self.__input_stream._read_bytes( + len(handshake_bytes) + ) + + if response_bytes != handshake_bytes: + raise Exception("Invalid handshake response") + + print(f"Handshake successful") + except Exception as e: + print(f"Error during handshake: {e}") + await self.close() + + async def read_packet_header(self) -> typing.Tuple[int, int, bytes, int, int]: + if self.__input_stream is None: + raise Exception("Input stream not initialized") + header_data = await self.__input_stream._read_bytes(10) + return struct.unpack("!IIcBB", header_data) + + async def read_packet_data(self, length: int) -> bytes: + if self.__input_stream is None: + raise Exception("Input stream not initialized") + return await self.__input_stream._read_bytes(length) + + async def write_packet_header( + self, length: int, packet_id: int, flags: bytes, command_set: int, command: int + ) -> None: + header_data = struct.pack( + "!IIcBB", length, packet_id, flags, command_set, command + ) + if self.__output_stream is None: + raise Exception("Output stream not initialized") + await self.__output_stream._write_bytes(header_data) + + async def write_packet_data(self, data: bytes) -> None: + if self.__output_stream is None: + raise Exception("Output stream not initialized") + await self.__output_stream._write_bytes(data) + + @staticmethod + def get_next_packet_id() -> int: + JVMConnection.next_packet_id += 1 + return JVMConnection.next_packet_id