Give each addon a separate addon_ctx bucket
This fixes addons being able to accidentally stomp all over each others' state just because they happened to use the same name for a SessionProperty.
This commit is contained in:
@@ -6,7 +6,7 @@ from hippolyzer.lib.proxy.sessions import Session
|
|||||||
def handle_lludp_message(session: Session, region: ProxiedRegion, message: Message):
|
def handle_lludp_message(session: Session, region: ProxiedRegion, message: Message):
|
||||||
# addon_ctx will persist across addon reloads, use for storing data that
|
# addon_ctx will persist across addon reloads, use for storing data that
|
||||||
# needs to survive across calls to this function
|
# needs to survive across calls to this function
|
||||||
ctx = session.addon_ctx
|
ctx = session.addon_ctx[__name__]
|
||||||
if message.name == "ChatFromViewer":
|
if message.name == "ChatFromViewer":
|
||||||
chat = message["ChatData"]["Message"]
|
chat = message["ChatData"]["Message"]
|
||||||
if chat == "COUNT":
|
if chat == "COUNT":
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ def _to_spongecase(val):
|
|||||||
|
|
||||||
|
|
||||||
def handle_lludp_message(session: Session, _region: ProxiedRegion, message: Message):
|
def handle_lludp_message(session: Session, _region: ProxiedRegion, message: Message):
|
||||||
ctx = session.addon_ctx
|
ctx = session.addon_ctx[__name__]
|
||||||
ctx.setdefault("spongecase", False)
|
ctx.setdefault("spongecase", False)
|
||||||
if message.name == "ChatFromViewer":
|
if message.name == "ChatFromViewer":
|
||||||
chat = message["ChatData"]["Message"]
|
chat = message["ChatData"]["Message"]
|
||||||
|
|||||||
@@ -211,13 +211,17 @@ class BaseAddonProperty(abc.ABC, Generic[_T, _U]):
|
|||||||
session_manager.addon_ctx dict, without any namespacing. Can be accessed either
|
session_manager.addon_ctx dict, without any namespacing. Can be accessed either
|
||||||
through `AddonClass.property_name` or `addon_instance.property_name`.
|
through `AddonClass.property_name` or `addon_instance.property_name`.
|
||||||
"""
|
"""
|
||||||
__slots__ = ("name", "default")
|
__slots__ = ("name", "default", "_owner")
|
||||||
|
|
||||||
def __init__(self, default=dataclasses.MISSING):
|
def __init__(self, default=dataclasses.MISSING):
|
||||||
self.default = default
|
self.default = default
|
||||||
|
self._owner = None
|
||||||
|
|
||||||
def __set_name__(self, owner, name: str):
|
def __set_name__(self, owner, name: str):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
# Keep track of which addon "owns" this property so that we can shove
|
||||||
|
# the data in a bucket specific to that addon name.
|
||||||
|
self._owner = owner
|
||||||
|
|
||||||
def _make_default(self) -> _T:
|
def _make_default(self) -> _T:
|
||||||
if self.default is not dataclasses.MISSING:
|
if self.default is not dataclasses.MISSING:
|
||||||
@@ -235,18 +239,20 @@ class BaseAddonProperty(abc.ABC, Generic[_T, _U]):
|
|||||||
if ctx_obj is None:
|
if ctx_obj is None:
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
f"{self.__class__} {self.name} accessed outside proper context")
|
f"{self.__class__} {self.name} accessed outside proper context")
|
||||||
|
addon_state = ctx_obj.addon_ctx[self._owner.__name__]
|
||||||
# Set a default if we have one, otherwise let the keyerror happen.
|
# Set a default if we have one, otherwise let the keyerror happen.
|
||||||
# Maybe we should do this at addon initialization instead of on get.
|
# Maybe we should do this at addon initialization instead of on get.
|
||||||
if self.name not in ctx_obj.addon_ctx:
|
if self.name not in addon_state:
|
||||||
default = self._make_default()
|
default = self._make_default()
|
||||||
if default is not dataclasses.MISSING:
|
if default is not dataclasses.MISSING:
|
||||||
ctx_obj.addon_ctx[self.name] = default
|
addon_state[self.name] = default
|
||||||
else:
|
else:
|
||||||
raise AttributeError(f"{self.name} is not set")
|
raise AttributeError(f"{self.name} is not set")
|
||||||
return ctx_obj.addon_ctx[self.name]
|
return addon_state[self.name]
|
||||||
|
|
||||||
def __set__(self, _obj, value: _T) -> None:
|
def __set__(self, _obj, value: _T) -> None:
|
||||||
self._get_context_obj().addon_ctx[self.name] = value
|
addon_state = self._get_context_obj().addon_ctx[self._owner.__name__]
|
||||||
|
addon_state[self.name] = value
|
||||||
|
|
||||||
|
|
||||||
class SessionProperty(BaseAddonProperty[_T, "Session"]):
|
class SessionProperty(BaseAddonProperty[_T, "Session"]):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import collections
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import datetime
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
@@ -43,7 +44,8 @@ class Session(BaseClientSession):
|
|||||||
self.circuit_code = circuit_code
|
self.circuit_code = circuit_code
|
||||||
self.global_caps = {}
|
self.global_caps = {}
|
||||||
# Bag of arbitrary data addons can use to persist data across addon reloads
|
# Bag of arbitrary data addons can use to persist data across addon reloads
|
||||||
self.addon_ctx = {}
|
# Each addon name gets its own separate dict within this dict
|
||||||
|
self.addon_ctx: Dict[str, Dict[str, Any]] = collections.defaultdict(dict)
|
||||||
self.session_manager: SessionManager = session_manager or None
|
self.session_manager: SessionManager = session_manager or None
|
||||||
self.selected: SelectionModel = SelectionModel()
|
self.selected: SelectionModel = SelectionModel()
|
||||||
self.regions: List[ProxiedRegion] = []
|
self.regions: List[ProxiedRegion] = []
|
||||||
@@ -188,7 +190,7 @@ class SessionManager:
|
|||||||
self.flow_context = HTTPFlowContext()
|
self.flow_context = HTTPFlowContext()
|
||||||
self.asset_repo = HTTPAssetRepo()
|
self.asset_repo = HTTPAssetRepo()
|
||||||
self.message_logger: Optional[BaseMessageLogger] = None
|
self.message_logger: Optional[BaseMessageLogger] = None
|
||||||
self.addon_ctx: Dict[str, Any] = {}
|
self.addon_ctx: Dict[str, Dict[str, Any]] = collections.defaultdict(dict)
|
||||||
self.name_cache = ProxyNameCache()
|
self.name_cache = ProxyNameCache()
|
||||||
self.pending_leap_clients: List[LEAPClient] = []
|
self.pending_leap_clients: List[LEAPClient] = []
|
||||||
|
|
||||||
|
|||||||
@@ -88,12 +88,12 @@ class AddonIntegrationTests(BaseProxyTest):
|
|||||||
self._setup_default_circuit()
|
self._setup_default_circuit()
|
||||||
self._fake_command("foobar baz")
|
self._fake_command("foobar baz")
|
||||||
await self._wait_drained()
|
await self._wait_drained()
|
||||||
self.assertEqual(self.session.addon_ctx["bazquux"], "baz")
|
self.assertEqual(self.session.addon_ctx["MockAddon"]["bazquux"], "baz")
|
||||||
|
|
||||||
# In session context these should be equivalent
|
# In session context these should be equivalent
|
||||||
with addon_ctx.push(new_session=self.session):
|
with addon_ctx.push(new_session=self.session):
|
||||||
self.assertEqual(self.session.addon_ctx["bazquux"], self.addon.bazquux)
|
self.assertEqual(self.session.addon_ctx["MockAddon"]["bazquux"], self.addon.bazquux)
|
||||||
self.assertEqual(self.session.addon_ctx["another"], "baz")
|
self.assertEqual(self.session.addon_ctx["MockAddon"]["another"], "baz")
|
||||||
|
|
||||||
# Outside session context it should raise
|
# Outside session context it should raise
|
||||||
with self.assertRaises(AttributeError):
|
with self.assertRaises(AttributeError):
|
||||||
@@ -104,7 +104,7 @@ class AddonIntegrationTests(BaseProxyTest):
|
|||||||
|
|
||||||
self.session.addon_ctx.clear()
|
self.session.addon_ctx.clear()
|
||||||
with addon_ctx.push(new_session=self.session):
|
with addon_ctx.push(new_session=self.session):
|
||||||
# This has no default so should fail
|
# This has no default so it should fail
|
||||||
with self.assertRaises(AttributeError):
|
with self.assertRaises(AttributeError):
|
||||||
_something = self.addon.bazquux
|
_something = self.addon.bazquux
|
||||||
# This has a default
|
# This has a default
|
||||||
@@ -144,9 +144,9 @@ class AddonIntegrationTests(BaseProxyTest):
|
|||||||
AddonManager.load_addon_from_path(str(self.parent_path), reload=True)
|
AddonManager.load_addon_from_path(str(self.parent_path), reload=True)
|
||||||
# Wait for the init hooks to run
|
# Wait for the init hooks to run
|
||||||
await asyncio.sleep(0.001)
|
await asyncio.sleep(0.001)
|
||||||
self.assertFalse("quux" in self.session_manager.addon_ctx)
|
self.assertFalse("quux" in self.session_manager.addon_ctx["ParentAddon"])
|
||||||
parent_addon_mod = AddonManager.FRESH_ADDON_MODULES['hippolyzer.user_addon_parent_addon']
|
parent_addon_mod = AddonManager.FRESH_ADDON_MODULES['hippolyzer.user_addon_parent_addon']
|
||||||
self.assertEqual(0, parent_addon_mod.ParentAddon.quux)
|
self.assertEqual(0, parent_addon_mod.ParentAddon.quux)
|
||||||
self.assertEqual(0, self.session_manager.addon_ctx["quux"])
|
self.assertEqual(0, self.session_manager.addon_ctx["ParentAddon"]["quux"])
|
||||||
parent_addon_mod.ParentAddon.quux = 1
|
parent_addon_mod.ParentAddon.quux = 1
|
||||||
self.assertEqual(1, self.session_manager.addon_ctx["quux"])
|
self.assertEqual(1, self.session_manager.addon_ctx["ParentAddon"]["quux"])
|
||||||
|
|||||||
Reference in New Issue
Block a user