diff --git a/tests/test_debugstream.py b/tests/test_debugstream.py new file mode 100644 index 0000000..4483ab3 --- /dev/null +++ b/tests/test_debugstream.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# coding: utf-8 + +from xrpld_netgen.utils.deploy_kit import build_debugstream_service, build_stop_sh + + +class TestBuildDebugstreamService: + """Test build_debugstream_service returns correct dict structure""" + + def test_default_port(self): + result = build_debugstream_service("xahau", "xahau-net") + assert result["container_name"] == "debugstream" + assert result["ports"] == ["9999:9999"] + assert result["build"] == { + "context": "./debugstream", + "dockerfile": "Dockerfile", + } + assert result["networks"] == ["xahau-net"] + + def test_custom_port(self): + result = build_debugstream_service("xrpl", "xrpl-net", port=8888) + assert result["ports"] == ["8888:8888"] + + def test_volume_readonly(self): + result = build_debugstream_service("xahau", "xahau-net") + volumes = result["volumes"] + assert len(volumes) == 1 + assert volumes[0].endswith(":ro") + + def test_volume_uses_protocol_name(self): + result = build_debugstream_service("xahau", "xahau-net") + assert result["volumes"] == ["xahau-log:/opt/ripple/log:ro"] + + def test_depends_on_protocol(self): + result = build_debugstream_service("xahau", "xahau-net") + assert "xahau" in result["depends_on"] + + def test_depends_on_xrpl(self): + result = build_debugstream_service("xrpl", "xrpl-net") + assert "xrpl" in result["depends_on"] + + +class TestStopShStandalone: + """Test build_stop_sh for standalone mode""" + + def test_has_volume_flag(self): + result = build_stop_sh( + basedir="/workspace", + protocol="xahau", + name="test", + num_validators=0, + num_peers=0, + standalone=True, + ) + assert "down -v" in result + + def test_no_log_rm(self): + result = build_stop_sh( + basedir="/workspace", + protocol="xahau", + name="test", + num_validators=0, + num_peers=0, + standalone=True, + ) + assert "rm -r xahau/log" not in result diff --git a/xrpld_netgen/cli.py b/xrpld_netgen/cli.py index 6378d12..0469106 100644 --- a/xrpld_netgen/cli.py +++ b/xrpld_netgen/cli.py @@ -343,6 +343,13 @@ def main(): choices=["Memory", "NuDB"], default="NuDB", ) + parser_us.add_argument( + "--debugstream_port", + type=int, + required=False, + help="The debugstream port", + default=9999, + ) # down:standalone parser_ds = subparsers.add_parser("down:standalone", help="Down Standalone") parser_ds.add_argument("--name", required=False, help="The name of the network") @@ -615,6 +622,7 @@ def main(): BUILD_VERSION = args.version IPFS_SERVER = args.ipfs NODEDB_TYPE = args.nodedb_type + DEBUGSTREAM_PORT = args.debugstream_port if PROTOCOL == "xahau" and not IMPORT_KEY: IMPORT_KEY: str = ( @@ -651,6 +659,7 @@ def main(): print(f" - Build Version: {BUILD_VERSION}") print(f" - IPFS Server: {IPFS_SERVER}") print(f" - Node DB: {NODEDB_TYPE}") + print(f" - Debugstream Port: {DEBUGSTREAM_PORT}") if BUILD_TYPE == "image": create_standalone_image( @@ -664,6 +673,7 @@ def main(): BUILD_VERSION, IPFS_SERVER, NODEDB_TYPE, + DEBUGSTREAM_PORT, ) else: create_standalone_binary( @@ -677,6 +687,7 @@ def main(): BUILD_VERSION, IPFS_SERVER, NODEDB_TYPE, + DEBUGSTREAM_PORT, ) run_start( diff --git a/xrpld_netgen/deploykit/debugstream/Dockerfile b/xrpld_netgen/deploykit/debugstream/Dockerfile new file mode 100644 index 0000000..a004652 --- /dev/null +++ b/xrpld_netgen/deploykit/debugstream/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.12-slim + +RUN pip install --no-cache-dir aiohttp + +COPY server.py . + +EXPOSE 9999 + +CMD ["python", "server.py"] diff --git a/xrpld_netgen/deploykit/debugstream/server.py b/xrpld_netgen/deploykit/debugstream/server.py new file mode 100644 index 0000000..6070baf --- /dev/null +++ b/xrpld_netgen/deploykit/debugstream/server.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# coding: utf-8 + +import os +import asyncio + +from aiohttp import web + + +LOG_PATH = os.environ.get("LOG_PATH", "/opt/ripple/log/debug.log") +PORT = int(os.environ.get("PORT", "9999")) + + +async def tail_log(ws: web.WebSocketResponse, raddress: str): + while not os.path.exists(LOG_PATH): + if ws.closed: + return + await asyncio.sleep(0.5) + + pos = os.path.getsize(LOG_PATH) + + while not ws.closed: + try: + size = os.path.getsize(LOG_PATH) + except FileNotFoundError: + await asyncio.sleep(0.5) + continue + + # Handle file rotation + if size < pos: + pos = 0 + + if size > pos: + with open(LOG_PATH, "r", errors="replace") as f: + f.seek(pos) + for line in f: + if raddress in line: + try: + await ws.send_str(line.rstrip("\n")) + except ConnectionResetError: + return + pos = f.tell() + + await asyncio.sleep(0.1) + + +async def handle_debugstream(request: web.Request) -> web.WebSocketResponse: + raddress = request.match_info["raddress"] + ws = web.WebSocketResponse() + await ws.prepare(request) + + task = asyncio.create_task(tail_log(ws, raddress)) + try: + async for _ in ws: + pass + finally: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + return ws + + +def create_app() -> web.Application: + app = web.Application() + app.router.add_get("/debugstream/{raddress}", handle_debugstream) + return app + + +if __name__ == "__main__": + web.run_app(create_app(), port=PORT) diff --git a/xrpld_netgen/main.py b/xrpld_netgen/main.py index 27b83c9..67d7733 100644 --- a/xrpld_netgen/main.py +++ b/xrpld_netgen/main.py @@ -11,6 +11,7 @@ from xrpld_netgen.utils.deploy_kit import ( create_dockerfile, download_binary, + build_debugstream_service, build_stop_sh, build_start_sh, build_local_start_sh, @@ -184,6 +185,7 @@ def create_standalone_image( build_name: str, add_ipfs: bool = False, nodedb_type: str = "NuDB", + debugstream_port: int = 9999, ) -> None: name: str = build_name os.makedirs(f"{basedir}/{protocol}-{name}", exist_ok=True) @@ -265,6 +267,7 @@ def create_xahau_standalone_folder( net_type: str, log_level: str = "trace", nodedb_type: str = "NuDB", + debugstream_port: int = 9999, ): cfg_path = f"{basedir}/{protocol}-{name}/config" rpc_public, rpc_admin, ws_public, ws_admin, peer = generate_ports(0, "standalone") @@ -342,6 +345,12 @@ def create_xahau_standalone_folder( f"{package_dir}/deploykit/{protocol}.entrypoint", f"{basedir}/{protocol}-{name}/entrypoint", ) + debugstream_src = f"{package_dir}/deploykit/debugstream" + debugstream_dst = f"{basedir}/{protocol}-{name}/debugstream" + if os.path.exists(debugstream_dst): + shutil.rmtree(debugstream_dst) + shutil.copytree(debugstream_src, debugstream_dst) + print(f"✅ {bcolors.CYAN}Building docker container...") pwd_str: str = "${PWD}" services[f"{protocol}"] = { @@ -360,11 +369,14 @@ def create_xahau_standalone_folder( ], "volumes": [ f"{pwd_str}/{protocol}/config:/etc/opt/ripple", - f"{pwd_str}/{protocol}/log:/opt/ripple/log", + f"{protocol}-log:/opt/ripple/log", f"{pwd_str}/{protocol}/lib:/opt/ripple/lib", ], "networks": ["standalone-network"], } + services["debugstream"] = build_debugstream_service( + protocol, "standalone-network", debugstream_port + ) def create_standalone_binary( @@ -378,6 +390,7 @@ def create_standalone_binary( build_version: str, add_ipfs: bool = False, nodedb_type: str = "NuDB", + debugstream_port: int = 9999, ) -> None: name: str = build_version os.makedirs(f"{basedir}/{protocol}-{name}", exist_ok=True) @@ -404,6 +417,7 @@ def create_standalone_binary( net_type, log_level, nodedb_type, + debugstream_port, ) services["explorer"] = { "image": "transia/explorer:latest", @@ -435,6 +449,7 @@ def create_standalone_binary( compose = { "version": "3.9", "services": services, + "volumes": {f"{protocol}-log": None}, "networks": {"standalone-network": {"driver": "bridge"}}, } diff --git a/xrpld_netgen/utils/deploy_kit.py b/xrpld_netgen/utils/deploy_kit.py index 6e75977..6b85905 100644 --- a/xrpld_netgen/utils/deploy_kit.py +++ b/xrpld_netgen/utils/deploy_kit.py @@ -213,6 +213,24 @@ def update_dockerfile(build_version: str, save_path: str) -> None: print(f"Dockerfile has been updated with the new xrpld version: {build_version}") +def build_debugstream_service( + protocol: str, + network_name: str, + port: int = 9999, +) -> dict: + return { + "build": { + "context": "./debugstream", + "dockerfile": "Dockerfile", + }, + "container_name": "debugstream", + "ports": [f"{port}:{port}"], + "volumes": [f"{protocol}-log:/opt/ripple/log:ro"], + "depends_on": [protocol], + "networks": [network_name], + } + + def build_stop_sh( basedir: str, protocol: str, @@ -235,10 +253,9 @@ def build_stop_sh( stop_sh_content += f"rm -r pnode{i}/log\n" if standalone: - stop_sh_content += f"docker compose -f {basedir}/{protocol}-{name}/docker-compose.yml down --remove-orphans\n" # noqa: E501 + stop_sh_content += f"docker compose -f {basedir}/{protocol}-{name}/docker-compose.yml down -v --remove-orphans\n" # noqa: E501 stop_sh_content += f"rm -r {protocol}/config\n" stop_sh_content += f"rm -r {protocol}/lib\n" - stop_sh_content += f"rm -r {protocol}/log\n" stop_sh_content += f"rm -r {protocol}\n" if local: