WEFT_OS/crates/weft-appd/src/ws.rs
Marco Allegretti 7cebac4188 feat(appd): add WebSocket UI endpoint for Servo shell integration
Implements the weft-appd WebSocket server that allows the system-ui.html
page running inside Servo to send requests and receive push notifications
without requiring custom SpiderMonkey bindings.

ws.rs — WebSocket connection handler:
- Accepts a tokio TcpStream, performs WebSocket handshake via
  tokio-tungstenite accept_async.
- Reads JSON Text frames, deserializes as Request (serde_json), calls
  dispatch(), sends Response as JSON Text.
- Subscribes to a broadcast::Receiver<Response> for server-push
  notifications (APP_READY, etc.); forwards to client via select!.
- Handles close frames, partial errors, and lagged broadcast gracefully.

main.rs — server changes:
- broadcast::channel(16) created at startup; WebSocket handlers
  subscribe for push delivery.
- TcpListener bound on 127.0.0.1:7410 (default) or WEFT_APPD_WS_PORT.
- ws_port() / write_ws_port(): port written to
  XDG_RUNTIME_DIR/weft/appd.wsport for runtime discovery.
- WS accept branch added to the main select! loop alongside Unix socket.

ipc.rs — Response and AppStateKind now derive Clone (required by
broadcast::Sender<Response>).

system-ui.html — appd WebSocket client:
- appdConnect(): opens ws://127.0.0.1:<port>/appd with exponential
  backoff reconnect (1s → 16s max).
- On open: sends QUERY_RUNNING to populate taskbar with live sessions.
- handleAppdMessage(): maps LAUNCH_ACK and RUNNING_APPS to taskbar
  entries; APP_READY shows a timed notification; APP_STATE::stopped
  removes the taskbar entry.
- WEFT_APPD_WS_PORT window global overrides the default port.

New deps: tokio-tungstenite 0.24, futures-util 0.3 (sink+std),
serde_json 1.
2026-03-11 09:01:54 +01:00

54 lines
2 KiB
Rust

use futures_util::{SinkExt, StreamExt};
use tokio::sync::broadcast;
use tokio_tungstenite::{accept_async, tungstenite::Message};
use crate::{Registry, dispatch, ipc::Request, ipc::Response};
pub async fn handle_ws_connection(
stream: tokio::net::TcpStream,
registry: Registry,
broadcast_rx: broadcast::Receiver<Response>,
) -> anyhow::Result<()> {
let ws_stream = accept_async(stream).await?;
let (mut ws_write, mut ws_read) = ws_stream.split();
let mut broadcast_rx = broadcast_rx;
loop {
tokio::select! {
msg = ws_read.next() => {
match msg {
Some(Ok(Message::Text(text))) => {
let req: Request = match serde_json::from_str(&text) {
Ok(r) => r,
Err(e) => {
tracing::warn!(error = %e, "invalid WS request");
continue;
}
};
tracing::debug!(?req, "ws request");
let resp = dispatch(req, &registry).await;
let json = serde_json::to_string(&resp)?;
ws_write.send(Message::Text(json)).await?;
}
Some(Ok(Message::Close(_))) | None => break,
Some(Ok(_)) => {}
Some(Err(e)) => {
tracing::warn!(error = %e, "ws error");
break;
}
}
}
notification = broadcast_rx.recv() => {
match notification {
Ok(resp) => {
let json = serde_json::to_string(&resp)?;
ws_write.send(Message::Text(json)).await?;
}
Err(broadcast::error::RecvError::Lagged(_)) => {}
Err(broadcast::error::RecvError::Closed) => break,
}
}
}
}
Ok(())
}