attempt to add plugin events to the plugin frontend api.

unable to test right now though
This commit is contained in:
Party Wumpus
2024-04-09 15:44:38 +01:00
committed by PartyWumpus
parent de9d2144a6
commit f9ff518e6d
6 changed files with 57 additions and 21 deletions
+3 -11
View File
@@ -8,7 +8,6 @@ from typing import Any, Tuple, Dict
from aiohttp import web from aiohttp import web
from os.path import exists from os.path import exists
from attr import dataclass
from watchdog.events import RegexMatchingEventHandler, DirCreatedEvent, DirModifiedEvent, FileCreatedEvent, FileModifiedEvent # type: ignore from watchdog.events import RegexMatchingEventHandler, DirCreatedEvent, DirModifiedEvent, FileCreatedEvent, FileModifiedEvent # type: ignore
from watchdog.observers import Observer # type: ignore from watchdog.observers import Observer # type: ignore
@@ -66,12 +65,6 @@ class FileChangeHandler(RegexMatchingEventHandler):
self.logger.debug(f"file modified: {src_path}") self.logger.debug(f"file modified: {src_path}")
self.maybe_reload(src_path) self.maybe_reload(src_path)
@dataclass
class PluginEvent:
plugin_name: str
event: str
data: str
class Loader: class Loader:
def __init__(self, server_instance: PluginManager, ws: WSRouter, plugin_path: str, loop: AbstractEventLoop, live_reload: bool = False) -> None: def __init__(self, server_instance: PluginManager, ws: WSRouter, plugin_path: str, loop: AbstractEventLoop, live_reload: bool = False) -> None:
self.loop = loop self.loop = loop
@@ -149,10 +142,9 @@ class Loader:
def import_plugin(self, file: str, plugin_directory: str, refresh: bool | None = False, batch: bool | None = False): def import_plugin(self, file: str, plugin_directory: str, refresh: bool | None = False, batch: bool | None = False):
try: try:
async def plugin_emitted_event(event: str, data: Any): async def plugin_emitted_event(event: str, args: Any):
self.logger.debug(f"PLUGIN EMITTED EVENT: {str(event)} {data}") self.logger.debug(f"PLUGIN EMITTED EVENT: {event} with args {args}")
event_data = PluginEvent(plugin_name=plugin.name, event=event, data=data) await self.ws.emit(f"loader/plugin_event", {plugin:plugin.name, event:event, args:args})
await self.ws.emit("loader/plugin_event", event_data)
plugin = PluginWrapper(file, plugin_directory, self.plugin_path, plugin_emitted_event) plugin = PluginWrapper(file, plugin_directory, self.plugin_path, plugin_emitted_event)
if plugin.name in self.plugins: if plugin.name in self.plugins:
+2 -3
View File
@@ -19,7 +19,7 @@ import subprocess
import logging import logging
import time import time
from typing import TypeVar, Type from typing import Any
""" """
Constants Constants
@@ -213,9 +213,8 @@ logger.setLevel(logging.INFO)
""" """
Event handling Event handling
""" """
DataType = TypeVar("DataType")
# TODO better docstring im lazy # TODO better docstring im lazy
async def emit(event: str, data: DataType | None = None, data_type: Type[DataType] | None = None) -> None: async def emit(event: str, *args: Any) -> None:
""" """
Send an event to the frontend. Send an event to the frontend.
""" """
@@ -16,7 +16,7 @@ __version__ = '0.1.0'
import logging import logging
from typing import TypeVar, Type from typing import Any
""" """
Constants Constants
@@ -177,9 +177,8 @@ logger: logging.Logger
""" """
Event handling Event handling
""" """
DataType = TypeVar("DataType")
# TODO better docstring im lazy # TODO better docstring im lazy
async def emit(event: str, data: DataType | None = None, data_type: Type[DataType] | None = None) -> None: async def emit(event: str, *args: Any) -> None:
""" """
Send an event to the frontend. Send an event to the frontend.
""" """
+1 -1
View File
@@ -59,7 +59,7 @@ class PluginWrapper:
if line != None: if line != None:
res = loads(line) res = loads(line)
if res["type"] == SocketMessageType.EVENT.value: if res["type"] == SocketMessageType.EVENT.value:
create_task(self.emitted_event_callback(res["event"], res["data"])) create_task(self.emitted_event_callback(res["event"], res["args"]))
elif res["type"] == SocketMessageType.RESPONSE.value: elif res["type"] == SocketMessageType.RESPONSE.value:
self._method_call_requests.pop(res["id"]).set_result(res) self._method_call_requests.pop(res["id"]).set_result(res)
except: except:
@@ -14,7 +14,7 @@ from ..localplatform.localplatform import setgid, setuid, get_username, get_home
from ..enums import UserType from ..enums import UserType
from .. import helpers from .. import helpers
from typing import List, TypeVar, Type from typing import List, TypeVar, Any
DataType = TypeVar("DataType") DataType = TypeVar("DataType")
@@ -83,11 +83,11 @@ class SandboxedPlugin:
sysmodules[key.replace("decky_loader.", "")] = sysmodules[key] sysmodules[key.replace("decky_loader.", "")] = sysmodules[key]
from .imports import decky from .imports import decky
async def emit(event: str, data: DataType | None = None, data_type: Type[DataType] | None = None) -> None: async def emit(event: str, *args: Any) -> None:
await self._socket.write_single_line_server(dumps({ await self._socket.write_single_line_server(dumps({
"type": SocketMessageType.EVENT, "type": SocketMessageType.EVENT,
"event": event, "event": event,
"data": data "args": args
})) }))
# copy the docstring over so we don't have to duplicate it # copy the docstring over so we don't have to duplicate it
emit.__doc__ = decky.emit.__doc__ emit.__doc__ = decky.emit.__doc__
+46
View File
@@ -48,6 +48,9 @@ declare global {
} }
} }
/** Map of event names to event listeners */
type listenerMap = Map<string, Set<(...args: any) => any>>;
const callPluginMethod = DeckyBackend.callable<[pluginName: string, method: string, ...args: any], any>( const callPluginMethod = DeckyBackend.callable<[pluginName: string, method: string, ...args: any], any>(
'loader/call_plugin_method', 'loader/call_plugin_method',
); );
@@ -58,6 +61,8 @@ class PluginLoader extends Logger {
private routerHook: RouterHook = new RouterHook(); private routerHook: RouterHook = new RouterHook();
public toaster: Toaster = new Toaster(); public toaster: Toaster = new Toaster();
private deckyState: DeckyState = new DeckyState(); private deckyState: DeckyState = new DeckyState();
// stores a map of plugin names to all their event listeners
private pluginEventListeners: Map<string, listenerMap> = new Map();
public frozenPluginsService = new FrozenPluginService(this.deckyState); public frozenPluginsService = new FrozenPluginService(this.deckyState);
public hiddenPluginsService = new HiddenPluginsService(this.deckyState); public hiddenPluginsService = new HiddenPluginsService(this.deckyState);
@@ -81,6 +86,7 @@ class PluginLoader extends Logger {
DeckyBackend.addEventListener('updater/update_download_percentage', () => { DeckyBackend.addEventListener('updater/update_download_percentage', () => {
this.deckyState.setIsLoaderUpdating(true); this.deckyState.setIsLoaderUpdating(true);
}); });
DeckyBackend.addEventListener(`loader/plugin_event`, this.pluginEventListener);
this.tabsHook.init(); this.tabsHook.init();
@@ -513,6 +519,9 @@ class PluginLoader extends Logger {
throw new Error(`Plugin ${pluginName} requested invalid backend api version ${version}.`); throw new Error(`Plugin ${pluginName} requested invalid backend api version ${version}.`);
} }
const eventListeners: listenerMap = new Map();
this.pluginEventListeners.set(pluginName, eventListeners);
const backendAPI = { const backendAPI = {
call: (methodName: string, ...args: any) => { call: (methodName: string, ...args: any) => {
return callPluginMethod(pluginName, methodName, ...args); return callPluginMethod(pluginName, methodName, ...args);
@@ -520,6 +529,20 @@ class PluginLoader extends Logger {
callable: (methodName: string) => { callable: (methodName: string) => {
return (...args: any) => callPluginMethod(pluginName, methodName, ...args); return (...args: any) => callPluginMethod(pluginName, methodName, ...args);
}, },
addEventListener: (event: string, listener: (...args: any) => any) => {
if (!eventListeners.has(event)) {
eventListeners.set(event, new Set([listener]));
} else {
eventListeners.get(event)?.add(listener);
}
return listener;
},
removeEventListener: (event: string, listener: (...args: any) => any) => {
if (eventListeners.has(event)) {
const set = eventListeners.get(event);
set?.delete(listener);
}
},
}; };
this.debug(`${pluginName} connected to backend API.`); this.debug(`${pluginName} connected to backend API.`);
@@ -528,6 +551,29 @@ class PluginLoader extends Logger {
}; };
} }
pluginEventListener = (data: { plugin: string; event: string; args: any }) => {
const { plugin, event, args } = data;
this.debug(`Recieved plugin event ${event} for ${plugin} with args`, args);
if (!this.pluginEventListeners.has(plugin)) {
this.warn(`plugin ${plugin} does not have event listeners`);
return;
}
const eventListeners = this.pluginEventListeners.get(plugin)!;
if (eventListeners.has(event)) {
for (const listener of eventListeners.get(event)!) {
(async () => {
try {
await listener(...args);
} catch (e) {
this.error(`error in event ${event}`, e, listener);
}
})();
}
} else {
this.warn(`event ${event} has no listeners`);
}
};
createLegacyPluginAPI(pluginName: string) { createLegacyPluginAPI(pluginName: string) {
const pluginAPI = { const pluginAPI = {
routerHook: this.routerHook, routerHook: this.routerHook,