From f02a4798348462c4eaa0cecbd774ef6f3a06b944 Mon Sep 17 00:00:00 2001 From: Salad Dais Date: Wed, 20 Jul 2022 09:20:27 +0000 Subject: [PATCH] Add get_task_inventory_cap.py addon example An example of mocking out actually useful behavior for the viewer. Better (faster!) task inventory fetching API. --- addon_examples/get_task_inventory_cap.py | 100 +++++++++++++++++++++++ hippolyzer/lib/proxy/region.py | 6 +- hippolyzer/lib/proxy/webapp_cap_addon.py | 5 +- 3 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 addon_examples/get_task_inventory_cap.py diff --git a/addon_examples/get_task_inventory_cap.py b/addon_examples/get_task_inventory_cap.py new file mode 100644 index 0000000..9008b5c --- /dev/null +++ b/addon_examples/get_task_inventory_cap.py @@ -0,0 +1,100 @@ +""" +Loading task inventory doesn't actually need to be slow. + +By using a cap instead of the slow xfer path and sending the LLSD inventory +model we get 15x speedups even when mocking things behind the scenes by using +a hacked up version of xfer. See turbo_object_inventory.py +""" + +import asyncio + +import asgiref.wsgi +from typing import * + +from flask import Flask, Response, request + +from hippolyzer.lib.base import llsd +from hippolyzer.lib.base.datatypes import UUID +from hippolyzer.lib.base.inventory import InventoryModel +from hippolyzer.lib.base.message.message import Message, Block +from hippolyzer.lib.base.templates import XferFilePath +from hippolyzer.lib.proxy import addon_ctx +from hippolyzer.lib.proxy.webapp_cap_addon import WebAppCapAddon + +app = Flask("GetTaskInventoryCapApp") + + +@app.route('/', methods=["POST"]) +async def get_task_inventory(): + # Should always have the current region, the cap handler is bound to one. + # Just need to pull it from the `addon_ctx` module's global. + region = addon_ctx.region.get() + session = addon_ctx.session.get() + obj_id = UUID(request.args["task_id"]) + obj = region.objects.lookup_fullid(obj_id) + if not obj: + return Response(f"Couldn't find {obj_id}", status=404, mimetype="text/plain") + request_msg = Message( + 'RequestTaskInventory', + Block('AgentData', AgentID=session.agent_id, SessionID=session.id), + Block('InventoryData', LocalID=obj.LocalID), + ) + # Keep around a dict of chunks we saw previously in case we have to restart + # an Xfer due to missing chunks. We don't expect chunks to change across Xfers + # so this can be used to recover from dropped SendXferPackets in subsequent attempts + existing_chunks: Dict[int, bytes] = {} + for _ in range(3): + # Any previous requests will have triggered a delete of the inventory file + # by marking it complete on the server-side. Re-send our RequestTaskInventory + # To make sure there's a fresh copy. + region.circuit.send(request_msg.take()) + inv_message = await region.message_handler.wait_for( + ('ReplyTaskInventory',), + predicate=lambda x: x["InventoryData"]["TaskID"] == obj.FullID, + timeout=5.0, + ) + # No task inventory, send the reply as-is + file_name = inv_message["InventoryData"]["Filename"] + if not file_name: + return Response("", status=204) + + if inv_message["InventoryData"]["Serial"] == int(request.args.get("last_serial", None)): + # Nothing has changed since the version of the inventory they say they have, say so. + return Response("", status=304) + + xfer = region.xfer_manager.request( + file_name=file_name, + file_path=XferFilePath.CACHE, + turbo=True, + ) + xfer.chunks.update(existing_chunks) + try: + await xfer + except asyncio.TimeoutError: + # We likely failed the request due to missing chunks, store + # the chunks that we _did_ get for the next attempt. + existing_chunks.update(xfer.chunks) + continue + + inv_model = InventoryModel.from_str(xfer.reassemble_chunks().decode("utf8")) + + return Response( + llsd.format_notation({ + "inventory": inv_model.to_llsd(), + "inv_serial": inv_message["InventoryData"]["Serial"], + }), + headers={"Content-Type": "application/llsd+notation"}, + ) + raise asyncio.TimeoutError("Failed to get inventory after 3 tries") + + +class GetTaskInventoryCapExampleAddon(WebAppCapAddon): + # A cap URL with this name will be tied to each region when + # the sim is first connected to. The URL will be returned to the + # viewer in the Seed if the viewer requests it by name. + CAP_NAME = "GetTaskInventoryExample" + # Any asgi app should be fine. + APP = asgiref.wsgi.WsgiToAsgi(app) + + +addons = [GetTaskInventoryCapExampleAddon()] diff --git a/hippolyzer/lib/proxy/region.py b/hippolyzer/lib/proxy/region.py index 52cb76e..cd6ce1b 100644 --- a/hippolyzer/lib/proxy/region.py +++ b/hippolyzer/lib/proxy/region.py @@ -129,12 +129,16 @@ class ProxiedRegion(BaseClientRegion): def register_proxy_cap(self, name: str): """Register a cap to be completely handled by the proxy""" + if name in self.caps: + # If we have an existing cap then we should just use that. + cap_data = self.caps[name] + if cap_data[1] == CapType.PROXY_ONLY: + return cap_data[0] cap_url = f"http://{uuid.uuid4()!s}.caps.hippo-proxy.localhost" self.register_cap(name, cap_url, CapType.PROXY_ONLY) return cap_url def register_cap(self, name: str, cap_url: str, cap_type: CapType = CapType.NORMAL): - """Register a Cap that only has meaning the first time it's used""" self.caps.add(name, (cap_type, cap_url)) self._recalc_caps() diff --git a/hippolyzer/lib/proxy/webapp_cap_addon.py b/hippolyzer/lib/proxy/webapp_cap_addon.py index e5947e3..04d1d42 100644 --- a/hippolyzer/lib/proxy/webapp_cap_addon.py +++ b/hippolyzer/lib/proxy/webapp_cap_addon.py @@ -27,7 +27,10 @@ class WebAppCapAddon(BaseAddon, abc.ABC): def handle_region_registered(self, session: Session, region: ProxiedRegion): # Register a fake URL for our cap. This will add the cap URL to the Seed # response that gets sent back to the client if that cap name was requested. - if self.CAP_NAME not in region.cap_urls: + region.register_proxy_cap(self.CAP_NAME) + + def handle_session_init(self, session: Session): + for region in session.regions: region.register_proxy_cap(self.CAP_NAME) def handle_http_request(self, session_manager: SessionManager, flow: HippoHTTPFlow):