feat(appd): implement IPC server with Unix socket and MessagePack framing
Replaces the skeleton bail with a functional IPC server.
ipc.rs — transport layer:
- Request enum: LaunchApp, TerminateApp, QueryRunning, QueryAppState.
Serialized with serde MessagePack (rmp-serde, SCREAMING_SNAKE_CASE
type tag).
- Response enum: LaunchAck, AppReady, RunningApps, AppState, Error.
- AppStateKind: Starting, Running, Stopping, Stopped, NotFound.
- read_frame / write_frame: async 4-byte LE length-prefixed codec over
any AsyncRead / AsyncWrite.
main.rs — server:
- SessionRegistry: in-memory HashMap<session_id, AppStateKind> with
monotonic ID counter; launch / terminate / running_ids / state.
- run(): creates socket parent directory, removes stale socket, binds
UnixListener, sends sd_notify READY=1, then accept-loops with
ctrl-c / SIGTERM shutdown. Cleans up socket on exit.
- handle_connection(): splits stream into BufReader/BufWriter, reads
request frames, calls dispatch, writes response frames.
- dispatch(): maps Request variants to SessionRegistry operations;
returns typed Response. Wasmtime spawning and compositor client
deferred to later implementation.
New deps: serde (derive), rmp-serde, tokio net/io-util/sync/rt-multi-thread.
2026-03-11 07:25:55 +00:00
|
|
|
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 },
|
2026-03-11 10:23:46 +00:00
|
|
|
QueryInstalledApps,
|
feat(appd): implement IPC server with Unix socket and MessagePack framing
Replaces the skeleton bail with a functional IPC server.
ipc.rs — transport layer:
- Request enum: LaunchApp, TerminateApp, QueryRunning, QueryAppState.
Serialized with serde MessagePack (rmp-serde, SCREAMING_SNAKE_CASE
type tag).
- Response enum: LaunchAck, AppReady, RunningApps, AppState, Error.
- AppStateKind: Starting, Running, Stopping, Stopped, NotFound.
- read_frame / write_frame: async 4-byte LE length-prefixed codec over
any AsyncRead / AsyncWrite.
main.rs — server:
- SessionRegistry: in-memory HashMap<session_id, AppStateKind> with
monotonic ID counter; launch / terminate / running_ids / state.
- run(): creates socket parent directory, removes stale socket, binds
UnixListener, sends sd_notify READY=1, then accept-loops with
ctrl-c / SIGTERM shutdown. Cleans up socket on exit.
- handle_connection(): splits stream into BufReader/BufWriter, reads
request frames, calls dispatch, writes response frames.
- dispatch(): maps Request variants to SessionRegistry operations;
returns typed Response. Wasmtime spawning and compositor client
deferred to later implementation.
New deps: serde (derive), rmp-serde, tokio net/io-util/sync/rt-multi-thread.
2026-03-11 07:25:55 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-11 09:42:40 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct SessionInfo {
|
|
|
|
|
pub session_id: u64,
|
|
|
|
|
pub app_id: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 10:23:46 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct AppInfo {
|
|
|
|
|
pub app_id: String,
|
|
|
|
|
pub name: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 08:01:54 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
feat(appd): implement IPC server with Unix socket and MessagePack framing
Replaces the skeleton bail with a functional IPC server.
ipc.rs — transport layer:
- Request enum: LaunchApp, TerminateApp, QueryRunning, QueryAppState.
Serialized with serde MessagePack (rmp-serde, SCREAMING_SNAKE_CASE
type tag).
- Response enum: LaunchAck, AppReady, RunningApps, AppState, Error.
- AppStateKind: Starting, Running, Stopping, Stopped, NotFound.
- read_frame / write_frame: async 4-byte LE length-prefixed codec over
any AsyncRead / AsyncWrite.
main.rs — server:
- SessionRegistry: in-memory HashMap<session_id, AppStateKind> with
monotonic ID counter; launch / terminate / running_ids / state.
- run(): creates socket parent directory, removes stale socket, binds
UnixListener, sends sd_notify READY=1, then accept-loops with
ctrl-c / SIGTERM shutdown. Cleans up socket on exit.
- handle_connection(): splits stream into BufReader/BufWriter, reads
request frames, calls dispatch, writes response frames.
- dispatch(): maps Request variants to SessionRegistry operations;
returns typed Response. Wasmtime spawning and compositor client
deferred to later implementation.
New deps: serde (derive), rmp-serde, tokio net/io-util/sync/rt-multi-thread.
2026-03-11 07:25:55 +00:00
|
|
|
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
|
|
|
|
|
pub enum Response {
|
|
|
|
|
LaunchAck {
|
|
|
|
|
session_id: u64,
|
2026-03-11 09:46:28 +00:00
|
|
|
app_id: String,
|
feat(appd): implement IPC server with Unix socket and MessagePack framing
Replaces the skeleton bail with a functional IPC server.
ipc.rs — transport layer:
- Request enum: LaunchApp, TerminateApp, QueryRunning, QueryAppState.
Serialized with serde MessagePack (rmp-serde, SCREAMING_SNAKE_CASE
type tag).
- Response enum: LaunchAck, AppReady, RunningApps, AppState, Error.
- AppStateKind: Starting, Running, Stopping, Stopped, NotFound.
- read_frame / write_frame: async 4-byte LE length-prefixed codec over
any AsyncRead / AsyncWrite.
main.rs — server:
- SessionRegistry: in-memory HashMap<session_id, AppStateKind> with
monotonic ID counter; launch / terminate / running_ids / state.
- run(): creates socket parent directory, removes stale socket, binds
UnixListener, sends sd_notify READY=1, then accept-loops with
ctrl-c / SIGTERM shutdown. Cleans up socket on exit.
- handle_connection(): splits stream into BufReader/BufWriter, reads
request frames, calls dispatch, writes response frames.
- dispatch(): maps Request variants to SessionRegistry operations;
returns typed Response. Wasmtime spawning and compositor client
deferred to later implementation.
New deps: serde (derive), rmp-serde, tokio net/io-util/sync/rt-multi-thread.
2026-03-11 07:25:55 +00:00
|
|
|
},
|
|
|
|
|
AppReady {
|
|
|
|
|
session_id: u64,
|
2026-03-11 09:50:41 +00:00
|
|
|
app_id: String,
|
feat(appd): implement IPC server with Unix socket and MessagePack framing
Replaces the skeleton bail with a functional IPC server.
ipc.rs — transport layer:
- Request enum: LaunchApp, TerminateApp, QueryRunning, QueryAppState.
Serialized with serde MessagePack (rmp-serde, SCREAMING_SNAKE_CASE
type tag).
- Response enum: LaunchAck, AppReady, RunningApps, AppState, Error.
- AppStateKind: Starting, Running, Stopping, Stopped, NotFound.
- read_frame / write_frame: async 4-byte LE length-prefixed codec over
any AsyncRead / AsyncWrite.
main.rs — server:
- SessionRegistry: in-memory HashMap<session_id, AppStateKind> with
monotonic ID counter; launch / terminate / running_ids / state.
- run(): creates socket parent directory, removes stale socket, binds
UnixListener, sends sd_notify READY=1, then accept-loops with
ctrl-c / SIGTERM shutdown. Cleans up socket on exit.
- handle_connection(): splits stream into BufReader/BufWriter, reads
request frames, calls dispatch, writes response frames.
- dispatch(): maps Request variants to SessionRegistry operations;
returns typed Response. Wasmtime spawning and compositor client
deferred to later implementation.
New deps: serde (derive), rmp-serde, tokio net/io-util/sync/rt-multi-thread.
2026-03-11 07:25:55 +00:00
|
|
|
},
|
|
|
|
|
RunningApps {
|
2026-03-11 09:42:40 +00:00
|
|
|
sessions: Vec<SessionInfo>,
|
feat(appd): implement IPC server with Unix socket and MessagePack framing
Replaces the skeleton bail with a functional IPC server.
ipc.rs — transport layer:
- Request enum: LaunchApp, TerminateApp, QueryRunning, QueryAppState.
Serialized with serde MessagePack (rmp-serde, SCREAMING_SNAKE_CASE
type tag).
- Response enum: LaunchAck, AppReady, RunningApps, AppState, Error.
- AppStateKind: Starting, Running, Stopping, Stopped, NotFound.
- read_frame / write_frame: async 4-byte LE length-prefixed codec over
any AsyncRead / AsyncWrite.
main.rs — server:
- SessionRegistry: in-memory HashMap<session_id, AppStateKind> with
monotonic ID counter; launch / terminate / running_ids / state.
- run(): creates socket parent directory, removes stale socket, binds
UnixListener, sends sd_notify READY=1, then accept-loops with
ctrl-c / SIGTERM shutdown. Cleans up socket on exit.
- handle_connection(): splits stream into BufReader/BufWriter, reads
request frames, calls dispatch, writes response frames.
- dispatch(): maps Request variants to SessionRegistry operations;
returns typed Response. Wasmtime spawning and compositor client
deferred to later implementation.
New deps: serde (derive), rmp-serde, tokio net/io-util/sync/rt-multi-thread.
2026-03-11 07:25:55 +00:00
|
|
|
},
|
|
|
|
|
AppState {
|
|
|
|
|
session_id: u64,
|
|
|
|
|
state: AppStateKind,
|
|
|
|
|
},
|
2026-03-11 10:23:46 +00:00
|
|
|
InstalledApps {
|
|
|
|
|
apps: Vec<AppInfo>,
|
|
|
|
|
},
|
feat(appd): implement IPC server with Unix socket and MessagePack framing
Replaces the skeleton bail with a functional IPC server.
ipc.rs — transport layer:
- Request enum: LaunchApp, TerminateApp, QueryRunning, QueryAppState.
Serialized with serde MessagePack (rmp-serde, SCREAMING_SNAKE_CASE
type tag).
- Response enum: LaunchAck, AppReady, RunningApps, AppState, Error.
- AppStateKind: Starting, Running, Stopping, Stopped, NotFound.
- read_frame / write_frame: async 4-byte LE length-prefixed codec over
any AsyncRead / AsyncWrite.
main.rs — server:
- SessionRegistry: in-memory HashMap<session_id, AppStateKind> with
monotonic ID counter; launch / terminate / running_ids / state.
- run(): creates socket parent directory, removes stale socket, binds
UnixListener, sends sd_notify READY=1, then accept-loops with
ctrl-c / SIGTERM shutdown. Cleans up socket on exit.
- handle_connection(): splits stream into BufReader/BufWriter, reads
request frames, calls dispatch, writes response frames.
- dispatch(): maps Request variants to SessionRegistry operations;
returns typed Response. Wasmtime spawning and compositor client
deferred to later implementation.
New deps: serde (derive), rmp-serde, tokio net/io-util/sync/rt-multi-thread.
2026-03-11 07:25:55 +00:00
|
|
|
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(())
|
|
|
|
|
}
|
2026-03-11 07:32:02 +00:00
|
|
|
|
|
|
|
|
#[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() {
|
2026-03-11 09:46:28 +00:00
|
|
|
let resp = Response::LaunchAck {
|
|
|
|
|
session_id: 7,
|
|
|
|
|
app_id: "com.example.app".into(),
|
|
|
|
|
};
|
2026-03-11 07:32:02 +00:00
|
|
|
let bytes = rmp_serde::to_vec(&resp).unwrap();
|
|
|
|
|
let decoded: Response = rmp_serde::from_slice(&bytes).unwrap();
|
|
|
|
|
match decoded {
|
2026-03-11 09:46:28 +00:00
|
|
|
Response::LaunchAck { session_id, app_id } => {
|
|
|
|
|
assert_eq!(session_id, 7);
|
|
|
|
|
assert_eq!(app_id, "com.example.app");
|
|
|
|
|
}
|
2026-03-11 07:32:02 +00:00
|
|
|
_ => panic!("wrong variant"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 09:42:40 +00:00
|
|
|
#[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");
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 10:51:04 +00:00
|
|
|
#[test]
|
|
|
|
|
fn app_info_roundtrip() {
|
|
|
|
|
let info = super::AppInfo {
|
|
|
|
|
app_id: "com.example.app".into(),
|
|
|
|
|
name: "Example App".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");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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(),
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
_ => panic!("wrong variant"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 11:12:20 +00:00
|
|
|
#[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
|
|
|
|
|
}
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 07:32:02 +00:00
|
|
|
#[tokio::test]
|
|
|
|
|
async fn frame_write_read_roundtrip() {
|
2026-03-11 09:42:40 +00:00
|
|
|
let resp = Response::RunningApps { sessions: vec![] };
|
2026-03-11 07:32:02 +00:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
}
|