Sprint 2: X11 Server Management
Goal: Start and manage Xorg server, launch greeter process, and handle session startup.
Objectives
- Start Xorg on appropriate VT with correct permissions
- Launch greeter as unprivileged user
- Start user session after successful auth
- Handle X server lifecycle (crash recovery, clean shutdown)
- Integrate with systemd-logind for session registration
Background: X11 Display Manager Flow
1. gardmd starts
2. gardmd spawns Xorg on VT (e.g., :0 on vt1)
3. gardmd waits for X to be ready (connect test)
4. gardmd spawns greeter as unprivileged user
5. Greeter authenticates user via IPC
6. gardmd kills greeter, starts user session
7. User session runs until logout
8. gardmd restarts greeter (loop back to 4)
Tasks
2.1 X Server Launcher
// gardmd/src/x11.rs
use nix::unistd::{fork, ForkResult, setsid, Pid};
use std::process::{Command, Child};
use std::time::Duration;
pub struct XServer {
process: Child,
display: String,
vt: u32,
}
impl XServer {
/// Start Xorg server on specified display and VT
pub fn start(display: &str, vt: u32) -> anyhow::Result<Self> {
// Xorg arguments
let mut cmd = Command::new("/usr/bin/Xorg");
cmd.arg(display)
.arg(&format!("vt{}", vt))
.arg("-keeptty")
.arg("-noreset")
.arg("-novtswitch")
.arg("-nolisten").arg("tcp");
// For multi-seat support (future):
// cmd.arg("-seat").arg("seat0");
// Capture output for debugging
cmd.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let process = cmd.spawn()?;
tracing::info!("Started Xorg on {} (vt{}, pid={})",
display, vt, process.id());
let server = Self {
process,
display: display.to_string(),
vt,
};
// Wait for X to be ready
server.wait_ready(Duration::from_secs(10))?;
Ok(server)
}
/// Wait for X server to be ready by attempting connection
fn wait_ready(&self, timeout: Duration) -> anyhow::Result<()> {
use std::time::Instant;
let start = Instant::now();
while start.elapsed() < timeout {
// Try to connect to X server
match x11rb::connect(Some(&self.display)) {
Ok(_) => {
tracing::debug!("X server ready on {}", self.display);
return Ok(());
}
Err(_) => {
std::thread::sleep(Duration::from_millis(100));
}
}
}
anyhow::bail!("X server failed to become ready within {:?}", timeout);
}
/// Get the display string (e.g., ":0")
pub fn display(&self) -> &str {
&self.display
}
/// Get the VT number
pub fn vt(&self) -> u32 {
self.vt
}
/// Check if X server is still running
pub fn is_running(&mut self) -> bool {
match self.process.try_wait() {
Ok(None) => true,
_ => false,
}
}
/// Stop the X server
pub fn stop(&mut self) -> anyhow::Result<()> {
tracing::info!("Stopping X server");
self.process.kill()?;
self.process.wait()?;
Ok(())
}
}
impl Drop for XServer {
fn drop(&mut self) {
let _ = self.stop();
}
}
2.2 VT Allocation
// gardmd/src/vt.rs
use std::fs::OpenOptions;
use std::os::unix::io::AsRawFd;
use nix::ioctl_read_bad;
// ioctl for getting/setting VT
const VT_GETSTATE: u64 = 0x5603;
const VT_ACTIVATE: u64 = 0x5606;
const VT_WAITACTIVE: u64 = 0x5607;
const VT_OPENQRY: u64 = 0x5600;
#[repr(C)]
pub struct VtStat {
pub v_active: u16,
pub v_signal: u16,
pub v_state: u16,
}
/// Find an unused VT
pub fn find_unused_vt() -> anyhow::Result<u32> {
let console = OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty0")?;
let mut vt: i32 = 0;
unsafe {
if libc::ioctl(console.as_raw_fd(), VT_OPENQRY as _, &mut vt) < 0 {
anyhow::bail!("Failed to find unused VT");
}
}
if vt <= 0 {
anyhow::bail!("No unused VT available");
}
Ok(vt as u32)
}
/// Switch to a specific VT
pub fn switch_to_vt(vt: u32) -> anyhow::Result<()> {
let console = OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty0")?;
unsafe {
if libc::ioctl(console.as_raw_fd(), VT_ACTIVATE as _, vt) < 0 {
anyhow::bail!("Failed to activate VT {}", vt);
}
if libc::ioctl(console.as_raw_fd(), VT_WAITACTIVE as _, vt) < 0 {
anyhow::bail!("Failed to wait for VT {}", vt);
}
}
Ok(())
}
2.3 Greeter Process Manager
// gardmd/src/greeter.rs
use nix::unistd::{Uid, Gid, setuid, setgid, User};
use std::process::{Command, Child};
pub struct GreeterProcess {
process: Child,
}
impl GreeterProcess {
/// Start the greeter as the gardm user
pub fn start(greeter_cmd: &str, display: &str) -> anyhow::Result<Self> {
// Get gardm user info
let user = User::from_name("gardm")?
.ok_or_else(|| anyhow::anyhow!("gardm user not found"))?;
let mut cmd = Command::new(greeter_cmd);
cmd.env("DISPLAY", display)
.env("XDG_SESSION_CLASS", "greeter")
.env("XDG_SESSION_TYPE", "x11");
// Run as gardm user
unsafe {
cmd.pre_exec(move || {
setgid(Gid::from_raw(user.gid.as_raw()))?;
setuid(Uid::from_raw(user.uid.as_raw()))?;
Ok(())
});
}
let process = cmd.spawn()?;
tracing::info!("Started greeter (pid={})", process.id());
Ok(Self { process })
}
/// Check if greeter is still running
pub fn is_running(&mut self) -> bool {
matches!(self.process.try_wait(), Ok(None))
}
/// Wait for greeter to exit
pub fn wait(&mut self) -> anyhow::Result<std::process::ExitStatus> {
Ok(self.process.wait()?)
}
/// Kill the greeter
pub fn kill(&mut self) -> anyhow::Result<()> {
self.process.kill()?;
self.process.wait()?;
Ok(())
}
}
2.4 Session Launcher
// gardmd/src/session.rs
use nix::unistd::{Uid, Gid, setuid, setgid, User, initgroups, chdir};
use std::process::{Command, Child};
use std::ffi::CString;
pub struct UserSession {
process: Child,
username: String,
}
impl UserSession {
/// Start a user session
pub fn start(
username: &str,
session_cmd: &[String],
display: &str,
vt: u32,
) -> anyhow::Result<Self> {
let user = User::from_name(username)?
.ok_or_else(|| anyhow::anyhow!("User {} not found", username))?;
let home = user.dir.to_string_lossy().to_string();
let shell = user.shell.to_string_lossy().to_string();
let uid = user.uid;
let gid = user.gid;
let username_c = CString::new(username)?;
// Build session command
let (cmd_path, cmd_args) = if session_cmd.is_empty() {
// Default to gar-session.sh
("/usr/local/bin/gar-session.sh".to_string(), vec![])
} else {
(session_cmd[0].clone(), session_cmd[1..].to_vec())
};
let mut cmd = Command::new(&cmd_path);
cmd.args(&cmd_args)
.env("DISPLAY", display)
.env("HOME", &home)
.env("USER", username)
.env("LOGNAME", username)
.env("SHELL", &shell)
.env("XDG_SESSION_TYPE", "x11")
.env("XDG_VTNR", vt.to_string())
.env("XDG_SEAT", "seat0");
// Run as the user
unsafe {
cmd.pre_exec(move || {
// Set groups
initgroups(&username_c, gid)?;
setgid(gid)?;
setuid(uid)?;
// Change to home directory
let home_c = CString::new(home.as_str())?;
chdir(&home_c)?;
Ok(())
});
}
let process = cmd.spawn()?;
tracing::info!("Started session for {} (pid={})", username, process.id());
Ok(Self {
process,
username: username.to_string(),
})
}
/// Wait for session to end
pub fn wait(&mut self) -> anyhow::Result<std::process::ExitStatus> {
Ok(self.process.wait()?)
}
}
2.5 Main Loop Integration
// gardmd/src/main.rs
async fn run() -> anyhow::Result<()> {
let config = Config::load()?;
// Find VT to use
let vt = if config.general.vt > 0 {
config.general.vt
} else {
vt::find_unused_vt()?
};
// Start X server
let display = ":0";
let mut x_server = XServer::start(display, vt)?;
// Switch to our VT
vt::switch_to_vt(vt)?;
// Start IPC server
let ipc = IpcServer::new("/run/gardm.sock").await?;
let mut auth = AuthSession::new();
sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?;
loop {
// Start greeter
let mut greeter = GreeterProcess::start(&config.general.greeter, display)?;
// Handle greeter authentication
let session_info = handle_greeter_auth(&ipc, &mut auth).await?;
// Kill greeter before starting session
greeter.kill()?;
// Start user session
let mut session = UserSession::start(
&session_info.username,
&session_info.cmd,
display,
vt,
)?;
// Wait for session to end
let status = session.wait()?;
tracing::info!("Session ended with status: {:?}", status);
// Loop back to greeter
}
}
struct SessionInfo {
username: String,
cmd: Vec<String>,
}
async fn handle_greeter_auth(
ipc: &IpcServer,
auth: &mut AuthSession,
) -> anyhow::Result<SessionInfo> {
let mut client = ipc.accept().await?;
loop {
let request = client.read_request().await?
.ok_or_else(|| anyhow::anyhow!("Greeter disconnected"))?;
match request {
Request::CreateSession { username } => {
let response = auth.create_session(&username)?;
client.send_response(&response.into()).await?;
}
Request::Authenticate { response } => {
let result = auth.authenticate(&response)?;
client.send_response(&result.clone().into()).await?;
if matches!(result, AuthResponse::Success) {
// Read StartSession request
if let Some(Request::StartSession { cmd, .. }) =
client.read_request().await?
{
return Ok(SessionInfo {
username: auth.username().unwrap().to_string(),
cmd,
});
}
}
}
Request::CancelSession => {
auth.cancel();
client.send_response(&Response::Success).await?;
}
_ => {
client.send_response(&Response::Error {
message: "Unexpected request".to_string(),
}).await?;
}
}
}
}
Acceptance Criteria
- Xorg starts on specified VT
- Greeter launches as unprivileged gardm user
- After auth success, greeter is killed and session starts
- Session runs as authenticated user with correct environment
- After session logout, greeter restarts
- X server crash triggers recovery (restart X + greeter)
Pitfalls to Avoid
- Don't forget VT permissions - Xorg needs access to /dev/tty*
- X server takes time to start - must wait for it to be ready
- Environment inheritance - don't leak root environment to session
- Group membership - user needs video, audio groups for hardware access
- Home directory - chdir to $HOME before starting session
- Don't block on X - use async/spawn for process management
Testing
# Test X server startup (need to be root, on real TTY)
sudo ./target/release/gardmd
# Alternative: Test in Xephyr (nested X)
Xephyr -br -ac -noreset -screen 1280x720 :1 &
DISPLAY=:1 ./target/release/gardm-greeter
# Create gardm user for testing
sudo useradd -r -s /usr/bin/nologin -d /var/lib/gardm gardm
sudo mkdir -p /var/lib/gardm
sudo chown gardm:gardm /var/lib/gardm
Dependencies for This Sprint
# gardmd/Cargo.toml
[dependencies]
x11rb = "0.13"
libc = "0.2"
nix = { version = "0.27", features = ["user", "process", "ioctl"] }
Next Sprint
Sprint 3 will build the graphical greeter UI with Cairo/Pango.
View source
| 1 | # Sprint 2: X11 Server Management |
| 2 | |
| 3 | **Goal:** Start and manage Xorg server, launch greeter process, and handle session startup. |
| 4 | |
| 5 | ## Objectives |
| 6 | |
| 7 | - Start Xorg on appropriate VT with correct permissions |
| 8 | - Launch greeter as unprivileged user |
| 9 | - Start user session after successful auth |
| 10 | - Handle X server lifecycle (crash recovery, clean shutdown) |
| 11 | - Integrate with systemd-logind for session registration |
| 12 | |
| 13 | ## Background: X11 Display Manager Flow |
| 14 | |
| 15 | ``` |
| 16 | 1. gardmd starts |
| 17 | 2. gardmd spawns Xorg on VT (e.g., :0 on vt1) |
| 18 | 3. gardmd waits for X to be ready (connect test) |
| 19 | 4. gardmd spawns greeter as unprivileged user |
| 20 | 5. Greeter authenticates user via IPC |
| 21 | 6. gardmd kills greeter, starts user session |
| 22 | 7. User session runs until logout |
| 23 | 8. gardmd restarts greeter (loop back to 4) |
| 24 | ``` |
| 25 | |
| 26 | ## Tasks |
| 27 | |
| 28 | ### 2.1 X Server Launcher |
| 29 | |
| 30 | ```rust |
| 31 | // gardmd/src/x11.rs |
| 32 | |
| 33 | use nix::unistd::{fork, ForkResult, setsid, Pid}; |
| 34 | use std::process::{Command, Child}; |
| 35 | use std::time::Duration; |
| 36 | |
| 37 | pub struct XServer { |
| 38 | process: Child, |
| 39 | display: String, |
| 40 | vt: u32, |
| 41 | } |
| 42 | |
| 43 | impl XServer { |
| 44 | /// Start Xorg server on specified display and VT |
| 45 | pub fn start(display: &str, vt: u32) -> anyhow::Result<Self> { |
| 46 | // Xorg arguments |
| 47 | let mut cmd = Command::new("/usr/bin/Xorg"); |
| 48 | cmd.arg(display) |
| 49 | .arg(&format!("vt{}", vt)) |
| 50 | .arg("-keeptty") |
| 51 | .arg("-noreset") |
| 52 | .arg("-novtswitch") |
| 53 | .arg("-nolisten").arg("tcp"); |
| 54 | |
| 55 | // For multi-seat support (future): |
| 56 | // cmd.arg("-seat").arg("seat0"); |
| 57 | |
| 58 | // Capture output for debugging |
| 59 | cmd.stdout(std::process::Stdio::piped()) |
| 60 | .stderr(std::process::Stdio::piped()); |
| 61 | |
| 62 | let process = cmd.spawn()?; |
| 63 | |
| 64 | tracing::info!("Started Xorg on {} (vt{}, pid={})", |
| 65 | display, vt, process.id()); |
| 66 | |
| 67 | let server = Self { |
| 68 | process, |
| 69 | display: display.to_string(), |
| 70 | vt, |
| 71 | }; |
| 72 | |
| 73 | // Wait for X to be ready |
| 74 | server.wait_ready(Duration::from_secs(10))?; |
| 75 | |
| 76 | Ok(server) |
| 77 | } |
| 78 | |
| 79 | /// Wait for X server to be ready by attempting connection |
| 80 | fn wait_ready(&self, timeout: Duration) -> anyhow::Result<()> { |
| 81 | use std::time::Instant; |
| 82 | |
| 83 | let start = Instant::now(); |
| 84 | while start.elapsed() < timeout { |
| 85 | // Try to connect to X server |
| 86 | match x11rb::connect(Some(&self.display)) { |
| 87 | Ok(_) => { |
| 88 | tracing::debug!("X server ready on {}", self.display); |
| 89 | return Ok(()); |
| 90 | } |
| 91 | Err(_) => { |
| 92 | std::thread::sleep(Duration::from_millis(100)); |
| 93 | } |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | anyhow::bail!("X server failed to become ready within {:?}", timeout); |
| 98 | } |
| 99 | |
| 100 | /// Get the display string (e.g., ":0") |
| 101 | pub fn display(&self) -> &str { |
| 102 | &self.display |
| 103 | } |
| 104 | |
| 105 | /// Get the VT number |
| 106 | pub fn vt(&self) -> u32 { |
| 107 | self.vt |
| 108 | } |
| 109 | |
| 110 | /// Check if X server is still running |
| 111 | pub fn is_running(&mut self) -> bool { |
| 112 | match self.process.try_wait() { |
| 113 | Ok(None) => true, |
| 114 | _ => false, |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | /// Stop the X server |
| 119 | pub fn stop(&mut self) -> anyhow::Result<()> { |
| 120 | tracing::info!("Stopping X server"); |
| 121 | self.process.kill()?; |
| 122 | self.process.wait()?; |
| 123 | Ok(()) |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | impl Drop for XServer { |
| 128 | fn drop(&mut self) { |
| 129 | let _ = self.stop(); |
| 130 | } |
| 131 | } |
| 132 | ``` |
| 133 | |
| 134 | ### 2.2 VT Allocation |
| 135 | |
| 136 | ```rust |
| 137 | // gardmd/src/vt.rs |
| 138 | |
| 139 | use std::fs::OpenOptions; |
| 140 | use std::os::unix::io::AsRawFd; |
| 141 | use nix::ioctl_read_bad; |
| 142 | |
| 143 | // ioctl for getting/setting VT |
| 144 | const VT_GETSTATE: u64 = 0x5603; |
| 145 | const VT_ACTIVATE: u64 = 0x5606; |
| 146 | const VT_WAITACTIVE: u64 = 0x5607; |
| 147 | const VT_OPENQRY: u64 = 0x5600; |
| 148 | |
| 149 | #[repr(C)] |
| 150 | pub struct VtStat { |
| 151 | pub v_active: u16, |
| 152 | pub v_signal: u16, |
| 153 | pub v_state: u16, |
| 154 | } |
| 155 | |
| 156 | /// Find an unused VT |
| 157 | pub fn find_unused_vt() -> anyhow::Result<u32> { |
| 158 | let console = OpenOptions::new() |
| 159 | .read(true) |
| 160 | .write(true) |
| 161 | .open("/dev/tty0")?; |
| 162 | |
| 163 | let mut vt: i32 = 0; |
| 164 | unsafe { |
| 165 | if libc::ioctl(console.as_raw_fd(), VT_OPENQRY as _, &mut vt) < 0 { |
| 166 | anyhow::bail!("Failed to find unused VT"); |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | if vt <= 0 { |
| 171 | anyhow::bail!("No unused VT available"); |
| 172 | } |
| 173 | |
| 174 | Ok(vt as u32) |
| 175 | } |
| 176 | |
| 177 | /// Switch to a specific VT |
| 178 | pub fn switch_to_vt(vt: u32) -> anyhow::Result<()> { |
| 179 | let console = OpenOptions::new() |
| 180 | .read(true) |
| 181 | .write(true) |
| 182 | .open("/dev/tty0")?; |
| 183 | |
| 184 | unsafe { |
| 185 | if libc::ioctl(console.as_raw_fd(), VT_ACTIVATE as _, vt) < 0 { |
| 186 | anyhow::bail!("Failed to activate VT {}", vt); |
| 187 | } |
| 188 | if libc::ioctl(console.as_raw_fd(), VT_WAITACTIVE as _, vt) < 0 { |
| 189 | anyhow::bail!("Failed to wait for VT {}", vt); |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | Ok(()) |
| 194 | } |
| 195 | ``` |
| 196 | |
| 197 | ### 2.3 Greeter Process Manager |
| 198 | |
| 199 | ```rust |
| 200 | // gardmd/src/greeter.rs |
| 201 | |
| 202 | use nix::unistd::{Uid, Gid, setuid, setgid, User}; |
| 203 | use std::process::{Command, Child}; |
| 204 | |
| 205 | pub struct GreeterProcess { |
| 206 | process: Child, |
| 207 | } |
| 208 | |
| 209 | impl GreeterProcess { |
| 210 | /// Start the greeter as the gardm user |
| 211 | pub fn start(greeter_cmd: &str, display: &str) -> anyhow::Result<Self> { |
| 212 | // Get gardm user info |
| 213 | let user = User::from_name("gardm")? |
| 214 | .ok_or_else(|| anyhow::anyhow!("gardm user not found"))?; |
| 215 | |
| 216 | let mut cmd = Command::new(greeter_cmd); |
| 217 | cmd.env("DISPLAY", display) |
| 218 | .env("XDG_SESSION_CLASS", "greeter") |
| 219 | .env("XDG_SESSION_TYPE", "x11"); |
| 220 | |
| 221 | // Run as gardm user |
| 222 | unsafe { |
| 223 | cmd.pre_exec(move || { |
| 224 | setgid(Gid::from_raw(user.gid.as_raw()))?; |
| 225 | setuid(Uid::from_raw(user.uid.as_raw()))?; |
| 226 | Ok(()) |
| 227 | }); |
| 228 | } |
| 229 | |
| 230 | let process = cmd.spawn()?; |
| 231 | tracing::info!("Started greeter (pid={})", process.id()); |
| 232 | |
| 233 | Ok(Self { process }) |
| 234 | } |
| 235 | |
| 236 | /// Check if greeter is still running |
| 237 | pub fn is_running(&mut self) -> bool { |
| 238 | matches!(self.process.try_wait(), Ok(None)) |
| 239 | } |
| 240 | |
| 241 | /// Wait for greeter to exit |
| 242 | pub fn wait(&mut self) -> anyhow::Result<std::process::ExitStatus> { |
| 243 | Ok(self.process.wait()?) |
| 244 | } |
| 245 | |
| 246 | /// Kill the greeter |
| 247 | pub fn kill(&mut self) -> anyhow::Result<()> { |
| 248 | self.process.kill()?; |
| 249 | self.process.wait()?; |
| 250 | Ok(()) |
| 251 | } |
| 252 | } |
| 253 | ``` |
| 254 | |
| 255 | ### 2.4 Session Launcher |
| 256 | |
| 257 | ```rust |
| 258 | // gardmd/src/session.rs |
| 259 | |
| 260 | use nix::unistd::{Uid, Gid, setuid, setgid, User, initgroups, chdir}; |
| 261 | use std::process::{Command, Child}; |
| 262 | use std::ffi::CString; |
| 263 | |
| 264 | pub struct UserSession { |
| 265 | process: Child, |
| 266 | username: String, |
| 267 | } |
| 268 | |
| 269 | impl UserSession { |
| 270 | /// Start a user session |
| 271 | pub fn start( |
| 272 | username: &str, |
| 273 | session_cmd: &[String], |
| 274 | display: &str, |
| 275 | vt: u32, |
| 276 | ) -> anyhow::Result<Self> { |
| 277 | let user = User::from_name(username)? |
| 278 | .ok_or_else(|| anyhow::anyhow!("User {} not found", username))?; |
| 279 | |
| 280 | let home = user.dir.to_string_lossy().to_string(); |
| 281 | let shell = user.shell.to_string_lossy().to_string(); |
| 282 | let uid = user.uid; |
| 283 | let gid = user.gid; |
| 284 | let username_c = CString::new(username)?; |
| 285 | |
| 286 | // Build session command |
| 287 | let (cmd_path, cmd_args) = if session_cmd.is_empty() { |
| 288 | // Default to gar-session.sh |
| 289 | ("/usr/local/bin/gar-session.sh".to_string(), vec![]) |
| 290 | } else { |
| 291 | (session_cmd[0].clone(), session_cmd[1..].to_vec()) |
| 292 | }; |
| 293 | |
| 294 | let mut cmd = Command::new(&cmd_path); |
| 295 | cmd.args(&cmd_args) |
| 296 | .env("DISPLAY", display) |
| 297 | .env("HOME", &home) |
| 298 | .env("USER", username) |
| 299 | .env("LOGNAME", username) |
| 300 | .env("SHELL", &shell) |
| 301 | .env("XDG_SESSION_TYPE", "x11") |
| 302 | .env("XDG_VTNR", vt.to_string()) |
| 303 | .env("XDG_SEAT", "seat0"); |
| 304 | |
| 305 | // Run as the user |
| 306 | unsafe { |
| 307 | cmd.pre_exec(move || { |
| 308 | // Set groups |
| 309 | initgroups(&username_c, gid)?; |
| 310 | setgid(gid)?; |
| 311 | setuid(uid)?; |
| 312 | |
| 313 | // Change to home directory |
| 314 | let home_c = CString::new(home.as_str())?; |
| 315 | chdir(&home_c)?; |
| 316 | |
| 317 | Ok(()) |
| 318 | }); |
| 319 | } |
| 320 | |
| 321 | let process = cmd.spawn()?; |
| 322 | tracing::info!("Started session for {} (pid={})", username, process.id()); |
| 323 | |
| 324 | Ok(Self { |
| 325 | process, |
| 326 | username: username.to_string(), |
| 327 | }) |
| 328 | } |
| 329 | |
| 330 | /// Wait for session to end |
| 331 | pub fn wait(&mut self) -> anyhow::Result<std::process::ExitStatus> { |
| 332 | Ok(self.process.wait()?) |
| 333 | } |
| 334 | } |
| 335 | ``` |
| 336 | |
| 337 | ### 2.5 Main Loop Integration |
| 338 | |
| 339 | ```rust |
| 340 | // gardmd/src/main.rs |
| 341 | |
| 342 | async fn run() -> anyhow::Result<()> { |
| 343 | let config = Config::load()?; |
| 344 | |
| 345 | // Find VT to use |
| 346 | let vt = if config.general.vt > 0 { |
| 347 | config.general.vt |
| 348 | } else { |
| 349 | vt::find_unused_vt()? |
| 350 | }; |
| 351 | |
| 352 | // Start X server |
| 353 | let display = ":0"; |
| 354 | let mut x_server = XServer::start(display, vt)?; |
| 355 | |
| 356 | // Switch to our VT |
| 357 | vt::switch_to_vt(vt)?; |
| 358 | |
| 359 | // Start IPC server |
| 360 | let ipc = IpcServer::new("/run/gardm.sock").await?; |
| 361 | let mut auth = AuthSession::new(); |
| 362 | |
| 363 | sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?; |
| 364 | |
| 365 | loop { |
| 366 | // Start greeter |
| 367 | let mut greeter = GreeterProcess::start(&config.general.greeter, display)?; |
| 368 | |
| 369 | // Handle greeter authentication |
| 370 | let session_info = handle_greeter_auth(&ipc, &mut auth).await?; |
| 371 | |
| 372 | // Kill greeter before starting session |
| 373 | greeter.kill()?; |
| 374 | |
| 375 | // Start user session |
| 376 | let mut session = UserSession::start( |
| 377 | &session_info.username, |
| 378 | &session_info.cmd, |
| 379 | display, |
| 380 | vt, |
| 381 | )?; |
| 382 | |
| 383 | // Wait for session to end |
| 384 | let status = session.wait()?; |
| 385 | tracing::info!("Session ended with status: {:?}", status); |
| 386 | |
| 387 | // Loop back to greeter |
| 388 | } |
| 389 | } |
| 390 | |
| 391 | struct SessionInfo { |
| 392 | username: String, |
| 393 | cmd: Vec<String>, |
| 394 | } |
| 395 | |
| 396 | async fn handle_greeter_auth( |
| 397 | ipc: &IpcServer, |
| 398 | auth: &mut AuthSession, |
| 399 | ) -> anyhow::Result<SessionInfo> { |
| 400 | let mut client = ipc.accept().await?; |
| 401 | |
| 402 | loop { |
| 403 | let request = client.read_request().await? |
| 404 | .ok_or_else(|| anyhow::anyhow!("Greeter disconnected"))?; |
| 405 | |
| 406 | match request { |
| 407 | Request::CreateSession { username } => { |
| 408 | let response = auth.create_session(&username)?; |
| 409 | client.send_response(&response.into()).await?; |
| 410 | } |
| 411 | Request::Authenticate { response } => { |
| 412 | let result = auth.authenticate(&response)?; |
| 413 | client.send_response(&result.clone().into()).await?; |
| 414 | |
| 415 | if matches!(result, AuthResponse::Success) { |
| 416 | // Read StartSession request |
| 417 | if let Some(Request::StartSession { cmd, .. }) = |
| 418 | client.read_request().await? |
| 419 | { |
| 420 | return Ok(SessionInfo { |
| 421 | username: auth.username().unwrap().to_string(), |
| 422 | cmd, |
| 423 | }); |
| 424 | } |
| 425 | } |
| 426 | } |
| 427 | Request::CancelSession => { |
| 428 | auth.cancel(); |
| 429 | client.send_response(&Response::Success).await?; |
| 430 | } |
| 431 | _ => { |
| 432 | client.send_response(&Response::Error { |
| 433 | message: "Unexpected request".to_string(), |
| 434 | }).await?; |
| 435 | } |
| 436 | } |
| 437 | } |
| 438 | } |
| 439 | ``` |
| 440 | |
| 441 | ## Acceptance Criteria |
| 442 | |
| 443 | 1. Xorg starts on specified VT |
| 444 | 2. Greeter launches as unprivileged gardm user |
| 445 | 3. After auth success, greeter is killed and session starts |
| 446 | 4. Session runs as authenticated user with correct environment |
| 447 | 5. After session logout, greeter restarts |
| 448 | 6. X server crash triggers recovery (restart X + greeter) |
| 449 | |
| 450 | ## Pitfalls to Avoid |
| 451 | |
| 452 | 1. **Don't forget VT permissions** - Xorg needs access to /dev/tty* |
| 453 | 2. **X server takes time to start** - must wait for it to be ready |
| 454 | 3. **Environment inheritance** - don't leak root environment to session |
| 455 | 4. **Group membership** - user needs video, audio groups for hardware access |
| 456 | 5. **Home directory** - chdir to $HOME before starting session |
| 457 | 6. **Don't block on X** - use async/spawn for process management |
| 458 | |
| 459 | ## Testing |
| 460 | |
| 461 | ```bash |
| 462 | # Test X server startup (need to be root, on real TTY) |
| 463 | sudo ./target/release/gardmd |
| 464 | |
| 465 | # Alternative: Test in Xephyr (nested X) |
| 466 | Xephyr -br -ac -noreset -screen 1280x720 :1 & |
| 467 | DISPLAY=:1 ./target/release/gardm-greeter |
| 468 | |
| 469 | # Create gardm user for testing |
| 470 | sudo useradd -r -s /usr/bin/nologin -d /var/lib/gardm gardm |
| 471 | sudo mkdir -p /var/lib/gardm |
| 472 | sudo chown gardm:gardm /var/lib/gardm |
| 473 | ``` |
| 474 | |
| 475 | ## Dependencies for This Sprint |
| 476 | |
| 477 | ```toml |
| 478 | # gardmd/Cargo.toml |
| 479 | [dependencies] |
| 480 | x11rb = "0.13" |
| 481 | libc = "0.2" |
| 482 | nix = { version = "0.27", features = ["user", "process", "ioctl"] } |
| 483 | ``` |
| 484 | |
| 485 | ## Next Sprint |
| 486 | |
| 487 | Sprint 3 will build the graphical greeter UI with Cairo/Pango. |