WEFT_OS/crates/weft-appd/src/ipc.rs

170 lines
4.7 KiB
Rust
Raw Normal View History

use serde::{Deserialize, Serialize};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Request {
LaunchApp { app_id: String, surface_id: u64 },
TerminateApp { session_id: u64 },
QueryRunning,
QueryAppState { session_id: u64 },
QueryInstalledApps,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInfo {
pub session_id: u64,
pub app_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppInfo {
pub app_id: String,
pub name: String,
}
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 08:01:54 +00:00
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Response {
LaunchAck {
session_id: u64,
app_id: String,
},
AppReady {
session_id: u64,
app_id: String,
},
RunningApps {
sessions: Vec<SessionInfo>,
},
AppState {
session_id: u64,
state: AppStateKind,
},
InstalledApps {
apps: Vec<AppInfo>,
},
Error {
code: u32,
message: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AppStateKind {
Starting,
Running,
Stopping,
Stopped,
NotFound,
}
pub async fn read_frame(
reader: &mut (impl AsyncReadExt + Unpin),
) -> anyhow::Result<Option<Request>> {
let mut len_buf = [0u8; 4];
match reader.read_exact(&mut len_buf).await {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e.into()),
}
let len = u32::from_le_bytes(len_buf) as usize;
let mut body = vec![0u8; len];
reader.read_exact(&mut body).await?;
let req = rmp_serde::from_slice(&body)?;
Ok(Some(req))
}
pub async fn write_frame(
writer: &mut (impl AsyncWriteExt + Unpin),
response: &Response,
) -> anyhow::Result<()> {
let body = rmp_serde::to_vec(response)?;
let len = (body.len() as u32).to_le_bytes();
writer.write_all(&len).await?;
writer.write_all(&body).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_msgpack_roundtrip() {
let req = Request::LaunchApp {
app_id: "com.example.app".into(),
surface_id: 42,
};
let bytes = rmp_serde::to_vec(&req).unwrap();
let decoded: Request = rmp_serde::from_slice(&bytes).unwrap();
match decoded {
Request::LaunchApp { app_id, surface_id } => {
assert_eq!(app_id, "com.example.app");
assert_eq!(surface_id, 42);
}
_ => panic!("wrong variant"),
}
}
#[test]
fn response_msgpack_roundtrip() {
let resp = Response::LaunchAck {
session_id: 7,
app_id: "com.example.app".into(),
};
let bytes = rmp_serde::to_vec(&resp).unwrap();
let decoded: Response = rmp_serde::from_slice(&bytes).unwrap();
match decoded {
Response::LaunchAck { session_id, app_id } => {
assert_eq!(session_id, 7);
assert_eq!(app_id, "com.example.app");
}
_ => panic!("wrong variant"),
}
}
#[test]
fn session_info_roundtrip() {
let info = super::SessionInfo {
session_id: 3,
app_id: "com.example.app".into(),
};
let bytes = rmp_serde::to_vec(&info).unwrap();
let decoded: super::SessionInfo = rmp_serde::from_slice(&bytes).unwrap();
assert_eq!(decoded.session_id, 3);
assert_eq!(decoded.app_id, "com.example.app");
}
#[tokio::test]
async fn frame_write_read_roundtrip() {
let resp = Response::RunningApps { sessions: vec![] };
let mut buf: Vec<u8> = Vec::new();
write_frame(&mut buf, &resp).await.unwrap();
assert_eq!(
buf.len() as u32,
u32::from_le_bytes(buf[..4].try_into().unwrap()) + 4
);
let req_to_write = Request::QueryRunning;
let mut req_buf: Vec<u8> = Vec::new();
let body = rmp_serde::to_vec(&req_to_write).unwrap();
let len = (body.len() as u32).to_le_bytes();
req_buf.extend_from_slice(&len);
req_buf.extend_from_slice(&body);
let mut cursor = std::io::Cursor::new(req_buf);
let decoded = read_frame(&mut cursor).await.unwrap();
assert!(matches!(decoded, Some(Request::QueryRunning)));
}
#[tokio::test]
async fn read_frame_eof_returns_none() {
let mut empty = std::io::Cursor::new(Vec::<u8>::new());
let result = read_frame(&mut empty).await.unwrap();
assert!(result.is_none());
}
}