Add session launcher, greeter, admin CLI, and session config

Session infrastructure for the launcher/greeter architecture (Phase 3):

- Launcher protocol: JSON-over-Unix-socket messages (LauncherRequest/
  LauncherResponse) for session_requested, session_authenticated,
  session_logout, plus admin queries (list_sessions, kill_session)

- Session config: ~/.config/wayray/session.toml with wm, panel,
  launcher, notifications, and autostart fields. Serde TOML parsing
  with sensible defaults (wr-wm-floating). 5 unit tests.

- wrsessd: Session launcher daemon listening on Unix socket. Manages
  per-token sessions, launches greeter on session_requested, starts
  desktop components from session.toml on session_authenticated,
  cleans up child processes on logout. Admin query support.

- wrlogin: Reference CLI greeter. Reads credentials from stdin,
  sends session_authenticated to wrsessd, exits on success.
  Token passed via WAYRAY_SESSION_TOKEN env var.

- wradm: Session management commands (list, kill) communicating
  with wrsessd via launcher protocol. Tabular output format.
This commit is contained in:
Till Wegmueller 2026-04-09 21:22:46 +02:00
parent d59411ca60
commit f6b9ea56ba
12 changed files with 1007 additions and 6 deletions

111
Cargo.lock generated
View file

@ -1919,7 +1919,7 @@ version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [ dependencies = [
"toml_edit", "toml_edit 0.25.10+spec-1.1.0",
] ]
[[package]] [[package]]
@ -2415,6 +2415,15 @@ dependencies = [
"zmij", "zmij",
] ]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.9" version = "0.10.9"
@ -2441,6 +2450,16 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]] [[package]]
name = "simd_cesu8" name = "simd_cesu8"
version = "1.1.1" version = "1.1.1"
@ -2819,9 +2838,11 @@ version = "1.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd" checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd"
dependencies = [ dependencies = [
"bytes",
"libc", "libc",
"mio", "mio",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@ -2838,6 +2859,27 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime 0.6.11",
"toml_edit 0.22.27",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "1.1.1+spec-1.1.0" version = "1.1.1+spec-1.1.0"
@ -2847,6 +2889,20 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime 0.6.11",
"toml_write",
"winnow 0.7.15",
]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.25.10+spec-1.1.0" version = "0.25.10+spec-1.1.0"
@ -2854,9 +2910,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82418ca169e235e6c399a84e395ab6debeb3bc90edc959bf0f48647c6a32d1b" checksum = "a82418ca169e235e6c399a84e395ab6debeb3bc90edc959bf0f48647c6a32d1b"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"toml_datetime", "toml_datetime 1.1.1+spec-1.1.0",
"toml_parser", "toml_parser",
"winnow", "winnow 1.0.1",
] ]
[[package]] [[package]]
@ -2865,9 +2921,15 @@ version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [ dependencies = [
"winnow", "winnow 1.0.1",
] ]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.44" version = "0.1.44"
@ -3278,7 +3340,10 @@ version = "0.1.0"
dependencies = [ dependencies = [
"postcard", "postcard",
"serde", "serde",
"serde_json",
"thiserror 2.0.18", "thiserror 2.0.18",
"toml",
"tracing",
"zstd", "zstd",
] ]
@ -3718,6 +3783,15 @@ dependencies = [
"xkbcommon-dl", "xkbcommon-dl",
] ]
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "1.0.1" version = "1.0.1"
@ -3830,7 +3904,10 @@ name = "wradm"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"miette", "miette",
"serde_json",
"tokio",
"tracing", "tracing",
"tracing-subscriber",
"wayray-protocol", "wayray-protocol",
] ]
@ -3854,6 +3931,32 @@ dependencies = [
"winit", "winit",
] ]
[[package]]
name = "wrlogin"
version = "0.1.0"
dependencies = [
"miette",
"serde",
"serde_json",
"tokio",
"tracing",
"tracing-subscriber",
"wayray-protocol",
]
[[package]]
name = "wrsessd"
version = "0.1.0"
dependencies = [
"miette",
"serde",
"serde_json",
"tokio",
"tracing",
"tracing-subscriber",
"wayray-protocol",
]
[[package]] [[package]]
name = "wrsrvd" name = "wrsrvd"
version = "0.1.0" version = "0.1.0"

View file

@ -7,6 +7,8 @@ members = [
"crates/wrclient", "crates/wrclient",
"crates/wradm", "crates/wradm",
"crates/wr-wm-tiling", "crates/wr-wm-tiling",
"crates/wrsessd",
"crates/wrlogin",
] ]
[workspace.package] [workspace.package]

View file

@ -8,4 +8,7 @@ license.workspace = true
serde.workspace = true serde.workspace = true
postcard.workspace = true postcard.workspace = true
thiserror.workspace = true thiserror.workspace = true
tracing.workspace = true
zstd.workspace = true zstd.workspace = true
toml = "0.8"
serde_json = "1"

View file

@ -0,0 +1,114 @@
//! Session launcher protocol.
//!
//! Defines the JSON-over-Unix-socket messages exchanged between the
//! WayRay compositor (wrsrvd) and the session launcher daemon (wrsessd).
//!
//! The protocol is intentionally simple: a few event types with JSON
//! serialization. Each message is a single JSON line (newline-delimited).
use serde::{Deserialize, Serialize};
/// Events sent from the compositor to the session launcher.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum LauncherRequest {
/// A new session is needed for this token.
/// The launcher should create the user environment and start the greeter.
#[serde(rename = "session_requested")]
SessionRequested {
token: String,
/// Path to the Wayland display socket (WAYLAND_DISPLAY).
wayland_display: String,
},
/// The user has authenticated successfully.
/// The launcher should start the user's configured desktop.
#[serde(rename = "session_authenticated")]
SessionAuthenticated { token: String, user: String },
/// The session is being logged out / destroyed.
/// The launcher should clean up the user environment.
#[serde(rename = "session_logout")]
SessionLogout { token: String, session_id: u64 },
/// Admin: list all managed sessions.
#[serde(rename = "list_sessions")]
ListSessions,
/// Admin: kill a session by token.
#[serde(rename = "kill_session")]
KillSession { token: String },
}
/// Responses sent from the session launcher back to the compositor.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum LauncherResponse {
/// Session environment is ready; greeter has been launched.
#[serde(rename = "session_ready")]
SessionReady { token: String },
/// User desktop has been started.
#[serde(rename = "desktop_started")]
DesktopStarted { token: String, user: String },
/// An error occurred during session setup.
#[serde(rename = "error")]
Error { token: String, message: String },
/// Admin: list of managed sessions.
#[serde(rename = "session_list")]
SessionList { sessions: Vec<SessionInfo> },
/// Admin: session was killed.
#[serde(rename = "session_killed")]
SessionKilled { token: String },
}
/// Session info returned by admin queries.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInfo {
pub token: String,
pub user: Option<String>,
pub child_count: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serialize_session_requested() {
let msg = LauncherRequest::SessionRequested {
token: "abc-123".to_string(),
wayland_display: "wayland-1".to_string(),
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"type\":\"session_requested\""));
assert!(json.contains("\"token\":\"abc-123\""));
}
#[test]
fn roundtrip_launcher_messages() {
let req = LauncherRequest::SessionAuthenticated {
token: "tok".to_string(),
user: "alice".to_string(),
};
let json = serde_json::to_string(&req).unwrap();
let parsed: LauncherRequest = serde_json::from_str(&json).unwrap();
assert!(matches!(
parsed,
LauncherRequest::SessionAuthenticated { ref user, .. } if user == "alice"
));
}
#[test]
fn roundtrip_launcher_response() {
let resp = LauncherResponse::SessionReady {
token: "t".to_string(),
};
let json = serde_json::to_string(&resp).unwrap();
let parsed: LauncherResponse = serde_json::from_str(&json).unwrap();
assert!(matches!(parsed, LauncherResponse::SessionReady { .. }));
}
}

View file

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

View file

@ -0,0 +1,196 @@
//! Session desktop configuration.
//!
//! Defines the `~/.config/wayray/session.toml` format that specifies
//! which window manager, panel, launcher, and autostart apps to run.
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
/// Desktop session configuration loaded from `session.toml`.
///
/// Specifies the composable desktop environment: WM, panel, launcher,
/// notification daemon, and autostart applications.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SessionConfig {
/// Window manager binary (connects via wayray_wm_manager_v1 protocol).
/// Default: "wr-wm-floating" (built-in floating WM).
pub wm: String,
/// Panel binary (layer-shell Wayland client, e.g., waybar).
pub panel: Option<String>,
/// Application launcher binary (layer-shell, e.g., fuzzel).
pub launcher: Option<String>,
/// Notification daemon binary (e.g., mako).
pub notifications: Option<String>,
/// Applications to start automatically after session setup.
#[serde(default)]
pub autostart: Vec<String>,
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
wm: "wr-wm-floating".to_string(),
panel: None,
launcher: None,
notifications: None,
autostart: Vec::new(),
}
}
}
impl SessionConfig {
/// Load session config from a TOML file.
pub fn load(path: &Path) -> Result<Self, SessionConfigError> {
let content = std::fs::read_to_string(path).map_err(|e| SessionConfigError::Io {
path: path.to_path_buf(),
source: e,
})?;
toml::from_str(&content).map_err(|e| SessionConfigError::Parse {
path: path.to_path_buf(),
source: e,
})
}
/// Load from the default location (`~/.config/wayray/session.toml`),
/// falling back to defaults if the file doesn't exist.
pub fn load_default() -> Self {
let config_path = default_config_path();
match Self::load(&config_path) {
Ok(config) => config,
Err(SessionConfigError::Io { .. }) => {
// File doesn't exist — use defaults.
Self::default()
}
Err(e) => {
tracing::warn!(error = %e, "failed to parse session config, using defaults");
Self::default()
}
}
}
/// Get all binaries that should be launched for this session.
/// Returns (binary_name, is_wm) pairs. WM is listed first.
pub fn launch_list(&self) -> Vec<(&str, bool)> {
let mut list = vec![(self.wm.as_str(), true)];
if let Some(panel) = &self.panel {
list.push((panel.as_str(), false));
}
if let Some(launcher) = &self.launcher {
list.push((launcher.as_str(), false));
}
if let Some(notifications) = &self.notifications {
list.push((notifications.as_str(), false));
}
for app in &self.autostart {
list.push((app.as_str(), false));
}
list
}
}
/// Default config file path: `$XDG_CONFIG_HOME/wayray/session.toml`.
pub fn default_config_path() -> PathBuf {
let base = std::env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(home).join(".config")
});
base.join("wayray").join("session.toml")
}
/// Errors from loading session config.
#[derive(Debug)]
pub enum SessionConfigError {
Io {
path: PathBuf,
source: std::io::Error,
},
Parse {
path: PathBuf,
source: toml::de::Error,
},
}
impl std::fmt::Display for SessionConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SessionConfigError::Io { path, source } => {
write!(f, "failed to read {}: {}", path.display(), source)
}
SessionConfigError::Parse { path, source } => {
write!(f, "failed to parse {}: {}", path.display(), source)
}
}
}
}
impl std::error::Error for SessionConfigError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config() {
let config = SessionConfig::default();
assert_eq!(config.wm, "wr-wm-floating");
assert!(config.panel.is_none());
assert!(config.autostart.is_empty());
}
#[test]
fn parse_full_config() {
let toml = r#"
wm = "wr-wm-tiling"
panel = "waybar"
launcher = "fuzzel"
notifications = "mako"
autostart = ["foot", "nm-applet"]
"#;
let config: SessionConfig = toml::from_str(toml).unwrap();
assert_eq!(config.wm, "wr-wm-tiling");
assert_eq!(config.panel.as_deref(), Some("waybar"));
assert_eq!(config.launcher.as_deref(), Some("fuzzel"));
assert_eq!(config.notifications.as_deref(), Some("mako"));
assert_eq!(config.autostart, vec!["foot", "nm-applet"]);
}
#[test]
fn parse_minimal_config() {
let toml = r#"wm = "my-custom-wm""#;
let config: SessionConfig = toml::from_str(toml).unwrap();
assert_eq!(config.wm, "my-custom-wm");
assert!(config.panel.is_none());
assert!(config.autostart.is_empty());
}
#[test]
fn parse_empty_uses_defaults() {
let config: SessionConfig = toml::from_str("").unwrap();
assert_eq!(config.wm, "wr-wm-floating");
}
#[test]
fn launch_list_order() {
let config = SessionConfig {
wm: "my-wm".to_string(),
panel: Some("waybar".to_string()),
launcher: None,
notifications: Some("mako".to_string()),
autostart: vec!["foot".to_string()],
};
let list = config.launch_list();
assert_eq!(list[0], ("my-wm", true));
assert_eq!(list[1], ("waybar", false));
assert_eq!(list[2], ("mako", false));
assert_eq!(list[3], ("foot", false));
}
}

