mirror of
https://github.com/CloudNebulaProject/wayray.git
synced 2026-04-10 21:20:40 +00:00
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:
parent
d59411ca60
commit
f6b9ea56ba
12 changed files with 1007 additions and 6 deletions
111
Cargo.lock
generated
111
Cargo.lock
generated
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
114
crates/wayray-protocol/src/launcher.rs
Normal file
114
crates/wayray-protocol/src/launcher.rs
Normal 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 { .. }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
196
crates/wayray-protocol/src/session_config.rs
Normal file
196
crates/wayray-protocol/src/session_config.rs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"] }
|
||||||
|
|
|
||||||
|
|
@ -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
15
crates/wrlogin/Cargo.toml
Normal 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
147
crates/wrlogin/src/main.rs
Normal 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
15
crates/wrsessd/Cargo.toml
Normal 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
266
crates/wrsessd/src/main.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue