WEFT_OS/crates/weft-appd/src/ipc.rs
Marco Allegretti 5d7c0bdf79 feat(appd): add version field to AppInfo; surface it in launcher tile tooltip
WappPackage and AppInfo both gain a version field. scan_installed_apps()
reads it from wapp.toml and includes it in InstalledApps responses.
system-ui.html shows it in the title tooltip as 'com.example.app v1.0.0'.
All roundtrip and integration tests updated.
2026-03-11 13:15:09 +01:00

256 lines
7.6 KiB
Rust

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,
pub version: String,
}
#[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");
}
#[test]
fn app_info_roundtrip() {
let info = super::AppInfo {
app_id: "com.example.app".into(),
name: "Example App".into(),
version: "1.2.3".into(),
};
let bytes = rmp_serde::to_vec(&info).unwrap();
let decoded: super::AppInfo = rmp_serde::from_slice(&bytes).unwrap();
assert_eq!(decoded.app_id, "com.example.app");
assert_eq!(decoded.name, "Example App");
assert_eq!(decoded.version, "1.2.3");
}
#[test]
fn query_installed_apps_request_roundtrip() {
let req = Request::QueryInstalledApps;
let bytes = rmp_serde::to_vec(&req).unwrap();
let decoded: Request = rmp_serde::from_slice(&bytes).unwrap();
assert!(matches!(decoded, Request::QueryInstalledApps));
}
#[test]
fn installed_apps_response_roundtrip() {
let resp = Response::InstalledApps {
apps: vec![super::AppInfo {
app_id: "com.example.app".into(),
name: "Example App".into(),
version: "0.9.0".into(),
}],
};
let bytes = rmp_serde::to_vec(&resp).unwrap();
let decoded: Response = rmp_serde::from_slice(&bytes).unwrap();
match decoded {
Response::InstalledApps { apps } => {
assert_eq!(apps.len(), 1);
assert_eq!(apps[0].app_id, "com.example.app");
assert_eq!(apps[0].name, "Example App");
assert_eq!(apps[0].version, "0.9.0");
}
_ => panic!("wrong variant"),
}
}
#[test]
fn terminate_app_request_roundtrip() {
let req = Request::TerminateApp { session_id: 42 };
let bytes = rmp_serde::to_vec(&req).unwrap();
let decoded: Request = rmp_serde::from_slice(&bytes).unwrap();
assert!(matches!(decoded, Request::TerminateApp { session_id: 42 }));
}
#[test]
fn error_response_roundtrip() {
let resp = Response::Error {
code: 1,
message: "not found".into(),
};
let bytes = rmp_serde::to_vec(&resp).unwrap();
let decoded: Response = rmp_serde::from_slice(&bytes).unwrap();
match decoded {
Response::Error { code, message } => {
assert_eq!(code, 1);
assert_eq!(message, "not found");
}
_ => panic!("wrong variant"),
}
}
#[test]
fn app_state_response_roundtrip() {
let resp = Response::AppState {
session_id: 5,
state: super::AppStateKind::Running,
};
let bytes = rmp_serde::to_vec(&resp).unwrap();
let decoded: Response = rmp_serde::from_slice(&bytes).unwrap();
assert!(matches!(
decoded,
Response::AppState {
session_id: 5,
state: super::AppStateKind::Running
}
));
}
#[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());
}
}