View file

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

View file

@ -1,3 +1,138 @@
fn main() { //! wradm -- WayRay administration CLI.
println!("wradm administration tool"); //!
//! Provides session management commands following the illumos `zoneadm`/`svcadm`
//! pattern. Communicates with the session launcher (wrsessd) via Unix socket.
//!
//! ## 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};
/// 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")
}
/// 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| {
miette::miette!(
"failed to connect to launcher at {}: {}\n\nIs wrsessd running?",
socket_path.display(),
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() {
eprintln!("Usage: wradm <command> [args]");
eprintln!();
eprintln!("Commands:");
eprintln!(" list List all managed sessions");
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();
let args: Vec<String> = std::env::args().collect();
let command = args.get(1).map(String::as_str).unwrap_or_else(|| {
print_usage();
std::process::exit(1);
});
match command {
"list" => {
let response = send_request(&LauncherRequest::ListSessions).await?;
match response {
LauncherResponse::SessionList { sessions } => {
if sessions.is_empty() {
println!("No active sessions.");
} else {
println!("{:<36} {:<16} PROCESSES", "TOKEN", "USER");
for s in sessions {
println!(
"{:<36} {:<16} {}",
s.token,
s.user.as_deref().unwrap_or("-"),
s.child_count,
);
}
}
}
other => {
eprintln!("Unexpected response: {other:?}");
}
}
}
"kill" => {
let token = args
.get(2)
.ok_or_else(|| miette::miette!("Usage: wradm kill <token>"))?;
let response = send_request(&LauncherRequest::KillSession {
token: token.clone(),
})
.await?;
match response {
LauncherResponse::SessionKilled { token } => {
println!("Session {token} killed.");
}
LauncherResponse::Error { message, .. } => {
eprintln!("Error: {message}");
std::process::exit(1);
}
other => {
eprintln!("Unexpected response: {other:?}");
}
}
}
_ => {
eprintln!("Unknown command: {command}");
print_usage();
std::process::exit(1);
}
}
Ok(())
} }

15
crates/wrlogin/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "wrlogin"
edition.workspace = true
version.workspace = true
license.workspace = true
description = "WayRay reference greeter / login screen"
[dependencies]
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"] }

147
crates/wrlogin/src/main.rs Normal file
View file

