markdown · 12887 bytes Raw Blame History

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

  1. Xorg starts on specified VT
  2. Greeter launches as unprivileged gardm user
  3. After auth success, greeter is killed and session starts
  4. Session runs as authenticated user with correct environment
  5. After session logout, greeter restarts
  6. X server crash triggers recovery (restart X + greeter)

Pitfalls to Avoid

  1. Don't forget VT permissions - Xorg needs access to /dev/tty*
  2. X server takes time to start - must wait for it to be ready
  3. Environment inheritance - don't leak root environment to session
  4. Group membership - user needs video, audio groups for hardware access
  5. Home directory - chdir to $HOME before starting session
  6. 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.