Add doors IPC transport for illumos, simplify client binaries

Add platform-aware IPC transport layer for the launcher protocol:

- transport module: send_request_sync() auto-selects doors (illumos)
  or Unix sockets (Linux) at compile time
- doors_transport: uses doors::Client for high-speed synchronous RPC,
  gated behind cfg(target_os = "illumos") + "doors" feature flag
- unix_transport: blocking Unix socket client for all platforms

Simplify client binaries to use the sync transport:
- wradm: drops tokio dependency entirely, now fully synchronous
- wrlogin: drops tokio dependency, uses sync transport
- wrsessd: uses shared default_ipc_path() for consistency

The doors crate (0.8.1) is an optional illumos-only dependency.
On Linux, all code compiles and tests pass with Unix socket transport.
This commit is contained in:
Till Wegmueller 2026-04-09 22:21:14 +02:00
parent f6b9ea56ba
commit 70859175c0
9 changed files with 208 additions and 145 deletions

83
Cargo.lock generated
View file

@ -330,7 +330,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@ -621,6 +621,26 @@ dependencies = [
"litrs",
]
[[package]]
name = "door-macros"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78929f3574c3a17e7a7a844bcb1cbcbf165d3e366432f81cef4e8cb6e8c2a715"
dependencies = [
"quote",
"syn 1.0.109",
]
[[package]]
name = "doors"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "571031accecc9496ab74c0ce6d9d99ef779a7344dd27f504e1574edd344cd479"
dependencies = [
"door-macros",
"libc",
]
[[package]]
name = "downcast-rs"
version = "1.2.1"
@ -738,7 +758,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@ -1033,7 +1053,7 @@ checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@ -1079,7 +1099,7 @@ dependencies = [
"quote",
"rustc_version",
"simd_cesu8",
"syn",
"syn 2.0.117",
]
[[package]]
@ -1107,7 +1127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
dependencies = [
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@ -1309,7 +1329,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@ -1448,7 +1468,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@ -1795,7 +1815,7 @@ checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@ -1910,7 +1930,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
"syn 2.0.117",
]
[[package]]
@ -1947,7 +1967,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
dependencies = [
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@ -2399,7 +2419,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@ -2641,7 +2661,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn",
"syn 2.0.117",
]
[[package]]
@ -2671,6 +2691,17 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.117"
@ -2750,7 +2781,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@ -2761,7 +2792,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@ -2856,7 +2887,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@ -2950,7 +2981,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@ -3140,7 +3171,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"wasm-bindgen-shared",
]
@ -3338,6 +3369,7 @@ dependencies = [
name = "wayray-protocol"
version = "0.1.0"
dependencies = [
"doors",
"postcard",
"serde",
"serde_json",
@ -3535,7 +3567,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@ -3546,7 +3578,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@ -3831,7 +3863,7 @@ dependencies = [
"heck",
"indexmap",
"prettyplease",
"syn",
"syn 2.0.117",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
@ -3847,7 +3879,7 @@ dependencies = [
"prettyplease",
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"wit-bindgen-core",
"wit-bindgen-rust",
]
@ -3904,10 +3936,6 @@ name = "wradm"
version = "0.1.0"
dependencies = [
"miette",
"serde_json",
"tokio",
"tracing",
"tracing-subscriber",
"wayray-protocol",
]
@ -3936,9 +3964,6 @@ name = "wrlogin"
version = "0.1.0"
dependencies = [
"miette",
"serde",
"serde_json",
"tokio",
"tracing",
"tracing-subscriber",
"wayray-protocol",
@ -4076,7 +4101,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]

View file

@ -4,6 +4,10 @@ edition.workspace = true
version.workspace = true
license.workspace = true
[features]
default = []
doors = ["dep:doors"]
[dependencies]
serde.workspace = true
postcard.workspace = true
@ -12,3 +16,6 @@ tracing.workspace = true
zstd.workspace = true
toml = "0.8"
serde_json = "1"
[target.'cfg(target_os = "illumos")'.dependencies]
doors = { version = "0.8", optional = true }

View file

@ -9,6 +9,7 @@ pub mod encoding;
pub mod launcher;
pub mod messages;
pub mod session_config;
pub mod transport;
/// Current protocol version. Incremented on breaking changes.
pub const PROTOCOL_VERSION: u32 = 1;

View file

@ -0,0 +1,114 @@
//! IPC transport abstraction for the launcher protocol.
//!
//! Provides platform-specific transports for request/response communication
//! between WayRay components (wrsessd, wradm, wrlogin, wrsrvd).
//!
//! - **Linux**: Unix domain sockets (async, via tokio)
//! - **illumos**: Doors IPC (sync, high-speed RPC) with Unix socket fallback
//!
//! The transport moves JSON bytes — serialization is handled by the caller
//! using [`LauncherRequest`] and [`LauncherResponse`].
use std::io;
use std::path::{Path, PathBuf};
use crate::launcher::{LauncherRequest, LauncherResponse};
/// Default IPC endpoint path.
///
/// On illumos with doors enabled, this is a door file.
/// On Linux, this is a Unix socket.
pub fn default_ipc_path() -> PathBuf {
let runtime_dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(runtime_dir).join("wayray-launcher.sock")
}
/// Send a request and receive a response (synchronous, blocking).
///
/// Automatically selects the transport based on the platform:
/// - illumos with `doors` feature: uses doors IPC
/// - everywhere else: uses Unix socket with blocking I/O
pub fn send_request_sync(path: &Path, request: &LauncherRequest) -> io::Result<LauncherResponse> {
#[cfg(all(target_os = "illumos", feature = "doors"))]
{
doors_transport::send_request(path, request)
}
#[cfg(not(all(target_os = "illumos", feature = "doors")))]
{
unix_transport::send_request(path, request)
}
}
// =============================================================================
// Unix socket transport (all platforms, used as default)
// =============================================================================
pub mod unix_transport {
use std::io::{self, BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::path::Path;
use crate::launcher::{LauncherRequest, LauncherResponse};
/// Send a request over a Unix socket and read the response.
pub fn send_request(path: &Path, request: &LauncherRequest) -> io::Result<LauncherResponse> {
let stream = UnixStream::connect(path)?;
let mut writer = io::BufWriter::new(&stream);
let mut reader = BufReader::new(&stream);
let mut json = serde_json::to_string(request)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
json.push('\n');
writer.write_all(json.as_bytes())?;
writer.flush()?;
let mut response_line = String::new();
reader.read_line(&mut response_line)?;
serde_json::from_str(response_line.trim())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
}
// =============================================================================
// Doors transport (illumos only)
// =============================================================================
#[cfg(all(target_os = "illumos", feature = "doors"))]
pub mod doors_transport {
use std::io;
use std::path::Path;
use crate::launcher::{LauncherRequest, LauncherResponse};
/// Send a request via illumos doors IPC.
///
/// Doors are synchronous RPC: call_with_data sends bytes and blocks
/// until the server procedure returns a response.
pub fn send_request(path: &Path, request: &LauncherRequest) -> io::Result<LauncherResponse> {
let json = serde_json::to_vec(request)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let client = doors::Client::open(path)
.map_err(|e| io::Error::new(io::ErrorKind::ConnectionRefused, format!("{e:?}")))?;
let response_bytes = client
.call_with_data(&json)
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{e:?}")))?;
serde_json::from_slice(&response_bytes)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_path_is_reasonable() {
let path = default_ipc_path();
assert!(path.to_str().unwrap().contains("wayray-launcher"));
}
}

View file

@ -6,8 +6,4 @@ license.workspace = true
[dependencies]
wayray-protocol.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
miette.workspace = true
serde_json = "1"
tokio = { workspace = true, features = ["rt", "net", "io-util", "macros"] }

View file

@ -1,61 +1,33 @@
//! wradm -- WayRay administration CLI.
//!
//! Provides session management commands following the illumos `zoneadm`/`svcadm`
//! pattern. Communicates with the session launcher (wrsessd) via Unix socket.
//! pattern. Communicates with the session launcher (wrsessd) via the platform
//! IPC transport (Unix sockets on Linux, doors on illumos).
//!
//! ## Commands
//!
//! - `wradm list` — List all managed sessions
//! - `wradm kill <token>` — Kill a session by token
//! - `wradm show <token>` — Show details for a session
use std::path::PathBuf;
use miette::Result;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::UnixStream;
use wayray_protocol::launcher::{LauncherRequest, LauncherResponse};
use wayray_protocol::transport;
/// Default launcher socket path.
fn default_socket_path() -> PathBuf {
let runtime_dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(runtime_dir).join("wayray-launcher.sock")
fn ipc_path() -> PathBuf {
std::env::var("WAYRAY_LAUNCHER_SOCKET")
.map(PathBuf::from)
.unwrap_or_else(|_| transport::default_ipc_path())
}
/// Send a request to the launcher and read the response.
async fn send_request(request: &LauncherRequest) -> Result<LauncherResponse> {
let socket_path = std::env::var("WAYRAY_LAUNCHER_SOCKET")
.map(PathBuf::from)
.unwrap_or_else(|_| default_socket_path());
let stream = UnixStream::connect(&socket_path).await.map_err(|e| {
fn send(request: &LauncherRequest) -> Result<LauncherResponse> {
transport::send_request_sync(&ipc_path(), request).map_err(|e| {
miette::miette!(
"failed to connect to launcher at {}: {}\n\nIs wrsessd running?",
socket_path.display(),
"launcher communication failed: {}\n\nIs wrsessd running?",
e
)
})?;
let (reader, mut writer) = stream.into_split();
let mut json = serde_json::to_string(request)
.map_err(|e| miette::miette!("serialization error: {}", e))?;
json.push('\n');
writer
.write_all(json.as_bytes())
.await
.map_err(|e| miette::miette!("failed to send request: {}", e))?;
let mut reader = BufReader::new(reader);
let mut response_line = String::new();
reader
.read_line(&mut response_line)
.await
.map_err(|e| miette::miette!("failed to read response: {}", e))?;
serde_json::from_str(response_line.trim())
.map_err(|e| miette::miette!("failed to parse response: {}", e))
})
}
fn print_usage() {
@ -66,15 +38,7 @@ fn print_usage() {
eprintln!(" kill <token> Kill a session by token");
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
)
.init();
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let command = args.get(1).map(String::as_str).unwrap_or_else(|| {
@ -84,7 +48,7 @@ async fn main() -> Result<()> {
match command {
"list" => {
let response = send_request(&LauncherRequest::ListSessions).await?;
let response = send(&LauncherRequest::ListSessions)?;
match response {
LauncherResponse::SessionList { sessions } => {
if sessions.is_empty() {
@ -110,10 +74,9 @@ async fn main() -> Result<()> {
let token = args
.get(2)
.ok_or_else(|| miette::miette!("Usage: wradm kill <token>"))?;
let response = send_request(&LauncherRequest::KillSession {
let response = send(&LauncherRequest::KillSession {
token: token.clone(),
})
.await?;
})?;
match response {
LauncherResponse::SessionKilled { token } => {
println!("Session {token} killed.");

View file

@ -10,6 +10,3 @@ wayray-protocol.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
miette.workspace = true
serde.workspace = true
serde_json = "1"
tokio = { workspace = true, features = ["rt", "net", "io-util", "macros"] }

View file

@ -13,17 +13,16 @@
//! The session launcher (wrsessd) starts wrlogin as the first client
//! in a new session. wrlogin:
//! 1. Prompts for username and password on the terminal
//! 2. Sends `session_authenticated` to wrsessd via the launcher socket
//! 2. Sends `session_authenticated` to wrsessd via the launcher IPC
//! 3. Exits on success, allowing the desktop to start
use std::io::{self, BufRead, Write};
use std::path::PathBuf;
use miette::Result;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use tokio::net::UnixStream;
use tracing::info;
use wayray_protocol::launcher::{LauncherRequest, LauncherResponse};
use wayray_protocol::transport;
/// Read a line from stdin with a prompt.
fn prompt(message: &str) -> io::Result<String> {
@ -36,53 +35,13 @@ fn prompt(message: &str) -> io::Result<String> {
Ok(line.trim().to_string())
}
/// Send an authentication request to the session launcher.
async fn authenticate(socket_path: &PathBuf, token: &str, user: &str) -> Result<LauncherResponse> {
let stream = UnixStream::connect(socket_path).await.map_err(|e| {
miette::miette!(
"failed to connect to launcher at {}: {}",
socket_path.display(),
e
)
})?;
let (reader, mut writer) = stream.into_split();
let request = LauncherRequest::SessionAuthenticated {
token: token.to_string(),
user: user.to_string(),
};
let mut json = serde_json::to_string(&request)
.map_err(|e| miette::miette!("failed to serialize request: {}", e))?;
json.push('\n');
writer
.write_all(json.as_bytes())
.await
.map_err(|e| miette::miette!("failed to send to launcher: {}", e))?;
let mut reader = tokio::io::BufReader::new(reader);
let mut response_line = String::new();
reader
.read_line(&mut response_line)
.await
.map_err(|e| miette::miette!("failed to read launcher response: {}", e))?;
let response: LauncherResponse = serde_json::from_str(response_line.trim())
.map_err(|e| miette::miette!("failed to parse launcher response: {}", e))?;
Ok(response)
fn ipc_path() -> PathBuf {
std::env::var("WAYRAY_LAUNCHER_SOCKET")
.map(PathBuf::from)
.unwrap_or_else(|_| transport::default_ipc_path())
}
/// Default launcher socket path.
fn default_socket_path() -> PathBuf {
let runtime_dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(runtime_dir).join("wayray-launcher.sock")
}
#[tokio::main]
async fn main() -> Result<()> {
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
@ -90,10 +49,6 @@ async fn main() -> Result<()> {
)
.init();
let socket_path = std::env::var("WAYRAY_LAUNCHER_SOCKET")
.map(PathBuf::from)
.unwrap_or_else(|_| default_socket_path());
// The session token is passed via environment variable by the launcher.
let token = std::env::var("WAYRAY_SESSION_TOKEN").unwrap_or_else(|_| {
eprintln!("warning: WAYRAY_SESSION_TOKEN not set, using 'unknown'");
@ -105,6 +60,8 @@ async fn main() -> Result<()> {
println!(" ============");
println!();
let path = ipc_path();
// Simple login loop.
loop {
let user = prompt(" Username: ").map_err(|e| miette::miette!("input error: {}", e))?;
@ -121,11 +78,15 @@ async fn main() -> Result<()> {
info!(%user, "authenticating with session launcher");
match authenticate(&socket_path, &token, &user).await {
let request = LauncherRequest::SessionAuthenticated {
token: token.clone(),
user: user.clone(),
};
match transport::send_request_sync(&path, &request) {
Ok(LauncherResponse::DesktopStarted { user, .. }) => {
println!(" Welcome, {user}! Starting desktop...");
println!();
// Exit — the launcher starts the desktop components.
return Ok(());
}
Ok(LauncherResponse::Error { message, .. }) => {

View file

@ -169,10 +169,9 @@ impl Launcher {
}
}
/// Default socket path for the launcher.
/// Default IPC path for the launcher (uses shared transport default).
fn default_socket_path() -> PathBuf {
let runtime_dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(runtime_dir).join("wayray-launcher.sock")
wayray_protocol::transport::default_ipc_path()
}
#[tokio::main]