diff --git a/pkgs/clan-app/clan_app/api/api_bridge.py b/pkgs/clan-app/clan_app/api/api_bridge.py index a29e18bfe..0d4569238 100644 --- a/pkgs/clan-app/clan_app/api/api_bridge.py +++ b/pkgs/clan-app/clan_app/api/api_bridge.py @@ -1,5 +1,6 @@ import logging import threading +import uuid from contextlib import ExitStack from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Protocol @@ -108,7 +109,13 @@ class ApiBridge(Protocol): timeout: Timeout in seconds when waiting for completion """ - op_key = request.op_key or "unknown" + op_key = request.header.get("op_key", request.op_key) + if not isinstance(op_key, str): + msg = f"Expected op_key to be a string, got {type(op_key)}" + raise TypeError(msg) + + # Validate operation key + self._validate_operation_key(op_key) def thread_task(stop_event: threading.Event) -> None: set_should_cancel(lambda: stop_event.is_set()) @@ -144,3 +151,15 @@ class ApiBridge(Protocol): "Request timeout", ["api_bridge", request.method_name], ) + + def _validate_operation_key(self, op_key: str) -> None: + """Validate that the operation key is valid and not in use.""" + try: + uuid.UUID(op_key) + except ValueError as e: + msg = f"op_key '{op_key}' is not a valid UUID" + raise TypeError(msg) from e + + if op_key in self.threads: + msg = f"Operation key '{op_key}' is already in use. Please try again." + raise ValueError(msg) diff --git a/pkgs/clan-app/clan_app/deps/http/http_bridge.py b/pkgs/clan-app/clan_app/deps/http/http_bridge.py index 287f20459..ba52f3730 100644 --- a/pkgs/clan-app/clan_app/deps/http/http_bridge.py +++ b/pkgs/clan-app/clan_app/deps/http/http_bridge.py @@ -283,6 +283,29 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler): ) return None + def _parse_request_data( + self, + request_data: dict[str, Any], + gen_op_key: str, + ) -> tuple[dict[str, Any], dict[str, Any], str]: + """Parse and validate request data components.""" + header = request_data.get("header", {}) + if not isinstance(header, dict): + msg = f"Expected header to be a dict, got {type(header)}" + raise TypeError(msg) + + body = request_data.get("body", {}) + if not isinstance(body, dict): + msg = f"Expected body to be a dict, got {type(body)}" + raise TypeError(msg) + + op_key = header.get("op_key", gen_op_key) + if not isinstance(op_key, str): + msg = f"Expected op_key to be a string, got {type(op_key)}" + raise TypeError(msg) + + return header, body, op_key + def _handle_api_request( self, method_name: str, @@ -315,41 +338,6 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler): self._process_api_request_in_thread(api_request) - def _parse_request_data( - self, - request_data: dict[str, Any], - gen_op_key: str, - ) -> tuple[dict[str, Any], dict[str, Any], str]: - """Parse and validate request data components.""" - header = request_data.get("header", {}) - if not isinstance(header, dict): - msg = f"Expected header to be a dict, got {type(header)}" - raise TypeError(msg) - - body = request_data.get("body", {}) - if not isinstance(body, dict): - msg = f"Expected body to be a dict, got {type(body)}" - raise TypeError(msg) - - op_key = header.get("op_key", gen_op_key) - if not isinstance(op_key, str): - msg = f"Expected op_key to be a string, got {type(op_key)}" - raise TypeError(msg) - - return header, body, op_key - - def _validate_operation_key(self, op_key: str) -> None: - """Validate that the operation key is valid and not in use.""" - try: - uuid.UUID(op_key) - except ValueError as e: - msg = f"op_key '{op_key}' is not a valid UUID" - raise TypeError(msg) from e - - if op_key in self.threads: - msg = f"Operation key '{op_key}' is already in use. Please try again." - raise ValueError(msg) - def process_request_in_thread( self, request: BackendRequest, diff --git a/pkgs/clan-app/clan_app/deps/webview/webview.py b/pkgs/clan-app/clan_app/deps/webview/webview.py index 4e2f38114..6136b8832 100644 --- a/pkgs/clan-app/clan_app/deps/webview/webview.py +++ b/pkgs/clan-app/clan_app/deps/webview/webview.py @@ -70,9 +70,32 @@ class Webview: # initialized later _bridge: WebviewBridge | None = None _handle: Any | None = None - _callbacks: dict[str, Callable[..., Any]] = field(default_factory=dict) + __callbacks: dict[str, Callable[..., Any]] = field(default_factory=dict) _middleware: list["Middleware"] = field(default_factory=list) + @property + def callbacks(self) -> dict[str, Callable[..., Any]]: + return self.__callbacks + + @callbacks.setter + def callbacks(self, value: dict[str, Callable[..., Any]]) -> None: + del value # Unused + msg = "Cannot set callbacks directly" + raise AttributeError(msg) + + def delete_callback(self, name: str) -> None: + if name in self.callbacks: + del self.__callbacks[name] + else: + msg = f"Callback {name} does not exist. Cannot delete." + raise RuntimeError(msg) + + def add_callback(self, name: str, callback: Callable[..., Any]) -> None: + if name in self.callbacks: + msg = f"Callback {name} already exists. Cannot add." + raise RuntimeError(msg) + self.__callbacks[name] = callback + def _create_handle(self) -> None: # Initialize the webview handle with_debugger = True @@ -84,12 +107,10 @@ class Webview: ) else: handle = _webview_lib.webview_create(int(with_debugger), self.window) - callbacks: dict[str, Callable[..., Any]] = {} # Since we can't use object.__setattr__, we'll initialize differently # by storing in __dict__ directly (this works for init=False fields) self._handle = handle - self._callbacks = callbacks if self.title: self.set_title(self.title) @@ -162,6 +183,7 @@ class Webview: """Create and initialize the WebviewBridge with current middleware.""" # Use shared_threads if provided, otherwise let WebviewBridge use its default if self.shared_threads is not None: + log.warning("create_bridge: Shared threads id: %s", id(self.shared_threads)) bridge = WebviewBridge( webview=self, middleware_chain=tuple(self._middleware), @@ -193,7 +215,7 @@ class Webview: def destroy(self) -> None: """Destroy the webview.""" - for name in list(self._callbacks.keys()): + for name in list(self.callbacks.keys()): self.unbind(name) _webview_lib.webview_terminate(self.handle) _webview_lib.webview_destroy(self.handle) @@ -217,11 +239,8 @@ class Webview: ) c_callback = _webview_lib.binding_callback_t(wrapper) - if name in self._callbacks: - msg = f"Callback {name} already exists. Skipping binding." - raise RuntimeError(msg) + self.add_callback(name, c_callback) - self._callbacks[name] = c_callback _webview_lib.webview_bind( self.handle, _encode_c_string(name), @@ -241,7 +260,7 @@ class Webview: self.return_(seq.decode(), 0 if success else 1, json.dumps(result)) c_callback = _webview_lib.binding_callback_t(wrapper) - self._callbacks[name] = c_callback + self.add_callback(name, c_callback) _webview_lib.webview_bind(self.handle, _encode_c_string(name), c_callback, None) def get_native_handle( @@ -260,9 +279,9 @@ class Webview: return handle if handle else None def unbind(self, name: str) -> None: - if name in self._callbacks: + if name in self.callbacks: _webview_lib.webview_unbind(self.handle, _encode_c_string(name)) - del self._callbacks[name] + self.delete_callback(name) def return_(self, seq: str, status: int, result: str) -> None: _webview_lib.webview_return( diff --git a/pkgs/clan-app/clan_app/deps/webview/webview_bridge.py b/pkgs/clan-app/clan_app/deps/webview/webview_bridge.py index 130e689b7..3a7175981 100644 --- a/pkgs/clan-app/clan_app/deps/webview/webview_bridge.py +++ b/pkgs/clan-app/clan_app/deps/webview/webview_bridge.py @@ -46,13 +46,11 @@ class WebviewBridge(ApiBridge): ) -> None: """Handle a call from webview's JavaScript bridge.""" try: - op_key = op_key_bytes.decode() + webview_op_key = op_key_bytes.decode() raw_args = json.loads(request_data.decode()) - # Parse the webview-specific request format header = {} args = {} - if len(raw_args) == 1: request = raw_args[0] header = request.get("header", {}) @@ -75,16 +73,14 @@ class WebviewBridge(ApiBridge): method_name=method_name, args=args, header=header, - op_key=op_key, + op_key=webview_op_key, ) except Exception as e: - msg = ( - f"Error while handling webview call {method_name} with op_key {op_key}" - ) + msg = f"Error while handling webview call {method_name} with op_key {webview_op_key}" log.exception(msg) self.send_api_error_response( - op_key, + webview_op_key, str(e), ["webview_bridge", method_name], ) @@ -93,6 +89,6 @@ class WebviewBridge(ApiBridge): # Process in a separate thread using the inherited method self.process_request_in_thread( api_request, - thread_name="WebviewThread", + thread_name=f"WebviewThread-{method_name}", wait_for_completion=False, ) diff --git a/pkgs/clan-app/clan_app/middleware/argument_parsing.py b/pkgs/clan-app/clan_app/middleware/argument_parsing.py index 10e97bb62..8e580e843 100644 --- a/pkgs/clan-app/clan_app/middleware/argument_parsing.py +++ b/pkgs/clan-app/clan_app/middleware/argument_parsing.py @@ -23,7 +23,6 @@ class ArgumentParsingMiddleware(Middleware): for k, v in context.request.args.items(): # Get the expected argument type from the API arg_class = self.api.get_method_argtype(context.request.method_name, k) - # Convert dictionary to dataclass instance reconciled_arguments[k] = from_dict(arg_class, v)