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