clan-app: fixed broken webview delete_task

This commit is contained in:
Qubasa
2025-09-30 14:07:25 +02:00
parent b9b8b6d5be
commit d36f97aa6d
5 changed files with 78 additions and 57 deletions

View File

@@ -1,5 +1,6 @@
import logging import logging
import threading import threading
import uuid
from contextlib import ExitStack from contextlib import ExitStack
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Protocol from typing import TYPE_CHECKING, Any, Protocol
@@ -108,7 +109,13 @@ class ApiBridge(Protocol):
timeout: Timeout in seconds when waiting for completion 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: def thread_task(stop_event: threading.Event) -> None:
set_should_cancel(lambda: stop_event.is_set()) set_should_cancel(lambda: stop_event.is_set())
@@ -144,3 +151,15 @@ class ApiBridge(Protocol):
"Request timeout", "Request timeout",
["api_bridge", request.method_name], ["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)

View File

@@ -283,6 +283,29 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
) )
return None 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( def _handle_api_request(
self, self,
method_name: str, method_name: str,
@@ -315,41 +338,6 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
self._process_api_request_in_thread(api_request) 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( def process_request_in_thread(
self, self,
request: BackendRequest, request: BackendRequest,

View File

@@ -70,9 +70,32 @@ class Webview:
# initialized later # initialized later
_bridge: WebviewBridge | None = None _bridge: WebviewBridge | None = None
_handle: Any | 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) _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: def _create_handle(self) -> None:
# Initialize the webview handle # Initialize the webview handle
with_debugger = True with_debugger = True
@@ -84,12 +107,10 @@ class Webview:
) )
else: else:
handle = _webview_lib.webview_create(int(with_debugger), self.window) 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 # Since we can't use object.__setattr__, we'll initialize differently
# by storing in __dict__ directly (this works for init=False fields) # by storing in __dict__ directly (this works for init=False fields)
self._handle = handle self._handle = handle
self._callbacks = callbacks
if self.title: if self.title:
self.set_title(self.title) self.set_title(self.title)
@@ -162,6 +183,7 @@ class Webview:
"""Create and initialize the WebviewBridge with current middleware.""" """Create and initialize the WebviewBridge with current middleware."""
# Use shared_threads if provided, otherwise let WebviewBridge use its default # Use shared_threads if provided, otherwise let WebviewBridge use its default
if self.shared_threads is not None: if self.shared_threads is not None:
log.warning("create_bridge: Shared threads id: %s", id(self.shared_threads))
bridge = WebviewBridge( bridge = WebviewBridge(
webview=self, webview=self,
middleware_chain=tuple(self._middleware), middleware_chain=tuple(self._middleware),
@@ -193,7 +215,7 @@ class Webview:
def destroy(self) -> None: def destroy(self) -> None:
"""Destroy the webview.""" """Destroy the webview."""
for name in list(self._callbacks.keys()): for name in list(self.callbacks.keys()):
self.unbind(name) self.unbind(name)
_webview_lib.webview_terminate(self.handle) _webview_lib.webview_terminate(self.handle)
_webview_lib.webview_destroy(self.handle) _webview_lib.webview_destroy(self.handle)
@@ -217,11 +239,8 @@ class Webview:
) )
c_callback = _webview_lib.binding_callback_t(wrapper) c_callback = _webview_lib.binding_callback_t(wrapper)
if name in self._callbacks: self.add_callback(name, c_callback)
msg = f"Callback {name} already exists. Skipping binding."
raise RuntimeError(msg)
self._callbacks[name] = c_callback
_webview_lib.webview_bind( _webview_lib.webview_bind(
self.handle, self.handle,
_encode_c_string(name), _encode_c_string(name),
@@ -241,7 +260,7 @@ class Webview:
self.return_(seq.decode(), 0 if success else 1, json.dumps(result)) self.return_(seq.decode(), 0 if success else 1, json.dumps(result))
c_callback = _webview_lib.binding_callback_t(wrapper) 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) _webview_lib.webview_bind(self.handle, _encode_c_string(name), c_callback, None)
def get_native_handle( def get_native_handle(
@@ -260,9 +279,9 @@ class Webview:
return handle if handle else None return handle if handle else None
def unbind(self, name: str) -> 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)) _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: def return_(self, seq: str, status: int, result: str) -> None:
_webview_lib.webview_return( _webview_lib.webview_return(

View File

@@ -46,13 +46,11 @@ class WebviewBridge(ApiBridge):
) -> None: ) -> None:
"""Handle a call from webview's JavaScript bridge.""" """Handle a call from webview's JavaScript bridge."""
try: try:
op_key = op_key_bytes.decode() webview_op_key = op_key_bytes.decode()
raw_args = json.loads(request_data.decode()) raw_args = json.loads(request_data.decode())
# Parse the webview-specific request format # Parse the webview-specific request format
header = {} header = {}
args = {} args = {}
if len(raw_args) == 1: if len(raw_args) == 1:
request = raw_args[0] request = raw_args[0]
header = request.get("header", {}) header = request.get("header", {})
@@ -75,16 +73,14 @@ class WebviewBridge(ApiBridge):
method_name=method_name, method_name=method_name,
args=args, args=args,
header=header, header=header,
op_key=op_key, op_key=webview_op_key,
) )
except Exception as e: except Exception as e:
msg = ( msg = f"Error while handling webview call {method_name} with op_key {webview_op_key}"
f"Error while handling webview call {method_name} with op_key {op_key}"
)
log.exception(msg) log.exception(msg)
self.send_api_error_response( self.send_api_error_response(
op_key, webview_op_key,
str(e), str(e),
["webview_bridge", method_name], ["webview_bridge", method_name],
) )
@@ -93,6 +89,6 @@ class WebviewBridge(ApiBridge):
# Process in a separate thread using the inherited method # Process in a separate thread using the inherited method
self.process_request_in_thread( self.process_request_in_thread(
api_request, api_request,
thread_name="WebviewThread", thread_name=f"WebviewThread-{method_name}",
wait_for_completion=False, wait_for_completion=False,
) )

View File

@@ -23,7 +23,6 @@ class ArgumentParsingMiddleware(Middleware):
for k, v in context.request.args.items(): for k, v in context.request.args.items():
# Get the expected argument type from the API # Get the expected argument type from the API
arg_class = self.api.get_method_argtype(context.request.method_name, k) arg_class = self.api.get_method_argtype(context.request.method_name, k)
# Convert dictionary to dataclass instance # Convert dictionary to dataclass instance
reconciled_arguments[k] = from_dict(arg_class, v) reconciled_arguments[k] = from_dict(arg_class, v)