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 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)

View File

@@ -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,

View File

@@ -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(

View File

@@ -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,
)

View File

@@ -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)