clan-app: fixed broken webview delete_task
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user