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.
This commit is contained in:
100
addon_examples/get_task_inventory_cap.py
Normal file
100
addon_examples/get_task_inventory_cap.py
Normal file
@@ -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()]
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user