@ -0,0 +1,147 @@
//! wrlogin -- WayRay reference greeter.
//!
//! A minimal CLI-based login screen that reads credentials from stdin
//! and communicates with the session launcher (wrsessd) to authenticate
//! the user and start their desktop session.
//!
//! This is a reference implementation using terminal I/O. Production
//! greeters would use a graphical Wayland client (GTK, iced, etc.)
//! with the same launcher protocol.
//!
//! ## Usage
//!
//! 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
//! 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};
/// Read a line from stdin with a prompt.
fn prompt(message: &str) -> io::Result<String> {
let mut stdout = io::stdout().lock();
stdout.write_all(message.as_bytes())?;
stdout.flush()?;
let mut line = String::new();
io::stdin().lock().read_line(&mut line)?;
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)
}
/// 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<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.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'");
"unknown".to_string()
});
println!();
println!(" WayRay Login");
println!(" ============");
println!();
// Simple login loop.
loop {
let user = prompt(" Username: ").map_err(|e| miette::miette!("input error: {}", e))?;
if user.is_empty() {
continue;
}
let _password =
prompt(" Password: ").map_err(|e| miette::miette!("input error: {}", e))?;
// NOTE: This reference greeter does NOT verify the password.
// A production greeter would authenticate via PAM here.
// For the reference implementation, any non-empty username succeeds.
info!(%user, "authenticating with session launcher");
match authenticate(&socket_path, &token, &user).await {
Ok(LauncherResponse::DesktopStarted { user, .. }) => {
println!(" Welcome, {user}! Starting desktop...");
println!();
// Exit — the launcher starts the desktop components.
return Ok(());
}
Ok(LauncherResponse::Error { message, .. }) => {
eprintln!(" Login failed: {message}");
eprintln!();
}
Ok(other) => {
info!(?other, "unexpected response from launcher");
eprintln!(" Unexpected response. Try again.");
eprintln!();
}
Err(e) => {
eprintln!(" Connection error: {e}");
eprintln!(" (Is the session launcher running?)");
eprintln!();
}
}
}
}

15
crates/wrsessd/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "wrsessd"
edition.workspace = true
version.workspace = true
license.workspace = true
description = "WayRay session launcher daemon"
[dependencies]
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", "process"] }

266
crates/wrsessd/src/main.rs Normal file
View file

