Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
zephraph committed Feb 2, 2025
1 parent 744c23d commit 8a5fd73
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 39 deletions.
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
4 changes: 2 additions & 2 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
12 changes: 9 additions & 3 deletions scripts/generate-schema/gen-python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(` """`);
Expand Down
2 changes: 2 additions & 0 deletions src/clients/deno/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
86 changes: 52 additions & 34 deletions src/clients/python/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -171,36 +166,58 @@ 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
return result

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

Expand Down Expand Up @@ -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)

0 comments on commit 8a5fd73

Please sign in to comment.