diff --git a/.vscode/settings.json b/.vscode/settings.json index 5b6fc7f..fca52b0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,4 +14,12 @@ "python.analysis.extraPaths": [ "${workspaceFolder}/src/clients/python/.venv/lib/python3.*/site-packages" ], + "debug.javascript.defaultRuntimeExecutable": { + "pwa-node": "/Users/just-be/.local/share/mise/shims/node" + }, + "ruff.path": [ + "/Users/just-be/.local/share/mise/shims/ruff" + ], + "deno.path": "/Users/just-be/.local/share/mise/shims/deno", + "python.defaultInterpreterPath": "/Users/just-be/Code/webview/src/clients/python/.venv/bin/python", } diff --git a/mise.toml b/mise.toml index b16056a..93dbcca 100644 --- a/mise.toml +++ b/mise.toml @@ -118,11 +118,11 @@ description = "Run a python example" depends = ["build:python"] dir = "src/clients/python" run = "uv run examples/{{arg(name=\"example\")}}.py" -env = { LOG_LEVEL = "debug", WEBVIEW_BIN = "../../../target/debug/webview" } +env = { LOG_LEVEL = "trace", WEBVIEW_BIN = "../../../target/debug/webview" } [tasks."example:deno"] description = "Run a deno example" depends = ["build:deno"] -env = { LOG_LEVEL = "debug", WEBVIEW_BIN = "../../../target/debug/webview" } +env = { LOG_LEVEL = "trace", WEBVIEW_BIN = "../../../target/debug/webview" } run = "deno run -E -R -N --allow-run examples/{{arg(name=\"example\")}}.ts" dir = "src/clients/deno" diff --git a/scripts/generate-schema/gen-python.ts b/scripts/generate-schema/gen-python.ts index 8e76a7e..863735c 100644 --- a/scripts/generate-schema/gen-python.ts +++ b/scripts/generate-schema/gen-python.ts @@ -105,13 +105,19 @@ function generateNode(node: Node, writer: Writer) { .with({ type: "union" }, (parent) => { const name = context.closestName(); const ident = node.properties.find((p) => p.required)?.key ?? ""; - wn(`class ${name}${cap(ident)}(msgspec.Struct, kw_only=True):`); + wn( + `class ${name}${ + cap(ident) + }(msgspec.Struct, kw_only=True, omit_defaults=True):`, + ); }) .with(P.nullish, () => { - wn(`class ${node.name}(msgspec.Struct):`); + wn(`class ${node.name}(msgspec.Struct, omit_defaults=True):`); }) .otherwise(() => { - wn(`class ${node.name}(msgspec.Struct, kw_only=True):`); + wn( + `class ${node.name}(msgspec.Struct, kw_only=True, omit_defaults=True):`, + ); }); if (node.description) { wn(` """`); diff --git a/src/clients/deno/main.ts b/src/clients/deno/main.ts index c0e497c..ca187ca 100644 --- a/src/clients/deno/main.ts +++ b/src/clients/deno/main.ts @@ -206,7 +206,9 @@ export class WebView implements Disposable { * @param webviewBinaryPath - The path to the webview binary. */ constructor(options: WebViewOptions, webviewBinaryPath: string) { + using _ = span(Level.TRACE, "WebView.constructor").enter(); this.#options = options; + trace("Creating webview", { options, webviewBinaryPath }); this.#process = new Deno.Command(webviewBinaryPath, { args: [JSON.stringify(options)], stdin: "piped", diff --git a/src/clients/python/main.py b/src/clients/python/main.py index 15e8a28..24cea24 100644 --- a/src/clients/python/main.py +++ b/src/clients/python/main.py @@ -2,6 +2,7 @@ import os import platform import subprocess +import sys from typing import Any, Callable, Literal, Union, cast from pathlib import Path import aiofiles @@ -52,24 +53,19 @@ # Constants BIN_VERSION = "0.1.14" -# Create options encoder for sending to the webview process -options_encoder = msgspec.json.Encoder() - -# Create decoders/encoders for the message types -message_decoder = msgspec.json.Decoder(WebViewMessage) -message_encoder = msgspec.json.Encoder() - def return_result( result: Union[AckResponse, ResultResponse, ErrResponse], expected_type: type[ResultType], ) -> Any: + print(f"Return result: {result}") if isinstance(result, ResultResponse) and isinstance(result.result, expected_type): return result.result.value raise ValueError(f"Expected {expected_type.__name__} result got: {result}") def return_ack(result: Union[AckResponse, ResultResponse, ErrResponse]) -> None: + print(f"Return ack: {result}") if isinstance(result, AckResponse): return if isinstance(result, ErrResponse): @@ -138,24 +134,23 @@ def get_cache_dir() -> Path: class WebView: def __init__(self, options: WebViewOptions, webview_binary_path: str): self.options = options + encoded_options = str(msgspec.json.encode(options), "utf-8") + print(f"Launching webview binary: {webview_binary_path}") + print(f"With options: {encoded_options}") self.process = subprocess.Popen( - [ - webview_binary_path, - options_encoder.encode( - msgspec.structs.asdict(cast(Any, options)) - ).decode(), - ], + [webview_binary_path, encoded_options], stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - bufsize=1, + stderr=2, # Capture stderr + text=False, # Ensure binary mode + bufsize=0, # Unbuffered + env=os.environ, ) assert self.process.stdin is not None assert self.process.stdout is not None self.internal_event = AsyncIOEventEmitter() self.external_event = AsyncIOEventEmitter() - self.buffer = "" + self.buffer = b"" async def send( self, request: WebViewRequest @@ -171,10 +166,8 @@ def set_result(event: Union[AckResponse, ResultResponse, ErrResponse]) -> None: self.internal_event.once(request_id, set_result) assert self.process.stdin is not None - self.process.stdin.write( - message_encoder.encode(msgspec.structs.asdict(cast(Any, request))).decode() - + "\0" - ) + encoded = msgspec.json.encode(request, omit_none=True) + self.process.stdin.write(encoded + b"\n") self.process.stdin.flush() result = await future @@ -182,25 +175,49 @@ def set_result(event: Union[AckResponse, ResultResponse, ErrResponse]) -> None: async def recv(self) -> Union[WebViewNotification, None]: assert self.process.stdout is not None + print("Receiving messages from webview process...") + print(f"Process poll status: {self.process.poll()}") + print(f"Process stdout closed: {self.process.stdout.closed}") + + # Check if process is still alive + if self.process.poll() is not None: + print(f"Process has terminated with code: {self.process.poll()}") + return None + while True: - chunk = await asyncio.to_thread(self.process.stdout.read, 1) - if not chunk: + try: + # Read up to 8KB at a time instead of byte-by-byte + print("Attempting to read from stdout...") + chunk = await asyncio.to_thread(self.process.stdout.read, 8192) + print(f"Read chunk size: {len(chunk) if chunk else 0} bytes") + if not chunk: + print("Received empty chunk, process may have closed stdout") + print(f"Final process status: {self.process.poll()}") + return None + + self.buffer += chunk + while b"\n" in self.buffer: # Process all complete messages in buffer + message, self.buffer = self.buffer.split(b"\n", 1) + print(f"Received raw message: {message}") + try: + msg = msgspec.json.decode(message) + print(f"Decoded message: {msg}") + if isinstance(msg, NotificationMessage): + return msg.data + elif isinstance(msg, ResponseMessage): + self.internal_event.emit(msg.data.id, msg.data) + except msgspec.DecodeError as e: + print(f"Error parsing message: {message}") + print(f"Parse error details: {str(e)}") + except Exception as e: + print(f"Error reading from stdout: {str(e)}") return None - self.buffer += chunk - if "\0" in self.buffer: - message, self.buffer = self.buffer.split("\0", 1) - try: - msg = message_decoder.decode(message.encode()) - if isinstance(msg, NotificationMessage): - return msg.data - elif isinstance(msg, ResponseMessage): - self.internal_event.emit(msg.data.id, msg.data) - except msgspec.DecodeError: - print(f"Error parsing message: {message}") async def process_message_loop(self): + print("Processing message loop") while True: notification = await self.recv() + print(f"Received notification: {notification}") if not notification: return @@ -321,4 +338,5 @@ async def create_webview( options: WebViewOptions, ) -> WebView: bin_path = await get_webview_bin(options) + print(f"Created webview with bin path: {bin_path}") return WebView(options, bin_path)