@ -0,0 +1,266 @@
//! wrsessd -- WayRay session launcher daemon.
//!
//! Reference implementation of the session launcher interface. Listens on
//! a Unix socket for events from the WayRay compositor:
//!
//! - `session_requested`: Start a greeter for the new session
//! - `session_authenticated`: Launch the user's desktop from session.toml
//! - `session_logout`: Clean up child processes
//!
//! This is a reference implementation. Production deployments may replace
//! it with a custom launcher that integrates PAM, LDAP, NFS mounts, etc.
use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Stdio;
use miette::Result;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::UnixListener;
use tokio::process::{Child, Command};
use tracing::{error, info, warn};
use wayray_protocol::launcher::{LauncherRequest, LauncherResponse, SessionInfo};
use wayray_protocol::session_config::SessionConfig;
/// Tracked state for an active session.
struct ManagedSession {
token: String,
user: Option<String>,
/// Child processes launched for this session.
children: Vec<Child>,
/// The Wayland display socket name.
wayland_display: String,
}
/// Session launcher state.
struct Launcher {
sessions: HashMap<String, ManagedSession>,
/// Path to the greeter binary.
greeter_bin: String,
}
impl Launcher {
fn new() -> Self {
Self {
sessions: HashMap::new(),
greeter_bin: "wrlogin".to_string(),
}
}
/// Handle a session_requested event: launch the greeter.
async fn handle_session_requested(
&mut self,
token: String,
wayland_display: String,
) -> LauncherResponse {
info!(%token, %wayland_display, "session requested, launching greeter");
let mut session = ManagedSession {
token: token.clone(),
user: None,
children: Vec::new(),
wayland_display: wayland_display.clone(),
};
// Launch the greeter as a Wayland client.
match Command::new(&self.greeter_bin)
.env("WAYLAND_DISPLAY", &wayland_display)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
{
Ok(child) => {
session.children.push(child);
self.sessions.insert(token.clone(), session);
LauncherResponse::SessionReady { token }
}
Err(e) => {
warn!(error = %e, greeter = %self.greeter_bin, "failed to launch greeter");
// Still register the session even if greeter fails.
self.sessions.insert(token.clone(), session);
LauncherResponse::Error {
token,
message: format!("greeter launch failed: {e}"),
}
}
}
}
/// Handle session_authenticated: launch the user's desktop.
async fn handle_session_authenticated(
&mut self,
token: String,
user: String,
) -> LauncherResponse {
let Some(session) = self.sessions.get_mut(&token) else {
return LauncherResponse::Error {
token,
message: "no session found for token".to_string(),
};
};
session.user = Some(user.clone());
info!(%token, %user, "session authenticated, launching desktop");
// Load the user's session config.
let config = SessionConfig::load_default();
// Launch each component from the session config.
for (binary, is_wm) in config.launch_list() {
match Command::new(binary)
.env("WAYLAND_DISPLAY", &session.wayland_display)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
{
Ok(child) => {
info!(binary, is_wm, "launched session component");
session.children.push(child);
}
Err(e) => {
warn!(binary, error = %e, "failed to launch session component");
}
}
}
LauncherResponse::DesktopStarted { token, user }
}
/// Handle session_logout: kill all child processes.
async fn handle_session_logout(&mut self, token: String, _session_id: u64) {
if let Some(mut session) = self.sessions.remove(&token) {
info!(%token, "session logout, cleaning up");
for child in &mut session.children {
let _ = child.kill().await;
}
}
}
/// Admin: list all managed sessions.
fn list_sessions(&self) -> LauncherResponse {
let sessions = self
.sessions
.values()
.map(|s| SessionInfo {
token: s.token.clone(),
user: s.user.clone(),
child_count: s.children.len(),
})
.collect();
LauncherResponse::SessionList { sessions }
}
/// Admin: kill a session by token.
async fn kill_session(&mut self, token: String) -> LauncherResponse {
if let Some(mut session) = self.sessions.remove(&token) {
info!(%token, "admin: killing session");
for child in &mut session.children {
let _ = child.kill().await;
}
LauncherResponse::SessionKilled { token }
} else {
LauncherResponse::Error {
token,
message: "session not found".to_string(),
}
}
}
}
/// Default socket path for the launcher.
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<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
let socket_path = std::env::args()
.nth(1)
.map(PathBuf::from)
.unwrap_or_else(default_socket_path);
// Remove stale socket file if it exists.
let _ = std::fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path).map_err(|e| {
miette::miette!(
"failed to bind launcher socket at {}: {}",
socket_path.display(),
e
)
})?;
info!(socket = %socket_path.display(), "wrsessd listening");
let mut launcher = Launcher::new();
loop {
let (stream, _) = listener
.accept()
.await
.map_err(|e| miette::miette!("failed to accept connection: {}", e))?;
let (reader, mut writer) = stream.into_split();
let mut reader = BufReader::new(reader);
let mut line = String::new();
// Read JSON lines from the client (wrsrvd).
while reader.read_line(&mut line).await.unwrap_or(0) > 0 {
let trimmed = line.trim();
if trimmed.is_empty() {
line.clear();
continue;
}
match serde_json::from_str::<LauncherRequest>(trimmed) {
Ok(request) => {
let response = match request {
LauncherRequest::SessionRequested {
token,
wayland_display,
} => {
launcher
.handle_session_requested(token, wayland_display)
.await
}
LauncherRequest::SessionAuthenticated { token, user } => {
launcher.handle_session_authenticated(token, user).await
}
LauncherRequest::SessionLogout { token, session_id } => {
launcher.handle_session_logout(token, session_id).await;
// No response for logout.
line.clear();
continue;
}
LauncherRequest::ListSessions => launcher.list_sessions(),
LauncherRequest::KillSession { token } => {
launcher.kill_session(token).await
}
};
let mut resp_json = serde_json::to_string(&response).unwrap();
resp_json.push('\n');
if let Err(e) = writer.write_all(resp_json.as_bytes()).await {
warn!(error = %e, "failed to send response");
break;
}
}
Err(e) => {
error!(error = %e, line = trimmed, "failed to parse launcher request");
}
}
line.clear();
}
}
}