git.lucas.co / cce-display-manager
login greeter
git clone https://git.lucas.co/cce-display-manager.git

src/main.rs (82.8K)

   1 use cce_ui::widget::{
   2     Button, ContentBg, WidgetHost, ElementState, MouseButton, Key, NamedKey, KeyEvent, TextBox,
   3     focus, MouseScrollDelta
   4 };
   5 use wayland_client::QueueHandle;
   6 use cce_ui::engine::{EngineState, LogicalPosition, LogicalSize, WindowSettings, Vertex, quad_vertices};
   7 use calloop::channel;
   8 
   9 
  10 
  11 
  12 
  13 fn widget_vertices(w: &dyn WidgetHost, sw: f32, sh: f32) -> Vec<Vertex> {
  14     let (x, y, ww, h) = w.rect();
  15     quad_vertices(x, y, ww, h, sw, sh, w.color()).to_vec()
  16 }
  17 
  18 
  19 
  20 
  21 #[derive(Debug, Clone)]
  22 struct Session {
  23     name: String,
  24     exec: String,
  25     is_wayland: bool,
  26 }
  27 
  28 /// Parse a greeter `AUTH_SUCCESS|user|exec|is_wayland|password` line.
  29 ///
  30 /// The password is the LAST field and is taken verbatim to the end of the
  31 /// line (`splitn`), because it may itself contain `|` — a plain `split`
  32 /// silently produced six fields and dropped the login (the greeter had
  33 /// already authenticated, so the user just hung at a dead greeter).
  34 fn parse_auth_success(line: &str) -> Option<(String, String, bool, String)> {
  35     let mut parts = line.splitn(5, '|');
  36     if parts.next() != Some("AUTH_SUCCESS") {
  37         return None;
  38     }
  39     let username = parts.next()?.to_string();
  40     let exec = parts.next()?.to_string();
  41     let is_wayland = parts.next()?.parse::<bool>().unwrap_or(true);
  42     let password = parts.next()?.to_string();
  43     Some((username, exec, is_wayland, password))
  44 }
  45 
  46 fn sanitize_exec(exec: &str) -> (String, Vec<String>) {
  47     let mut parts = Vec::new();
  48     for part in exec.split_whitespace() {
  49         if part.starts_with('%') {
  50             continue; // ignore desktop entry field codes
  51         }
  52         parts.push(part.to_string());
  53     }
  54     if parts.is_empty() {
  55         return (String::new(), Vec::new());
  56     }
  57     let cmd = parts.remove(0);
  58     (cmd, parts)
  59 }
  60 
  61 fn parse_desktop_file(path: &std::path::Path, is_wayland: bool) -> Result<Session, std::io::Error> {
  62     let content = std::fs::read_to_string(path)?;
  63     let mut name = None;
  64     let mut exec = None;
  65     for line in content.lines() {
  66         let line = line.trim();
  67         if line.starts_with("Name=") {
  68             name = Some(line["Name=".len()..].to_string());
  69         } else if line.starts_with("Exec=") {
  70             exec = Some(line["Exec=".len()..].to_string());
  71         }
  72     }
  73     if let (Some(n), Some(e)) = (name, exec) {
  74         Ok(Session { name: n, exec: e, is_wayland })
  75     } else {
  76         Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid desktop file"))
  77     }
  78 }
  79 
  80 fn discover_sessions() -> Vec<Session> {
  81     let mut sessions = Vec::new();
  82     if let Ok(entries) = std::fs::read_dir("/usr/share/wayland-sessions") {
  83         for entry in entries.flatten() {
  84             if entry.path().extension().map_or(false, |ext| ext == "desktop") {
  85                 if let Ok(s) = parse_desktop_file(&entry.path(), true) {
  86                     sessions.push(s);
  87                 }
  88             }
  89         }
  90     }
  91     if let Ok(entries) = std::fs::read_dir("/usr/share/xsessions") {
  92         for entry in entries.flatten() {
  93             if entry.path().extension().map_or(false, |ext| ext == "desktop") {
  94                 if let Ok(s) = parse_desktop_file(&entry.path(), false) {
  95                     sessions.push(s);
  96                 }
  97             }
  98         }
  99     }
 100     sessions.push(Session {
 101         name: "Bash Shell".to_string(),
 102         exec: "bash".to_string(),
 103         is_wayland: true,
 104     });
 105     sessions
 106 }
 107 
 108 // ── Custom LoginCard Container WidgetHost (narrow traits, wrapped in Adapted) ──
 109 #[derive(Debug, Clone)]
 110 struct LoginCard;
 111 
 112 impl LoginCard {
 113     fn new() -> cce_ui::widget::Adapted<LoginCard> {
 114         cce_ui::widget::Adapted::new(LoginCard)
 115     }
 116 }
 117 
 118 impl cce_ui::widget::Layout for LoginCard {}
 119 
 120 impl cce_ui::widget::Paint for LoginCard {
 121     fn color(&self) -> [f32; 4] { [0.25, 0.25, 0.28, 0.75] } // Premium gray card background with transparency
 122 
 123     fn paint(&self, rect: cce_ui::scene::layout::Rect, pc: &mut cce_ui::scene::paint::PaintCtx) {
 124         // Only the card's header labels: the card plate (soft radial-glow blob) is drawn
 125         // via custom_vertices, not the display list — and the direct all_quads read in
 126         // custom_vertices relies on this paint emitting NO plain quads, like the legacy
 127         // empty extra_quads. The card is laid out full-screen; the header centers off it.
 128         let card_x = (rect.width - 360.0) / 2.0;
 129         let card_y = (rect.height - 300.0) / 2.0;
 130         pc.text("CCE DISPLAY MANAGER".to_string(), card_x + 30.0, card_y + 30.0, 15.0, [0xee, 0xee, 0xf5]);
 131         pc.text("Authenticate to begin your session".to_string(), card_x + 30.0, card_y + 50.0, 11.0, [0x83, 0x83, 0x8a]);
 132     }
 133 }
 134 
 135 impl cce_ui::widget::Input for LoginCard {}
 136 
 137 #[derive(Debug, Clone)]
 138 struct StatusLabel {
 139     pub text: String,
 140     pub is_error: bool,
 141 }
 142 
 143 impl StatusLabel {
 144     fn new(text: String) -> cce_ui::widget::Adapted<StatusLabel> {
 145         cce_ui::widget::Adapted::new(Self { text, is_error: false })
 146     }
 147 }
 148 
 149 impl cce_ui::widget::Layout for StatusLabel {}
 150 
 151 impl cce_ui::widget::Paint for StatusLabel {
 152     fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] } // Transparent background
 153 
 154     fn paint(&self, rect: cce_ui::scene::layout::Rect, pc: &mut cce_ui::scene::paint::PaintCtx) {
 155         let col = if self.is_error {
 156             [0xee, 0x5c, 0x5c] // Soft red
 157         } else {
 158             [0x83, 0x83, 0x8a] // Dim text
 159         };
 160         pc.text(self.text.clone(), rect.x, rect.y, 11.0, col);
 161     }
 162 }
 163 
 164 impl cce_ui::widget::Input for StatusLabel {}
 165 
 166 #[derive(Debug, Clone)]
 167 struct SessionList {
 168     sessions: Vec<Session>,
 169     selected_idx: usize,
 170     hovered_idx: Option<usize>,
 171 }
 172 
 173 impl SessionList {
 174     fn new(sessions: Vec<Session>) -> cce_ui::widget::Adapted<SessionList> {
 175         cce_ui::widget::Adapted::new(Self {
 176             sessions,
 177             selected_idx: 0,
 178             hovered_idx: None,
 179         })
 180     }
 181 
 182     fn selected_session(&self) -> Option<&Session> {
 183         self.sessions.get(self.selected_idx)
 184     }
 185 
 186     /// Row rect of item `i` within the laid-out panel rect (header is 40px tall).
 187     fn item_rect(&self, rect: cce_ui::scene::layout::Rect, i: usize) -> (f32, f32, f32, f32) {
 188         (rect.x + 10.0, rect.y + 40.0 + i as f32 * 36.0, rect.width - 20.0, 32.0)
 189     }
 190 }
 191 
 192 impl cce_ui::widget::Layout for SessionList {}
 193 
 194 impl cce_ui::widget::Paint for SessionList {
 195     fn color(&self) -> [f32; 4] { [0.07, 0.07, 0.10, 0.70] } // Semi-transparent sleek dark card background
 196 
 197     fn paint(&self, rect: cce_ui::scene::layout::Rect, pc: &mut cce_ui::scene::paint::PaintCtx) {
 198         use cce_ui::scene::layout::Rect;
 199         // The legacy panel never drew its base color through the display getters (no
 200         // rounded corners, extra_quads only) — same here: borders, selection, hover.
 201         let border_color = [0.20, 0.40, 0.65, 0.5];
 202         pc.quad(Rect { x: rect.x, y: rect.y, width: rect.width, height: 1.5 }, border_color); // top
 203         pc.quad(Rect { x: rect.x, y: rect.y + rect.height - 1.5, width: rect.width, height: 1.5 }, border_color); // bottom
 204         pc.quad(Rect { x: rect.x, y: rect.y, width: 1.5, height: rect.height }, border_color); // left
 205         pc.quad(Rect { x: rect.x + rect.width - 1.5, y: rect.y, width: 1.5, height: rect.height }, border_color); // right
 206 
 207         let item_w = rect.width - 20.0;
 208 
 209         // Selected item background
 210         let selected_color = [0.20, 0.40, 0.65, 0.8]; // Solid blue highlight
 211         let sel_y = rect.y + 40.0 + self.selected_idx as f32 * 36.0;
 212         pc.quad(Rect { x: rect.x + 10.0, y: sel_y, width: item_w, height: 32.0 }, selected_color);
 213 
 214         // Hovered item background
 215         if let Some(h_idx) = self.hovered_idx {
 216             if h_idx != self.selected_idx && h_idx < self.sessions.len() {
 217                 let hover_color = [1.0, 1.0, 1.0, 0.06]; // Subtle white overlay
 218                 let h_y = rect.y + 40.0 + h_idx as f32 * 36.0;
 219                 pc.quad(Rect { x: rect.x + 10.0, y: h_y, width: item_w, height: 32.0 }, hover_color);
 220             }
 221         }
 222 
 223         // Header title + session rows
 224         pc.text("SESSION MANAGER".to_string(), rect.x + 15.0, rect.y + 18.0, 11.0, [0x83, 0x83, 0x8a]);
 225         for (i, session) in self.sessions.iter().enumerate() {
 226             let item_y = rect.y + 40.0 + i as f32 * 36.0;
 227             let display_name = if session.name == "Bash Shell" {
 228                 "Bash Shell".to_string()
 229             } else {
 230                 format!("{} ({})", session.name, if session.is_wayland { "Wayland" } else { "X11" })
 231             };
 232             let color = if i == self.selected_idx {
 233                 [0xff, 0xff, 0xff]
 234             } else {
 235                 [0xee, 0xee, 0xf5]
 236             };
 237             pc.text(display_name, rect.x + 20.0, item_y + 10.0, 12.0, color);
 238         }
 239     }
 240 }
 241 
 242 impl cce_ui::widget::Input for SessionList {
 243     fn on_event(&mut self, event: &cce_ui::widget::Event, ectx: &mut cce_ui::widget::EventCtx) -> bool {
 244         match event {
 245             // Hover row tracking — the legacy on_cursor_moved override, against the
 246             // routed rect (a move outside the panel clears the hover, as before).
 247             cce_ui::widget::Event::PointerMove { x, y, .. } => {
 248                 let old_hovered = self.hovered_idx;
 249                 self.hovered_idx = None;
 250                 let r = ectx.rect;
 251                 if *x >= r.x && *x <= r.x + r.width && *y >= r.y && *y <= r.y + r.height {
 252                     for i in 0..self.sessions.len() {
 253                         let (ix, iy, iw, ih) = self.item_rect(r, i);
 254                         if *x >= ix && *x <= ix + iw && *y >= iy && *y <= iy + ih {
 255                             self.hovered_idx = Some(i);
 256                             break;
 257                         }
 258                     }
 259                 }
 260                 self.hovered_idx != old_hovered
 261             }
 262             // Presses arrive hit-gated to the panel rect; select the clicked row.
 263             cce_ui::widget::Event::MouseButton {
 264                 button: MouseButton::Left,
 265                 state: ElementState::Pressed,
 266                 x,
 267                 y,
 268                 ..
 269             } => {
 270                 for i in 0..self.sessions.len() {
 271                     let (ix, iy, iw, ih) = self.item_rect(ectx.rect, i);
 272                     if *x >= ix && *x <= ix + iw && *y >= iy && *y <= iy + ih {
 273                         if self.selected_idx != i {
 274                             self.selected_idx = i;
 275                             return true;
 276                         }
 277                     }
 278                 }
 279                 false
 280             }
 281             _ => false,
 282         }
 283     }
 284 }
 285 
 286 // ── App State and Renderer ──
 287 struct State {
 288     bg: cce_ui::widget::Adapted<ContentBg>,
 289     card: cce_ui::widget::Adapted<LoginCard>,
 290     username_box: cce_ui::widget::Adapted<TextBox>,
 291     password_box: cce_ui::widget::Adapted<TextBox>,
 292     login_btn: cce_ui::widget::Adapted<cce_ui::widget::Button>,
 293     status_lbl: cce_ui::widget::Adapted<StatusLabel>,
 294     session_list: cce_ui::widget::Adapted<SessionList>,
 295     ui_context: cce_ui::context::UiContext,
 296 
 297 
 298     cursor_x: f32,
 299     cursor_y: f32,
 300 
 301     width: f32,
 302     height: f32,
 303     physical_width: u32,
 304     physical_height: u32,
 305     scale: f64,
 306 
 307     // State tracking
 308     login_success: bool,
 309     is_authenticating: bool,
 310     auth_request_id: u64,
 311     auth_sender: channel::Sender<AuthEvent>,
 312     auth_receiver: Option<channel::Channel<AuthEvent>>,
 313     // The fingerprint attempt runs in a helper *process* (`--fprint-auth`),
 314     // not a thread: pam_authenticate blocks inside pam_fprintd and cannot be
 315     // interrupted, but a process can be killed — and killing it drops its
 316     // D-Bus connection, which is what makes fprintd release the sensor claim.
 317     fprint_child: Option<std::process::Child>,
 318 }
 319 
 320 impl State {
 321     fn widgets_iter(&self) -> Vec<&dyn WidgetHost> {
 322         vec![
 323             &self.bg,
 324             &self.card,
 325             &self.username_box,
 326             &self.password_box,
 327             &self.login_btn,
 328             &self.status_lbl,
 329             &self.session_list,
 330         ]
 331     }
 332 
 333     /// (Re-)register the widget tree at the widgets' CURRENT addresses. `new()` cannot do
 334     /// this — it would capture pointers into its own stack frame that dangle once the State
 335     /// moves — so this runs at the top of every frame. register/link are id-keyed and
 336     /// idempotent, and everything that resolves id→ptr afterwards (the paint walk's descent,
 337     /// propagate_event, the all_* child aggregation) then reads live widgets.
 338     fn relink_tree(&mut self) {
 339         let ctx = &mut self.ui_context;
 340         // Root Container DISSOLVED (Phase 6ax): the card and the session list are the two
 341         // dispatch/walk roots; register them directly (link_parent_child used to do it as a
 342         // side effect of the root links).
 343         ctx.register_widget(self.bg.id(), self.bg.as_ptr_mut());
 344         ctx.register_widget(self.card.id(), self.card.as_ptr_mut());
 345         ctx.register_widget(self.session_list.id(), self.session_list.as_ptr_mut());
 346         focus::link_parent_child(&mut self.card, &mut self.username_box, ctx);
 347         focus::link_parent_child(&mut self.card, &mut self.password_box, ctx);
 348         focus::link_parent_child(&mut self.card, &mut self.login_btn, ctx);
 349         focus::link_parent_child(&mut self.card, &mut self.status_lbl, ctx);
 350         // Initial focus: new() only set the box's own flag; point the context at the live
 351         // widget carrying it. Never fires once a runtime set_focused/clear_focus has run
 352         // (clear_focus also clears both flags).
 353         if self.ui_context.focused_widget.is_none() {
 354             if self.username_box.base().focused {
 355                 self.ui_context.set_focused(&mut self.username_box);
 356             } else if self.password_box.base().focused {
 357                 self.ui_context.set_focused(&mut self.password_box);
 358             }
 359         }
 360     }
 361 
 362     #[allow(dead_code)]
 363     fn widgets_iter_mut(&mut self) -> Vec<&mut dyn WidgetHost> {
 364         vec![
 365             &mut self.bg,
 366             &mut self.card,
 367             &mut self.username_box,
 368             &mut self.password_box,
 369             &mut self.login_btn,
 370             &mut self.status_lbl,
 371             &mut self.session_list,
 372         ]
 373     }
 374 
 375     fn apply_layout(&mut self) {
 376         let sw = self.width;
 377         let sh = self.height;
 378 
 379         // Background spans the whole screen
 380         self.bg.set_rect(0.0, 0.0, sw, sh);
 381 
 382         // Center card configuration
 383         let card_w = 360.0;
 384         let card_h = 280.0;
 385         let card_x = (sw - card_w) / 2.0;
 386         let card_y = (sh - card_h) / 2.0;
 387         
 388         // Card is full screen to render aspect-ratio centered oval custom graphic
 389         self.card.set_rect(0.0, 0.0, sw, sh);
 390 
 391         // Child components inside login card
 392         let content_x = card_x + 30.0;
 393         
 394         // Username text box
 395         self.username_box.set_rect(content_x, card_y + 80.0, 300.0, 36.0);
 396         
 397         // Password password box
 398         self.password_box.set_rect(content_x, card_y + 145.0, 300.0, 36.0);
 399 
 400         // Login button (full-width of the contents)
 401         self.login_btn.set_rect(content_x, card_y + 205.0, 300.0, 36.0);
 402 
 403         // Status message
 404         self.status_lbl.set_rect(content_x, card_y + 252.0, 300.0, 20.0);
 405 
 406         // Session list on top left
 407         let list_w = 260.0;
 408         let list_h = 40.0 + self.session_list.sessions.len() as f32 * 36.0;
 409         self.session_list.set_rect(30.0, 30.0, list_w, list_h);
 410     }
 411 
 412     pub fn widgets_cursor_moved(&mut self, cx: f32, cy: f32) -> bool {
 413         let mut changed = false;
 414         let event = cce_ui::widget::Event::PointerMove {
 415             x: cx,
 416             y: cy,
 417             local_x: cx,
 418             local_y: cy,
 419         };
 420         // Routed (6bd shrink): the background rides the same router as the other roots.
 421         let bg_root = self.bg.id();
 422         if self.ui_context.propagate_event(&event, bg_root) {
 423             changed = true;
 424         }
 425         let sl_root = self.session_list.id();
 426         let card_root = self.card.id();
 427         if self.ui_context.propagate_event(&event, sl_root) {
 428             changed = true;
 429         }
 430         if self.ui_context.propagate_event(&event, card_root) {
 431             changed = true;
 432         }
 433         changed
 434     }
 435 
 436     pub fn widgets_mouse_input(&mut self, button: MouseButton, state: ElementState, cx: f32, cy: f32) -> bool {
 437         let mut changed = false;
 438         let event = cce_ui::widget::Event::MouseButton {
 439             button,
 440             state,
 441             x: cx,
 442             y: cy,
 443             local_x: cx,
 444             local_y: cy,
 445         };
 446         // Routed (6bd shrink); the bg result stays outside `handled` so the
 447         // unfocus-on-missed-press rule below keys on the session list + card only.
 448         let bg_root = self.bg.id();
 449         if self.ui_context.propagate_event(&event, bg_root) {
 450             changed = true;
 451         }
 452         let sl_root = self.session_list.id();
 453         let card_root = self.card.id();
 454         let handled = self.ui_context.propagate_event(&event, sl_root)
 455             || self.ui_context.propagate_event(&event, card_root);
 456         if button == MouseButton::Left && state == ElementState::Pressed {
 457             if !handled {
 458                 self.ui_context.clear_focus();
 459                 self.username_box.unfocus();
 460                 self.password_box.unfocus();
 461                 changed = true;
 462             }
 463         }
 464         if handled {
 465             changed = true;
 466         }
 467         changed
 468     }
 469 
 470     pub fn widgets_keyboard_input(&mut self, event: &KeyEvent) -> bool {
 471         let mut changed = false;
 472         let ui_event = cce_ui::widget::Event::KeyInput(event.clone());
 473         // Fully short-circuited (the 6ac rule): every propagate call delivers KeyInput
 474         // to the ctx-focused widget first, so a non-short-circuited chain would insert
 475         // a typed key once per root.
 476         let bg_root = self.bg.id();
 477         let sl_root = self.session_list.id();
 478         let card_root = self.card.id();
 479         if self.ui_context.propagate_event(&ui_event, bg_root) {
 480             changed = true;
 481         } else if self.ui_context.propagate_event(&ui_event, sl_root) {
 482             changed = true;
 483         } else if self.ui_context.propagate_event(&ui_event, card_root) {
 484             changed = true;
 485         }
 486         changed
 487     }
 488     fn trigger_auth(&mut self) {
 489         let username = self.username_box.text.trim().to_string();
 490         let password = self.password_box.text.trim().to_string();
 491 
 492         if username.is_empty() {
 493             self.status_lbl.text = "Username cannot be empty".to_string();
 494             self.status_lbl.is_error = true;
 495             self.ui_context.set_focused(&mut self.username_box);
 496             self.username_box.focus();
 497         } else if password.is_empty() {
 498             if is_fprint_enabled() {
 499                 self.start_fprint_auth();
 500             } else {
 501                 self.status_lbl.text = "Password cannot be empty".to_string();
 502                 self.status_lbl.is_error = true;
 503                 self.ui_context.set_focused(&mut self.password_box);
 504                 self.password_box.focus();
 505             }
 506         } else {
 507             // A typed password supersedes any fingerprint attempt still
 508             // running; release the sensor so it is not left claimed.
 509             self.cancel_fprint_auth();
 510             self.auth_request_id += 1;
 511             self.status_lbl.text = "Authenticating...".to_string();
 512             self.status_lbl.is_error = false;
 513             self.is_authenticating = true;
 514             self.login_btn.base_mut().label = Some("Authenticating...".to_string());
 515             authenticate_user(self.auth_request_id, username, password, self.auth_sender.clone());
 516         }
 517     }
 518 
 519     /// Start (or restart) the fingerprint attempt for the username in the box.
 520     fn start_fprint_auth(&mut self) {
 521         let username = self.username_box.text.trim().to_string();
 522         self.cancel_fprint_auth();
 523         self.auth_request_id += 1;
 524         self.status_lbl.text = "Scan finger to login or type password".to_string();
 525         self.status_lbl.is_error = false;
 526         self.is_authenticating = true;
 527         self.login_btn.base_mut().label = Some("Authenticating...".to_string());
 528         match spawn_fprint_helper(self.auth_request_id, &username, self.auth_sender.clone()) {
 529             Ok(child) => self.fprint_child = Some(child),
 530             Err(e) => {
 531                 log::error!("Failed to spawn fingerprint helper: {}", e);
 532                 self.is_authenticating = false;
 533                 self.login_btn.base_mut().label = Some("Log In".to_string());
 534                 self.status_lbl.text = "Fingerprint unavailable — type password".to_string();
 535                 self.status_lbl.is_error = true;
 536             }
 537         }
 538     }
 539 
 540     /// Kill a running fingerprint helper, if any. Its exit drops the D-Bus
 541     /// connection pam_fprintd used to claim the sensor, so fprintd releases the
 542     /// device for the next attempt. Any late events it already queued are
 543     /// dropped by the request-id check in the auth event handler.
 544     fn cancel_fprint_auth(&mut self) {
 545         if let Some(mut child) = self.fprint_child.take() {
 546             let _ = child.kill();
 547             let _ = child.wait();
 548         }
 549     }
 550 }
 551 
 552 impl cce_ui::engine::Application for State {
 553     type Message = String;
 554 
 555     fn new(_qh: &QueueHandle<cce_ui::engine::EngineState<Self>>, _sender: channel::Sender<Self::Message>) -> Self {
 556         let (auth_sender, auth_receiver) = channel::channel::<AuthEvent>();
 557 
 558         // Prepopulate username from last_user file if it exists
 559         let last_user_path = "/var/lib/cce-display-manager/last_user";
 560         let current_user = if std::path::Path::new(last_user_path).exists() {
 561             std::fs::read_to_string(last_user_path)
 562                 .map(|s| s.trim().to_string())
 563                 .unwrap_or_else(|_| String::new())
 564         } else {
 565             let env_user = std::env::var("USER").unwrap_or_else(|_| String::new());
 566             if env_user == "root" || env_user == "cce-display-manager" {
 567                 String::new()
 568             } else {
 569                 env_user
 570             }
 571         };
 572 
 573         let sessions = discover_sessions();
 574         let last_session_path = "/var/lib/cce-display-manager/last_session";
 575         let last_session_exec = if std::path::Path::new(last_session_path).exists() {
 576             std::fs::read_to_string(last_session_path)
 577                 .map(|s| s.trim().to_string())
 578                 .unwrap_or_else(|_| String::new())
 579         } else {
 580             String::new()
 581         };
 582 
 583         let mut selected_idx = 0;
 584         if !last_session_exec.is_empty() {
 585             if let Some(pos) = sessions.iter().position(|s| s.exec == last_session_exec) {
 586                 selected_idx = pos;
 587             }
 588         }
 589 
 590         let bg = ContentBg::new();
 591         let card = LoginCard::new();
 592         let username_box = TextBox::new(current_user).with_label("USERNAME");
 593         let password_box = TextBox::new(String::new()).with_password(true).with_label("PASSWORD");
 594         let login_btn = Button::new(0.0, 0.0, 300.0, 36.0).with_label("Log In");
 595         let status_lbl = StatusLabel::new("Enter password to start".to_string());
 596         let mut session_list = SessionList::new(sessions);
 597         session_list.selected_idx = selected_idx;
 598 
 599         let mut app = Self {
 600             bg,
 601             card,
 602             username_box,
 603             password_box,
 604             login_btn,
 605             status_lbl,
 606             session_list,
 607             ui_context: cce_ui::context::UiContext::new(),
 608             cursor_x: 0.0,
 609             cursor_y: 0.0,
 610             width: 1024.0,
 611             height: 768.0,
 612             physical_width: 1024,
 613             physical_height: 768,
 614             scale: 1.0,
 615             login_success: false,
 616             is_authenticating: false,
 617             auth_request_id: 0,
 618             auth_sender,
 619             auth_receiver: Some(auth_receiver),
 620             fprint_child: None,
 621         };
 622 
 623         // The widget tree is NOT linked here: `app` is a stack local inside new(), so any
 624         // pointer registered now (tree registry, ui_context.focused_widget) dangles the
 625         // moment the State moves to its final address. relink_tree() registers the live
 626         // addresses at the top of every frame instead. Only the widgets' own focus FLAGS
 627         // (which move with the struct) are set here; relink_tree points focused_widget at
 628         // the flagged box.
 629         let has_username = !app.username_box.text.trim().to_string().is_empty();
 630         if has_username {
 631             app.password_box.focus();
 632         } else {
 633             app.username_box.focus();
 634         }
 635 
 636         // Start the background fingerprint attempt if the username is
 637         // prepopulated and fprintd is enabled — unless this greeter is a rapid
 638         // respawn of one that just did the same. The daemon relaunches the
 639         // greeter whenever it exits without AUTH_SUCCESS (crash, F5, Ctrl+C),
 640         // and every relaunch used to fire a fresh fingerprint attempt on its
 641         // own: three respawns in 90s were three attempts nobody asked for.
 642         // After a respawn the user starts it explicitly (Enter on an empty
 643         // password box).
 644         let username = app.username_box.text.trim().to_string();
 645         if !username.is_empty() && is_fprint_enabled() {
 646             if fprint_autostart_recently() {
 647                 log::info!("Greeter respawned within {}s of the last fingerprint auto-start; not auto-starting", FPRINT_AUTOSTART_COOLDOWN.as_secs());
 648                 app.status_lbl.text = "Press Enter to scan finger, or type password".to_string();
 649             } else {
 650                 mark_fprint_autostart();
 651                 app.start_fprint_auth();
 652             }
 653         }
 654 
 655         app
 656     }
 657 
 658     fn settings(&self) -> WindowSettings {
 659         WindowSettings {
 660             title: "CCE Display Manager".to_string(),
 661             app_id: "cce-display-manager".to_string(),
 662             width: 1024,
 663             height: 768,
 664             fullscreen: false,
 665             min_size: Some((1024, 768)),
 666         }
 667     }
 668 
 669     fn update(&mut self, _msg: Self::Message, _needs_rebuild: &mut bool, _exit: &mut bool) {}
 670 
 671     fn tick(&mut self, _dt: f32, _needs_rebuild: &mut bool) {}
 672 
 673     fn display_list(&mut self, size: LogicalSize, scale: f64) -> Option<cce_ui::scene::paint::DisplayList> {
 674         // Phase 6ah single paint path: the widget geometry (the legacy view_rounded_quads
 675         // then view() bodies, in the wrapper's order) and all text are this one list. The
 676         // card — the soft radial-glow blob with the circular clip disabled — stays in
 677         // custom_vertices, appended on top exactly as before (it is the escape-hatch layer,
 678         // not part of the display-list geometry).
 679         use cce_ui::scene::layout::Rect;
 680         self.relink_tree();
 681         if (self.width - size.width as f32).abs() > 0.001 || (self.height - size.height as f32).abs() > 0.001 || (self.scale - scale).abs() > 0.001 {
 682             self.width = size.width as f32;
 683             self.height = size.height as f32;
 684             self.physical_width = (size.width * scale as f32) as u32;
 685             self.physical_height = (size.height * scale as f32) as u32;
 686             self.scale = scale;
 687             self.apply_layout();
 688         }
 689 
 690         let mut pc = cce_ui::scene::paint::PaintCtx::new();
 691 
 692         for w in self.widgets_iter() {
 693             let is_card = w.base().id() == self.card.id();
 694             if is_card {
 695                 continue;
 696             }
 697             for (qx, qy, qw, qh, qr, qc, qcorners) in w.all_rounded_quads(&self.ui_context) {
 698                 let rect = Rect { x: qx, y: qy, width: qw, height: qh };
 699                 if qr > 0.1 {
 700                     pc.rounded_rect(rect, qr, qcorners, qc);
 701                 } else {
 702                     pc.quad(rect, qc);
 703                 }
 704             }
 705         }
 706 
 707         for w in self.widgets_iter() {
 708             let is_card = w.base().id() == self.card.id();
 709             if is_card {
 710                 continue;
 711             }
 712             for (qx, qy, qw, qh, qc) in w.all_quads(&self.ui_context) {
 713                 pc.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
 714             }
 715         }
 716 
 717         pc.text_with(
 718             "Press Tab to switch fields • Session selector: Click current session label".to_string(),
 719             20.0,
 720             self.height - 24.0,
 721             11.0,
 722             [0x60, 0x60, 0x6e],
 723             None,
 724             None,
 725         );
 726         pc.text_with(
 727             concat!("Built ", env!("CCE_BUILD_DATE")).to_string(),
 728             self.width - 130.0,
 729             self.height - 24.0,
 730             11.0,
 731             [0x60, 0x60, 0x6e],
 732             None,
 733             None,
 734         );
 735         // Widget text via the paint walk, over the TRUE roots (root Container dissolved,
 736         // Phase 6ax): the card descends into its input children via the walk; the session
 737         // list and bg are standalone leaves. Walking the flat widgets_iter would emit the
 738         // card's children twice (once via descent, once as standalone roots).
 739         cce_ui::scene::painter::append_widget_text(&self.ui_context, &self.bg, &mut pc);
 740         cce_ui::scene::painter::append_widget_text(&self.ui_context, &self.card, &mut pc);
 741         cce_ui::scene::painter::append_widget_text(&self.ui_context, &self.session_list, &mut pc);
 742 
 743         Some(pc.finish())
 744     }
 745 
 746     fn display_list_text(&self) -> bool {
 747         true
 748     }
 749 
 750     fn custom_vertices(&mut self, verts: &mut Vec<Vertex>, _size: LogicalSize, _scale: f64) {
 751         let sw = self.width;
 752         let sh = self.height;
 753 
 754         let mut card_verts = widget_vertices(&self.card, sw, sh);
 755         for v in &mut card_verts {
 756             v.clip_circle = [-999.0, 0.0, 0.0];
 757         }
 758         verts.extend(card_verts);
 759 
 760         for (qx, qy, qw, qh, qc) in self.card.all_quads(&self.ui_context) {
 761             let mut q_verts = quad_vertices(qx, qy, qw, qh, sw, sh, qc).to_vec();
 762             for v in &mut q_verts {
 763                 v.clip_circle = [-999.0, 0.0, 0.0];
 764             }
 765             verts.extend(q_verts);
 766         }
 767     }
 768 
 769     fn register_sources(&mut self, handle: &calloop::LoopHandle<'_, EngineState<Self>>) {
 770         if let Some(auth_receiver) = self.auth_receiver.take() {
 771             handle.insert_source(auth_receiver, |event, _metadata, engine_state| {
 772                 let app = engine_state.inner.as_mut().unwrap();
 773                 let mut redraw = false;
 774                 match event {
 775                     channel::Event::Msg(msg) => {
 776                         let ev_request_id = match &msg {
 777                             AuthEvent::Success { request_id, .. } => *request_id,
 778                             AuthEvent::Failure { request_id, .. } => *request_id,
 779                             AuthEvent::Info { request_id, .. } => *request_id,
 780                         };
 781 
 782                         if ev_request_id != app.auth_request_id {
 783                             return;
 784                         }
 785 
 786                         match msg {
 787                             AuthEvent::Success { username, .. } => {
 788                                 app.fprint_child = None;
 789                                 app.is_authenticating = false;
 790                                 app.login_btn.base_mut().label = Some("Log In".to_string());
 791                                 app.status_lbl.text = format!("Welcome, {}!", username);
 792                                 app.status_lbl.is_error = false;
 793                                 app.login_success = true;
 794                                 if let Some(session) = app.session_list.selected_session() {
 795                                     println!("AUTH_SUCCESS|{}|{}|{}|{}", app.username_box.text.trim(), session.exec, session.is_wayland, app.password_box.text.trim());
 796                                     std::process::exit(0);
 797                                 }
 798                             }
 799                             AuthEvent::Failure { err_msg, .. } => {
 800                                 let was_fprint = app.fprint_child.is_some();
 801                                 if let Some(mut child) = app.fprint_child.take() {
 802                                     let _ = child.wait();
 803                                 }
 804                                 app.is_authenticating = false;
 805                                 app.login_btn.base_mut().label = Some("Log In".to_string());
 806                                 app.status_lbl.text = if was_fprint {
 807                                     // Raw PAM codes ("AUTHINFO_UNAVAIL") told the
 808                                     // user nothing, least of all how to retry.
 809                                     format!("{} — press Enter to scan again, or type password", fprint_failure_text(&err_msg))
 810                                 } else {
 811                                     err_msg
 812                                 };
 813                                 app.status_lbl.is_error = true;
 814                                 app.password_box.text.clear();
 815                                 app.password_box.edit_buffer.clear();
 816                                 app.ui_context.set_focused(&mut app.password_box);
 817                                 app.password_box.focus();
 818                             }
 819                             AuthEvent::Info { msg, .. } => {
 820                                 app.status_lbl.text = msg;
 821                                 app.status_lbl.is_error = false;
 822                             }
 823                         }
 824                         redraw = true;
 825                     }
 826                     channel::Event::Closed => {}
 827                 }
 828                 if redraw {
 829                     engine_state.redraw = true;
 830                 }
 831             }).unwrap();
 832         }
 833     }
 834 
 835     fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
 836         let lx = pos.x as f32;
 837         let ly = pos.y as f32;
 838         self.cursor_x = lx;
 839         self.cursor_y = ly;
 840         if self.widgets_cursor_moved(lx, ly) {
 841             *needs_rebuild = true;
 842         }
 843     }
 844 
 845     fn handle_mouse_input(&mut self, button: MouseButton, state: ElementState, pos: LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message> {
 846         if self.is_authenticating {
 847             return None;
 848         }
 849         let lx = pos.x as f32;
 850         let ly = pos.y as f32;
 851         let mut changed = false;
 852         if self.widgets_mouse_input(button, state, lx, ly) {
 853             changed = true;
 854         }
 855         if button == MouseButton::Left && state == ElementState::Pressed {
 856             if self.login_btn.take_click() {
 857                 self.trigger_auth();
 858                 changed = true;
 859             }
 860         }
 861         if changed {
 862             *needs_rebuild = true;
 863         }
 864         None
 865     }
 866 
 867     fn handle_mouse_wheel(&mut self, _delta: &MouseScrollDelta, _pos: LogicalPosition, _needs_rebuild: &mut bool) {}
 868 
 869     fn handle_key_input(&mut self, event: &KeyEvent, needs_rebuild: &mut bool) -> Option<Self::Message> {
 870         let logical_key = &event.logical_key;
 871         let ctrl_pressed = event.ctrl;
 872 
 873         // Check for Ctrl+C to abort/exit back to TTY
 874         if ctrl_pressed && (logical_key == &Key::Character("c".to_string()) || logical_key == &Key::Character("C".to_string())) {
 875             log::error!("Ctrl+C pressed. Aborting greeter.");
 876             std::process::exit(130);
 877         }
 878 
 879         // Check for F5 to request daemon restart
 880         if logical_key == &Key::Named(NamedKey::F5) {
 881             log::info!("F5 pressed. Requesting daemon restart.");
 882             std::process::exit(135);
 883         }
 884 
 885         let is_ctrl_p = ctrl_pressed && (logical_key == &Key::Character("p".to_string()) || logical_key == &Key::Character("P".to_string()));
 886         let is_ctrl_n = ctrl_pressed && (logical_key == &Key::Character("n".to_string()) || logical_key == &Key::Character("N".to_string()));
 887 
 888         if event.state == ElementState::Pressed {
 889             // If we are currently in fingerprint authentication and the user starts typing a password,
 890             // cancel the fingerprint auth and let them type.
 891             if self.is_authenticating {
 892                 if self.password_box.text.is_empty() {
 893                     let is_typing = !ctrl_pressed && match logical_key {
 894                         Key::Character(_) | Key::Named(NamedKey::Backspace) | Key::Named(NamedKey::Delete) | Key::Named(NamedKey::Space) => true,
 895                         _ => false,
 896                     };
 897                     if is_typing {
 898                         self.cancel_fprint_auth();
 899                         self.auth_request_id += 1;
 900                         self.is_authenticating = false;
 901                         self.login_btn.base_mut().label = Some("Log In".to_string());
 902                         self.status_lbl.text = "Enter password to start".to_string();
 903                         self.status_lbl.is_error = false;
 904                     } else {
 905                         let is_nav = match logical_key {
 906                             Key::Named(NamedKey::ArrowUp) | Key::Named(NamedKey::ArrowDown) | Key::Named(NamedKey::Tab) => true,
 907                             _ => is_ctrl_p || is_ctrl_n,
 908                         };
 909                         if !is_nav {
 910                             return None;
 911                         }
 912                     }
 913                 } else {
 914                     return None;
 915                 }
 916             }
 917 
 918             let mut changed = false;
 919 
 920             // Handle Up/Down or Ctrl+P/N navigation to cycle sessions
 921             let cycle_up = (logical_key == &Key::Named(NamedKey::ArrowUp) || is_ctrl_p) && !self.session_list.sessions.is_empty();
 922             let cycle_down = (logical_key == &Key::Named(NamedKey::ArrowDown) || is_ctrl_n) && !self.session_list.sessions.is_empty();
 923 
 924             if cycle_up {
 925                 let len = self.session_list.sessions.len();
 926                 self.session_list.selected_idx = (self.session_list.selected_idx + len - 1) % len;
 927                 self.session_list.hovered_idx = None;
 928                 changed = true;
 929             } else if cycle_down {
 930                 let len = self.session_list.sessions.len();
 931                 self.session_list.selected_idx = (self.session_list.selected_idx + 1) % len;
 932                 self.session_list.hovered_idx = None;
 933                 changed = true;
 934             } else if logical_key == &Key::Named(NamedKey::Tab) {
 935                 let is_user_focused = self.username_box.focused(&self.ui_context);
 936                 if is_user_focused {
 937                     self.ui_context.set_focused(&mut self.password_box);
 938                     self.username_box.unfocus();
 939                     self.password_box.focus();
 940                 } else {
 941                     self.ui_context.set_focused(&mut self.username_box);
 942                     self.password_box.unfocus();
 943                     self.username_box.focus();
 944                 }
 945                 changed = true;
 946             } else if logical_key == &Key::Named(NamedKey::Enter) && self.password_box.focused(&self.ui_context) {
 947                 let kev = cce_ui::widget::Event::KeyInput(event.clone());
 948                 let root = self.password_box.id();
 949                 let _ = self.ui_context.propagate_event(&kev, root);
 950                 self.trigger_auth();
 951                 changed = true;
 952             } else if logical_key == &Key::Named(NamedKey::Enter) && self.username_box.focused(&self.ui_context) {
 953                 let kev = cce_ui::widget::Event::KeyInput(event.clone());
 954                 let root = self.username_box.id();
 955                 let _ = self.ui_context.propagate_event(&kev, root);
 956                 self.ui_context.set_focused(&mut self.password_box);
 957                 self.username_box.unfocus();
 958                 self.password_box.focus();
 959                 changed = true;
 960             } else {
 961                 if self.widgets_keyboard_input(event) {
 962                     changed = true;
 963                 }
 964             }
 965 
 966             if changed {
 967                 *needs_rebuild = true;
 968             }
 969         }
 970 
 971         None
 972     }
 973 
 974     fn clear_color(&self) -> [f32; 4] {
 975         [0.03, 0.03, 0.05, 1.0]
 976     }
 977 }
 978 
 979 #[derive(Debug, Clone)]
 980 enum AuthEvent {
 981     Success { request_id: u64, username: String },
 982     Failure { request_id: u64, err_msg: String },
 983     Info { request_id: u64, msg: String },
 984 }
 985 
 986 /// PAM service for the fingerprint attempt. It must be fingerprint-ONLY
 987 /// (`auth requisite pam_fprintd.so`, no system-local-login include in the auth
 988 /// stack): the old layout had pam_fprintd `sufficient` above the include, so a
 989 /// miss fell through into pam_unix with an empty password — one pam_faillock
 990 /// strike per miss, and after three the correct password was rejected too.
 991 const FPRINT_PAM_SERVICE: &str = "cce-display-manager-fprint";
 992 const PASSWORD_PAM_SERVICE: &str = "cce-display-manager-password";
 993 
 994 /// A greeter that starts within this window of the previous auto-start is a
 995 /// respawn; it does not auto-start the fingerprint attempt again.
 996 const FPRINT_AUTOSTART_COOLDOWN: std::time::Duration = std::time::Duration::from_secs(20);
 997 const FPRINT_AUTOSTART_STAMP: &str = "/run/cce-display-manager/fprint-autostart";
 998 
 999 /// Human text for the verdict the fingerprint helper reports (a PamReturnCode
1000 /// Debug name, or a helper-level message).
1001 fn fprint_failure_text(code: &str) -> String {
1002     match code {
1003         // pam_fprintd: verify timed out, or the user has no enrolled prints.
1004         "AUTHINFO_UNAVAIL" => "No fingerprint read (timed out or none enrolled)".to_string(),
1005         "MAXTRIES" | "AUTH_ERR" => "Fingerprint not recognized".to_string(),
1006         "SERVICE_ERR" | "SYSTEM_ERR" => "Fingerprint reader unavailable".to_string(),
1007         other => format!("Fingerprint failed ({})", other),
1008     }
1009 }
1010 
1011 fn fprint_autostart_recently() -> bool {
1012     std::fs::metadata(FPRINT_AUTOSTART_STAMP)
1013         .and_then(|m| m.modified())
1014         .ok()
1015         .and_then(|t| std::time::SystemTime::now().duration_since(t).ok())
1016         .map(|age| age < FPRINT_AUTOSTART_COOLDOWN)
1017         .unwrap_or(false)
1018 }
1019 
1020 fn mark_fprint_autostart() {
1021     if let Some(dir) = std::path::Path::new(FPRINT_AUTOSTART_STAMP).parent() {
1022         let _ = std::fs::create_dir_all(dir);
1023     }
1024     if let Err(e) = std::fs::write(FPRINT_AUTOSTART_STAMP, b"") {
1025         log::warn!("Could not write {}: {}", FPRINT_AUTOSTART_STAMP, e);
1026     }
1027 }
1028 
1029 fn is_fprint_enabled() -> bool {
1030     std::fs::read_to_string(format!("/etc/pam.d/{}", FPRINT_PAM_SERVICE))
1031         .map(|content| {
1032             content.lines().any(|line| {
1033                 let trimmed = line.trim();
1034                 trimmed.contains("pam_fprintd.so") && !trimmed.starts_with('#')
1035             })
1036         })
1037         .unwrap_or(false)
1038 }
1039 
1040 /// Password authentication, in a thread. Fingerprint goes through
1041 /// `spawn_fprint_helper` instead — never call this with an empty password.
1042 fn authenticate_user(request_id: u64, username: String, password: String, sender: channel::Sender<AuthEvent>) {
1043     debug_assert!(!password.is_empty(), "empty password must go through the fingerprint helper");
1044     std::thread::spawn(move || {
1045         let service = PASSWORD_PAM_SERVICE;
1046 
1047         let mut auth = match PamSession::new(service, &username, &password, request_id, Some(sender.clone())) {
1048             Ok(a) => a,
1049             Err(e) => {
1050                 let _ = sender.send(AuthEvent::Failure { request_id, err_msg: format!("{:?}", e) });
1051                 return;
1052             }
1053         };
1054 
1055         if let Err(e) = auth.authenticate() {
1056             let _ = sender.send(AuthEvent::Failure { request_id, err_msg: format!("{:?}", e) });
1057             return;
1058         }
1059 
1060         if let Err(e) = auth.open_session() {
1061             let _ = sender.send(AuthEvent::Failure { request_id, err_msg: format!("{:?}", e) });
1062             return;
1063         }
1064 
1065         let _ = sender.send(AuthEvent::Success { request_id, username });
1066     });
1067 }
1068 
1069 /// Line protocol between the greeter and its `--fprint-auth` helper (on the
1070 /// helper's stdout — which is a pipe to the greeter, NOT the greeter's own
1071 /// stdout, which carries AUTH_SUCCESS to the daemon).
1072 const FPRINT_LINE_INFO: &str = "INFO|";
1073 const FPRINT_LINE_OK: &str = "OK";
1074 const FPRINT_LINE_FAIL: &str = "FAIL|";
1075 
1076 /// Spawn `<self> --fprint-auth <user>` and forward its result lines as
1077 /// AuthEvents tagged with `request_id`. The helper is bound to the greeter with
1078 /// PR_SET_PDEATHSIG so a crashed or respawned greeter cannot leave it running
1079 /// with the sensor claimed (that "Device was already claimed" state made every
1080 /// later attempt fail instantly).
1081 fn spawn_fprint_helper(request_id: u64, username: &str, sender: channel::Sender<AuthEvent>) -> std::io::Result<std::process::Child> {
1082     use std::os::unix::process::CommandExt;
1083     let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("/usr/bin/cce-display-manager"));
1084     let mut cmd = std::process::Command::new(exe);
1085     cmd.arg("--fprint-auth")
1086         .arg(username)
1087         .stdin(std::process::Stdio::null())
1088         .stdout(std::process::Stdio::piped())
1089         .stderr(std::process::Stdio::inherit());
1090     unsafe {
1091         cmd.pre_exec(|| {
1092             // Runs in the child between fork and exec; PDEATHSIG survives exec.
1093             if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) != 0 {
1094                 return Err(std::io::Error::last_os_error());
1095             }
1096             // Parent already gone (raced between fork and prctl)? Then die now.
1097             if libc::getppid() == 1 {
1098                 libc::_exit(1);
1099             }
1100             Ok(())
1101         });
1102     }
1103     let mut child = cmd.spawn()?;
1104     let stdout = child.stdout.take().expect("piped stdout");
1105     let username = username.to_string();
1106     std::thread::spawn(move || {
1107         use std::io::BufRead;
1108         let mut concluded = false;
1109         for line in std::io::BufReader::new(stdout).lines() {
1110             let line = match line { Ok(l) => l, Err(_) => break };
1111             if let Some(msg) = line.strip_prefix(FPRINT_LINE_INFO) {
1112                 let _ = sender.send(AuthEvent::Info { request_id, msg: msg.to_string() });
1113             } else if line == FPRINT_LINE_OK {
1114                 concluded = true;
1115                 let _ = sender.send(AuthEvent::Success { request_id, username: username.clone() });
1116             } else if let Some(msg) = line.strip_prefix(FPRINT_LINE_FAIL) {
1117                 concluded = true;
1118                 let _ = sender.send(AuthEvent::Failure { request_id, err_msg: msg.to_string() });
1119             }
1120         }
1121         if !concluded {
1122             // EOF without a verdict: killed (cancelled) or crashed. A cancel
1123             // has already bumped auth_request_id, so this is dropped there.
1124             let _ = sender.send(AuthEvent::Failure { request_id, err_msg: "Fingerprint helper exited".to_string() });
1125         }
1126     });
1127     Ok(child)
1128 }
1129 
1130 /// `--fprint-auth <user>`: run the fingerprint-only PAM service to a verdict
1131 /// and report it on stdout. Authenticate + account check only — the greeter
1132 /// prints AUTH_SUCCESS with an empty password and the daemon opens the real
1133 /// session on cce-display-manager-autologin, so opening one here would just
1134 /// register a throwaway logind session under cage.
1135 fn run_fprint_helper(username: &str) -> ! {
1136     use std::io::Write;
1137     let (sender, receiver) = channel::channel::<AuthEvent>();
1138     let user = username.to_string();
1139     let worker = std::thread::spawn(move || {
1140         let mut auth = PamSession::new(FPRINT_PAM_SERVICE, &user, "", 0, Some(sender.clone()))
1141             .map_err(|e| format!("{:?}", e))?;
1142         auth.authenticate().map_err(|e| format!("{:?}", e))
1143     });
1144     let mut out = std::io::stdout();
1145     // Forward conversation messages (e.g. "Place your finger on the sensor")
1146     // until the worker's sender is dropped, i.e. the verdict is in.
1147     while let Ok(ev) = receiver.recv() {
1148         if let AuthEvent::Info { msg, .. } = ev {
1149             let _ = writeln!(out, "{}{}", FPRINT_LINE_INFO, msg.replace('\n', " "));
1150             let _ = out.flush();
1151         }
1152     }
1153     let verdict = match worker.join() {
1154         Ok(Ok(())) => FPRINT_LINE_OK.to_string(),
1155         Ok(Err(e)) => format!("{}{}", FPRINT_LINE_FAIL, e),
1156         Err(_) => format!("{}fingerprint worker panicked", FPRINT_LINE_FAIL),
1157     };
1158     let _ = writeln!(out, "{}", verdict);
1159     let _ = out.flush();
1160     std::process::exit(if verdict == FPRINT_LINE_OK { 0 } else { 1 });
1161 }
1162 
1163 #[derive(serde::Deserialize, Debug, Default)]
1164 struct SystemConfig {
1165     scale: Option<f64>,
1166 }
1167 
1168 fn load_system_config() -> SystemConfig {
1169     let path = "/etc/cce/cce.json";
1170     if std::path::Path::new(path).exists() {
1171         if let Ok(content) = std::fs::read_to_string(path) {
1172             if let Ok(config) = serde_json::from_str(&content) {
1173                 return config;
1174             }
1175         }
1176     }
1177     SystemConfig::default()
1178 }
1179 
1180 fn run_greeter() {
1181     let sys_config = load_system_config();
1182     let layout_scale = sys_config.scale.unwrap_or(1.0);
1183     let cursor_size = (24.0 * layout_scale) as u32;
1184     std::env::set_var("XCURSOR_SIZE", cursor_size.to_string());
1185     // cage reports a scale-1 output, so on a HiDPI panel the greeter would lay
1186     // out in physical pixels (everything half-size). cce-ui's forced-scale mode
1187     // scales layout/rendering by the system scale while keeping buffer_scale 1.
1188     if layout_scale > 1.0 && std::env::var("CCE_FORCE_SCALE").is_err() {
1189         std::env::set_var("CCE_FORCE_SCALE", layout_scale.to_string());
1190     }
1191 
1192     cce_ui::engine::run::<State>();
1193 
1194     std::process::exit(1);
1195 }
1196 
1197 struct PamSessionData {
1198     username: String,
1199     password: String,
1200     request_id: u64,
1201     sender: Option<channel::Sender<AuthEvent>>,
1202 }
1203 
1204 extern "C" fn pam_conversation_fn(
1205     num_msg: libc::c_int,
1206     msg: *mut *mut pam_sys::PamMessage,
1207     out_resp: *mut *mut pam_sys::PamResponse,
1208     appdata_ptr: *mut libc::c_void,
1209 ) -> libc::c_int {
1210     let data = unsafe { &*(appdata_ptr as *const PamSessionData) };
1211     let resp_size = std::mem::size_of::<pam_sys::PamResponse>();
1212     let resp = unsafe { libc::calloc(num_msg as usize, resp_size) as *mut pam_sys::PamResponse };
1213     if resp.is_null() {
1214         return pam_sys::PamReturnCode::BUF_ERR as libc::c_int;
1215     }
1216 
1217     for i in 0..num_msg as isize {
1218         unsafe {
1219             let m = &**msg.offset(i);
1220             let r = &mut *resp.offset(i);
1221             let style = m.msg_style;
1222             // unwrap_or_default, not unwrap: an interior NUL in the typed
1223             // password would otherwise panic across this extern "C" boundary
1224             // (process abort). An empty response just fails authentication.
1225             if style == pam_sys::PamMessageStyle::PROMPT_ECHO_ON as libc::c_int {
1226                 let user_c = std::ffi::CString::new(data.username.clone()).unwrap_or_default();
1227                 r.resp = libc::strdup(user_c.as_ptr());
1228             } else if style == pam_sys::PamMessageStyle::PROMPT_ECHO_OFF as libc::c_int {
1229                 let pass_c = std::ffi::CString::new(data.password.clone()).unwrap_or_default();
1230                 r.resp = libc::strdup(pass_c.as_ptr());
1231             } else if style == pam_sys::PamMessageStyle::ERROR_MSG as libc::c_int || style == pam_sys::PamMessageStyle::TEXT_INFO as libc::c_int {
1232                 if !m.msg.is_null() {
1233                     let msg_str = std::ffi::CStr::from_ptr(m.msg).to_string_lossy().into_owned();
1234                     if let Some(ref sender) = data.sender {
1235                         let _ = sender.send(AuthEvent::Info { request_id: data.request_id, msg: msg_str });
1236                     }
1237                 }
1238             }
1239         }
1240     }
1241 
1242     unsafe { *out_resp = resp };
1243     pam_sys::PamReturnCode::SUCCESS as libc::c_int
1244 }
1245 
1246 struct PamSession {
1247     handle: *mut pam_sys::PamHandle,
1248     _data: Box<PamSessionData>,
1249     has_open_session: bool,
1250 }
1251 
1252 impl PamSession {
1253     fn new(service: &str, username: &str, password: &str, request_id: u64, sender: Option<channel::Sender<AuthEvent>>) -> Result<Self, pam_sys::PamReturnCode> {
1254         let mut handle: *mut pam_sys::PamHandle = std::ptr::null_mut();
1255         let data = Box::new(PamSessionData {
1256             username: username.to_string(),
1257             password: password.to_string(),
1258             request_id,
1259             sender,
1260         });
1261         
1262         let conv = pam_sys::PamConversation {
1263             conv: Some(pam_conversation_fn),
1264             data_ptr: &*data as *const PamSessionData as *mut libc::c_void,
1265         };
1266 
1267         let rc = pam_sys::start(service, Some(username), &conv, &mut handle);
1268         if rc != pam_sys::PamReturnCode::SUCCESS {
1269             return Err(rc);
1270         }
1271 
1272         unsafe {
1273             let pass_c = std::ffi::CString::new(password).unwrap_or_default();
1274             let _ = pam_sys::raw::pam_set_item(handle, pam_sys::PamItemType::AUTHTOK as libc::c_int, pass_c.as_ptr() as *const libc::c_void);
1275             
1276             let raw_tty = std::fs::read_link("/proc/self/fd/0")
1277                 .ok()
1278                 .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
1279                 .unwrap_or_else(|| "tty1".to_string());
1280             let is_real_tty = raw_tty.starts_with("tty");
1281             let tty_name = if is_real_tty { raw_tty } else { "tty1".to_string() };
1282 
1283             let tty_c = std::ffi::CString::new(tty_name).unwrap();
1284             let _ = pam_sys::raw::pam_set_item(handle, pam_sys::PamItemType::TTY as libc::c_int, tty_c.as_ptr() as *const libc::c_void);
1285         }
1286 
1287         Ok(Self { handle, _data: data, has_open_session: false })
1288     }
1289 
1290     fn putenv(&mut self, name_value: &str) -> Result<(), pam_sys::PamReturnCode> {
1291         let c_str = std::ffi::CString::new(name_value).unwrap();
1292         let rc = unsafe { pam_sys::raw::pam_putenv(self.handle, c_str.as_ptr()) };
1293         if rc == 0 {
1294             Ok(())
1295         } else {
1296             Err(unsafe { std::mem::transmute(rc as u8) })
1297         }
1298     }
1299 
1300     fn authenticate(&mut self) -> Result<(), pam_sys::PamReturnCode> {
1301         unsafe {
1302             let rc = pam_sys::authenticate(&mut *self.handle, pam_sys::PamFlag::NONE);
1303             if rc != pam_sys::PamReturnCode::SUCCESS {
1304                 return Err(rc);
1305             }
1306 
1307             let rc = pam_sys::acct_mgmt(&mut *self.handle, pam_sys::PamFlag::NONE);
1308             if rc != pam_sys::PamReturnCode::SUCCESS {
1309                 return Err(rc);
1310             }
1311         }
1312         Ok(())
1313     }
1314 
1315     fn open_session(&mut self) -> Result<(), pam_sys::PamReturnCode> {
1316         unsafe {
1317             let rc = pam_sys::setcred(&mut *self.handle, pam_sys::PamFlag::ESTABLISH_CRED);
1318             if rc != pam_sys::PamReturnCode::SUCCESS {
1319                 return Err(rc);
1320             }
1321 
1322             let rc = pam_sys::open_session(&mut *self.handle, pam_sys::PamFlag::NONE);
1323             if rc != pam_sys::PamReturnCode::SUCCESS {
1324                 return Err(rc);
1325             }
1326 
1327             // Follow openSSH and call pam_setcred before and after open_session
1328             let rc = pam_sys::setcred(&mut *self.handle, pam_sys::PamFlag::REINITIALIZE_CRED);
1329             if rc != pam_sys::PamReturnCode::SUCCESS {
1330                 return Err(rc);
1331             }
1332         }
1333         self.has_open_session = true;
1334         Ok(())
1335     }
1336 
1337     fn get_env(&mut self) -> Vec<(String, String)> {
1338         let mut vec = Vec::new();
1339         unsafe {
1340             let env_list = pam_sys::getenvlist(&mut *self.handle);
1341             if !env_list.is_null() {
1342                 let mut idx = 0;
1343                 loop {
1344                     let env_ptr = *env_list.offset(idx);
1345                     if !env_ptr.is_null() {
1346                         idx += 1;
1347                         let env_str = std::ffi::CStr::from_ptr(env_ptr).to_string_lossy();
1348                         let split: Vec<_> = env_str.splitn(2, '=').collect();
1349                         if split.len() == 2 {
1350                             vec.push((split[0].to_string(), split[1].to_string()));
1351                         }
1352                     } else {
1353                         break;
1354                     }
1355                 }
1356                 pam_sys::raw::pam_misc_drop_env(env_list as *mut *mut libc::c_char);
1357             }
1358         }
1359         vec
1360     }
1361 }
1362 
1363 impl Drop for PamSession {
1364     fn drop(&mut self) {
1365         unsafe {
1366             if self.has_open_session {
1367                 pam_sys::close_session(&mut *self.handle, pam_sys::PamFlag::NONE);
1368             }
1369             let rc = pam_sys::setcred(&mut *self.handle, pam_sys::PamFlag::DELETE_CRED);
1370             pam_sys::end(&mut *self.handle, rc);
1371         }
1372     }
1373 }
1374 
1375 /// PID of the greeter's `cage` process while a greeter is showing, else 0.
1376 /// Shared with the resume watchdog so it can force a clean greeter respawn
1377 /// after sleep without racing the daemon's blocking read of the greeter's
1378 /// stdout. Set right after the cage is spawned, cleared once it is reaped.
1379 static GREETER_CAGE_PID: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
1380 
1381 fn run_daemon() {
1382     let uid = users::get_current_uid();
1383     if uid != 0 {
1384         log::error!("Error: Daemon mode must be run as root (UID 0). Effective UID: {}", uid);
1385         log::info!("For local development/testing, run with: cargo run -- --greeter");
1386         std::process::exit(1);
1387     }
1388 
1389     let raw_tty = std::fs::read_link("/proc/self/fd/0")
1390         .ok()
1391         .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
1392         .unwrap_or_else(|| "tty1".to_string());
1393     let is_real_tty = raw_tty.starts_with("tty");
1394     let tty_name = if is_real_tty { raw_tty } else { "tty1".to_string() };
1395 
1396     // Redirect stdout and stderr of the daemon to a log file. /var/log, not
1397     // /tmp: only root can create names there, so a local user cannot pre-place
1398     // a file or symlink at the predictable path for root to open and truncate.
1399     let log_path = format!("/var/log/cce-display-manager-{}.log", tty_name);
1400     if let Ok(log_file) = std::fs::OpenOptions::new()
1401         .create(true)
1402         .write(true)
1403         .truncate(true)
1404         .open(&log_path)
1405     {
1406         use std::os::unix::io::AsRawFd;
1407         let fd = log_file.as_raw_fd();
1408         unsafe {
1409             libc::dup2(fd, 1);
1410             libc::dup2(fd, 2);
1411         }
1412     }
1413 
1414     log::info!("Starting display manager daemon on {}...", tty_name);
1415 
1416     let runtime_dir = format!("/run/cce-display-manager-{}", tty_name);
1417     if !std::path::Path::new(&runtime_dir).exists() {
1418         std::fs::create_dir_all(&runtime_dir).expect("failed to create runtime dir");
1419         use std::os::unix::fs::PermissionsExt;
1420         std::fs::set_permissions(&runtime_dir, std::fs::Permissions::from_mode(0o700))
1421             .expect("failed to set runtime dir permissions");
1422     }
1423     // Set when the compositor requested a restart (`ccectl restart-compositor`
1424     // wrote the flag file and exited): the next loop iteration relaunches the
1425     // same session directly — no greeter, autologin PAM service.
1426     let mut pending_relaunch: Option<(String, String, bool)> = None;
1427 
1428     // Recover the greeter across suspend/resume: resume leaves the greeter's
1429     // cage DRM-paused and it cannot reliably reacquire the seat on its own.
1430     if is_real_tty {
1431         spawn_resume_watchdog(tty_name.clone());
1432     }
1433 
1434     loop {
1435         if let Some((username, exec, is_wayland)) = pending_relaunch.take() {
1436             log::info!(
1437                 "Compositor restart requested: relaunching '{}' for {} without the greeter",
1438                 exec, username
1439             );
1440             launch_session(username, exec, is_wayland, String::new(), &tty_name, &mut pending_relaunch);
1441             continue;
1442         }
1443 
1444         if is_real_tty {
1445             // Actively claim tty1 rather than passively waiting for it: after a
1446             // resume (or any stray VT switch) tty1 may not be foreground, and a
1447             // greeter cage spawned onto an inactive VT comes up DRM-paused.
1448             log::info!("Ensuring {} is the active TTY before spawning greeter...", tty_name);
1449             ensure_vt_active(&tty_name);
1450         }
1451 
1452         log::info!("Spawning greeter session via cage...");
1453  
1454         let mut exe_path = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("/usr/bin/cce-display-manager"));
1455         if !exe_path.exists() {
1456             exe_path = std::path::PathBuf::from("/usr/bin/cce-display-manager");
1457         }
1458 
1459         let mut child = std::process::Command::new("cage")
1460             .arg("-s")
1461             .arg("--")
1462             .arg(exe_path)
1463             .arg("--greeter")
1464             .env("XDG_RUNTIME_DIR", &runtime_dir)
1465             // The greeter runs as root: cce-ui's default bundled-fonts dir
1466             // ($HOME/Dropbox/Fonts) doesn't exist for root, and an empty font
1467             // db panics on the first shaped glyph. Point it at a system
1468             // location and load installed system fonts as a fallback.
1469             .env("CCE_FONTS_DIR", "/usr/share/fonts/cce")
1470             .env("CCE_LOAD_SYSTEM_FONTS", "1")
1471             .env("LIBSEAT_BACKEND", "seatd")
1472             .env("WLR_DRM_NO_MODIFIERS", "1")
1473             .env("WLR_DRM_DEVICES", "/dev/dri/card1:/dev/dri/card0")
1474             .stdout(std::process::Stdio::piped())
1475             .spawn()
1476             .expect("failed to spawn cage compositor wrapper. Is cage installed?");
1477         GREETER_CAGE_PID.store(child.id() as i32, std::sync::atomic::Ordering::SeqCst);
1478 
1479         let stdout = child.stdout.take().expect("failed to open child stdout");
1480         let reader = std::io::BufReader::new(stdout);
1481         let mut auth_success = None;
1482 
1483         use std::io::BufRead;
1484         for line in reader.lines() {
1485             if let Ok(line_str) = line {
1486                 if line_str.starts_with("AUTH_SUCCESS|") {
1487                     if let Some((username, exec, is_wayland, password)) = parse_auth_success(&line_str) {
1488                         log::info!("[greeter-stdout] AUTH_SUCCESS|{}|{}|{}", username, exec, is_wayland);
1489                         auth_success = Some((username, exec, is_wayland, password));
1490                         // Don't read to EOF: the greeter has already exited,
1491                         // but cage can linger indefinitely after its child is
1492                         // gone (observed wedged until a manual VT switch — the
1493                         // "login hangs until Ctrl+Alt+F2" failure). Stop
1494                         // reading and terminate it ourselves below.
1495                         break;
1496                     }
1497                     // Never echo the raw line: field 5 is the password.
1498                     log::warn!("[greeter-stdout] malformed AUTH_SUCCESS line (redacted); login attempt dropped");
1499                 } else {
1500                     log::info!("[greeter-stdout] {}", line_str);
1501                 }
1502             }
1503         }
1504 
1505         if auth_success.is_some() {
1506             terminate_greeter(&mut child);
1507         }
1508         let status = child.wait().expect("failed to wait on child process");
1509         GREETER_CAGE_PID.store(0, std::sync::atomic::Ordering::SeqCst);
1510         log::info!("Greeter session exited with status: {}", status);
1511 
1512         if status.code() == Some(130) {
1513             log::info!("Abort requested via Ctrl+C. Exiting display manager daemon.");
1514             std::process::exit(0);
1515         }
1516 
1517         if status.code() == Some(135) {
1518             log::info!("Restart requested via F5. Re-executing daemon...");
1519             let mut exe_path = std::path::PathBuf::from("/usr/bin/cce-display-manager");
1520             if !exe_path.exists() {
1521                 exe_path = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("/usr/bin/cce-display-manager"));
1522             }
1523             let args: Vec<String> = std::env::args().collect();
1524             use std::os::unix::process::CommandExt;
1525             let mut cmd = std::process::Command::new(&exe_path);
1526             cmd.args(&args[1..]);
1527             let err = cmd.exec();
1528             log::error!("Failed to re-exec daemon: {:?}", err);
1529         }
1530 
1531         if auth_success.is_none() {
1532             // Sleep briefly to prevent high CPU usage if the greeter keeps crashing on startup
1533             std::thread::sleep(std::time::Duration::from_millis(1000));
1534         }
1535 
1536         if let Some((username, exec, is_wayland, password)) = auth_success {
1537             // Write last logged-in user and session to persistent files
1538             let var_lib = "/var/lib/cce-display-manager";
1539             if let Err(e) = std::fs::create_dir_all(var_lib) {
1540                 log::error!("Failed to create var lib dir: {:?}", e);
1541             } else {
1542                 if let Err(e) = std::fs::write(format!("{}/last_user", var_lib), &username) {
1543                     log::error!("Failed to write last_user file: {:?}", e);
1544                 }
1545                 if let Err(e) = std::fs::write(format!("{}/last_session", var_lib), &exec) {
1546                     log::error!("Failed to write last_session file: {:?}", e);
1547                 }
1548             }
1549 
1550             launch_session(username, exec, is_wayland, password, &tty_name, &mut pending_relaunch);
1551         }
1552     }
1553 }
1554 
1555 /// Ask the greeter's cage to exit, escalating to SIGKILL if it doesn't. Cage
1556 /// exiting cleanly releases the seat/VT via seatd (which cleans the VT up
1557 /// without switching away); a wedged cage would otherwise block the login
1558 /// handoff forever.
1559 fn terminate_greeter(child: &mut std::process::Child) {
1560     unsafe {
1561         libc::kill(child.id() as libc::pid_t, libc::SIGTERM);
1562     }
1563     for _ in 0..30 {
1564         match child.try_wait() {
1565             Ok(Some(_)) | Err(_) => return,
1566             Ok(None) => std::thread::sleep(std::time::Duration::from_millis(100)),
1567         }
1568     }
1569     log::warn!("cage did not exit within 3s of SIGTERM; killing it");
1570     let _ = child.kill();
1571 }
1572 
1573 /// Per-message state machine over `busctl monitor` text output, detecting a
1574 /// logind resume: `PrepareForSleep(false)`. Each D-Bus message opens with a
1575 /// `Type=` header line (which resets us), a signal's header also carries
1576 /// `Member=...` (we arm only for `PrepareForSleep`), and the body carries the
1577 /// `BOOLEAN` payload. `PrepareForSleep(true)` precedes suspend and `(false)`
1578 /// follows resume, so we fire only on the `false`. Fail-safe: a format we do
1579 /// not recognise simply never fires (degrading to the pre-fix behaviour, never
1580 /// a spurious teardown).
1581 struct ResumeSignalParser {
1582     armed: bool,
1583 }
1584 
1585 impl ResumeSignalParser {
1586     fn new() -> Self {
1587         Self { armed: false }
1588     }
1589 
1590     /// Feed one output line; returns true exactly when a resume message completes.
1591     fn feed(&mut self, line: &str) -> bool {
1592         if line.contains("Type=") {
1593             self.armed = false;
1594         }
1595         if line.contains("PrepareForSleep") {
1596             self.armed = true;
1597         } else if self.armed && line.contains("BOOLEAN") {
1598             let resume = line.contains("false");
1599             self.armed = false;
1600             return resume;
1601         }
1602         false
1603     }
1604 }
1605 
1606 /// Watch logind's `PrepareForSleep` signal and, on resume, recover the greeter.
1607 /// Resume-from-suspend leaves the greeter's `cage` DRM-paused ("Atomic commit
1608 /// failed: Permission denied" looping on "Disabling seat"); it cannot reliably
1609 /// reacquire the seat on its own, so on resume we force the greeter's VT active
1610 /// and tear the cage down, letting the daemon loop spawn a fresh one on an
1611 /// active VT -- the same known-good state a service restart produces. This is a
1612 /// no-op while a user session is live (`GREETER_CAGE_PID == 0`): the running
1613 /// compositor owns the seat then, and we must not fight it. Uses `busctl`
1614 /// (always present with systemd) rather than a D-Bus crate to keep this
1615 /// login-critical binary's dependency surface minimal.
1616 fn spawn_resume_watchdog(tty_name: String) {
1617     std::thread::spawn(move || loop {
1618         let spawned = std::process::Command::new("busctl")
1619             .args(["monitor", "--system", "org.freedesktop.login1"])
1620             .stdout(std::process::Stdio::piped())
1621             .stderr(std::process::Stdio::null())
1622             .spawn();
1623         let mut child = match spawned {
1624             Ok(c) => c,
1625             Err(e) => {
1626                 log::warn!("resume watchdog: could not start busctl ({}); retrying in 5s", e);
1627                 std::thread::sleep(std::time::Duration::from_secs(5));
1628                 continue;
1629             }
1630         };
1631         if let Some(stdout) = child.stdout.take() {
1632             use std::io::BufRead;
1633             let reader = std::io::BufReader::new(stdout);
1634             let mut parser = ResumeSignalParser::new();
1635             for line in reader.lines() {
1636                 let Ok(line) = line else { break };
1637                 if parser.feed(&line) {
1638                     on_resume(&tty_name);
1639                 }
1640             }
1641         }
1642         let _ = child.wait();
1643         log::warn!("resume watchdog: busctl monitor exited; restarting in 2s");
1644         std::thread::sleep(std::time::Duration::from_secs(2));
1645     });
1646 }
1647 
1648 /// Force the greeter's VT active and, if a greeter `cage` is up, tear it down so
1649 /// the daemon loop respawns a clean one. See `spawn_resume_watchdog`. No-op when
1650 /// a user session owns the seat (`GREETER_CAGE_PID == 0`).
1651 fn on_resume(tty_name: &str) {
1652     use std::sync::atomic::Ordering;
1653     let pid = GREETER_CAGE_PID.load(Ordering::SeqCst);
1654     if pid <= 0 {
1655         return;
1656     }
1657     log::info!("Resume from sleep detected while greeter is up; forcing {} active and respawning greeter", tty_name);
1658     ensure_vt_active(tty_name);
1659     // SIGTERM first; a DRM-wedged cage can ignore it, so escalate to SIGKILL.
1660     // Re-check the PID before escalating so we never signal a cage the daemon
1661     // has already reaped and replaced with a fresh one.
1662     unsafe { libc::kill(pid, libc::SIGTERM); }
1663     for _ in 0..30 {
1664         if GREETER_CAGE_PID.load(Ordering::SeqCst) != pid {
1665             return;
1666         }
1667         std::thread::sleep(std::time::Duration::from_millis(100));
1668     }
1669     if GREETER_CAGE_PID.load(Ordering::SeqCst) == pid {
1670         log::warn!("resume watchdog: greeter cage {} did not exit on SIGTERM; killing", pid);
1671         unsafe { libc::kill(pid, libc::SIGKILL); }
1672     }
1673 }
1674 
1675 /// The greeter/cage teardown (or a stray VT switch) can leave the session's VT
1676 /// inactive; a logind session on an inactive VT never activates, so the
1677 /// compositor sits DRM-paused on a black screen. Force the VT active before
1678 /// handing the seat to the user session.
1679 fn ensure_vt_active(tty_name: &str) {
1680     let Some(vt) = tty_name.strip_prefix("tty").and_then(|s| s.parse::<u32>().ok()) else {
1681         return;
1682     };
1683     for _ in 0..20 {
1684         if let Ok(active) = std::fs::read_to_string("/sys/class/tty/tty0/active") {
1685             if active.trim() == tty_name {
1686                 return;
1687             }
1688         }
1689         let _ = std::process::Command::new("chvt").arg(vt.to_string()).status();
1690         std::thread::sleep(std::time::Duration::from_millis(100));
1691     }
1692     log::warn!("could not make {} the active VT", tty_name);
1693 }
1694 
1695 /// Fork the session worker (PAM open_session + user-session spawn) and wait
1696 /// for it — shared by the greeter login path and the compositor-restart
1697 /// relaunch path. If the session left a restart flag (`ccectl
1698 /// restart-compositor` writes it before a clean exit), arm
1699 /// `pending_relaunch` so the daemon loop relaunches this same session
1700 /// directly, greeter skipped (empty password → the autologin PAM service).
1701 fn launch_session(
1702     username: String,
1703     exec: String,
1704     is_wayland: bool,
1705     password: String,
1706     tty_name: &str,
1707     pending_relaunch: &mut Option<(String, String, bool)>,
1708 ) {
1709     use users::os::unix::UserExt;
1710     log::info!("Launching user session Exec: '{}' (Wayland: {}) for user: '{}'", exec, is_wayland, username);
1711     // A stale flag from a previous session must not trigger a phantom relaunch.
1712     let flag_path = format!("/tmp/cce-restart-requested-{}", username);
1713     let _ = std::fs::remove_file(&flag_path);
1714 
1715     ensure_vt_active(tty_name);
1716 
1717     let pid = unsafe { libc::fork() };
1718     if pid < 0 {
1719         log::error!("Fork failed: {}", std::io::Error::last_os_error());
1720         return;
1721     } else if pid == 0 {
1722                 // Child process: execute PAM session and spawn the compositor/user session
1723                 let user = match users::get_user_by_name(&username) {
1724                     Some(u) => u,
1725                     None => {
1726                         log::error!("Error: User '{}' not found in system.", username);
1727                         std::process::exit(1);
1728                     }
1729                 };
1730 
1731                 let user_uid = user.uid();
1732                 let user_gid = user.primary_group_id();
1733                 let home_dir = user.home_dir().to_path_buf();
1734                 let shell = user.shell().to_str().unwrap_or("/bin/bash").to_string();
1735 
1736                 let user_runtime_dir = format!("/run/user/{}", user_uid);
1737 
1738                 // Set environment variables in the session worker process before PAM open_session.
1739                 // This is crucial for pam_gnome_keyring.so / pam_kwallet5.so to run successfully.
1740                 std::env::set_var("USER", &username);
1741                 std::env::set_var("LOGNAME", &username);
1742                 std::env::set_var("HOME", home_dir.to_str().unwrap_or(""));
1743                 std::env::set_var("SHELL", &shell);
1744                 std::env::set_var("XDG_RUNTIME_DIR", &user_runtime_dir);
1745 
1746                 let service = if password.is_empty() {
1747                     "cce-display-manager-autologin"
1748                 } else {
1749                     "cce-display-manager-password"
1750                 };
1751                 let mut auth = match PamSession::new(service, &username, &password, 0, None) {
1752                     Ok(a) => a,
1753                     Err(_) => {
1754                         let fallback_service = if password.is_empty() {
1755                             "ly-autologin"
1756                         } else {
1757                             "login"
1758                         };
1759                         match PamSession::new(fallback_service, &username, &password, 0, None) {
1760                             Ok(a) => a,
1761                             Err(e) => {
1762                                 log::error!("PAM Init Error in child: {:?}", e);
1763                                 std::process::exit(1);
1764                             }
1765                         }
1766                     }
1767                 };
1768 
1769                 let session_type_env = if is_wayland {
1770                     "XDG_SESSION_TYPE=wayland"
1771                 } else {
1772                     "XDG_SESSION_TYPE=x11"
1773                 };
1774                 let _ = auth.putenv(session_type_env);
1775                 let _ = auth.putenv("XDG_SESSION_CLASS=user");
1776 
1777                 if let Err(e) = auth.authenticate() {
1778                     log::error!("PAM Authentication failed in child: {:?}", e);
1779                     std::process::exit(1);
1780                 }
1781 
1782                 if let Err(e) = auth.open_session() {
1783                     log::error!("PAM Session failed in child: {:?}", e);
1784                     std::process::exit(1);
1785                 }
1786 
1787                 let pam_env = auth.get_env();
1788                 log::info!("PAM Environment variables: {:?}", pam_env);
1789 
1790                 if let Some((_, session_id)) = pam_env.iter().find(|(k, _)| k == "XDG_SESSION_ID") {
1791                     log::info!("Explicitly activating logind session {} via loginctl...", session_id);
1792                     let _ = std::process::Command::new("loginctl")
1793                         .arg("activate")
1794                         .arg(session_id)
1795                         .status();
1796                 }
1797                 
1798                 let (cmd_bin, cmd_args): (String, Vec<String>) = if is_wayland {
1799                     sanitize_exec(&exec)
1800                 } else {
1801                     let (client_bin, client_args) = sanitize_exec(&exec);
1802                     let xinit_bin = "/usr/sbin/xinit".to_string();
1803                     let mut args = vec![client_bin];
1804                     args.extend(client_args);
1805                     args.push("--".to_string());
1806                     args.push("-keeptty".to_string());
1807                     (xinit_bin, args)
1808                 };
1809 
1810                 if cmd_bin.is_empty() {
1811                     log::error!("Error: Resolved execution command is empty.");
1812                     std::process::exit(1);
1813                 }
1814 
1815                 log::info!("Spawning session: {} with args {:?} for UID={}, GID={}", cmd_bin, cmd_args, user_uid, user_gid);
1816 
1817                 use std::os::unix::process::CommandExt;
1818                 let mut session_cmd = std::process::Command::new(&cmd_bin);
1819                 session_cmd
1820                     .args(&cmd_args)
1821                     .envs(pam_env)
1822                     .current_dir(&home_dir)
1823                     .env("USER", &username)
1824                     .env("LOGNAME", &username)
1825                     .env("HOME", home_dir.to_str().unwrap())
1826                     .env("SHELL", &shell)
1827                     .env("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")
1828                     .env("XDG_RUNTIME_DIR", &user_runtime_dir)
1829                     .env("XDG_SESSION_TYPE", if is_wayland { "wayland" } else { "x11" })
1830                     .env("XDG_SESSION_CLASS", "user")
1831                     .stdin(std::process::Stdio::inherit())
1832                     .stdout(std::process::Stdio::inherit())
1833                     .stderr(std::process::Stdio::inherit());
1834 
1835                 // Filter out sudo env vars so they don't leak into the user session
1836                 for key in &["SUDO_USER", "SUDO_UID", "SUDO_GID", "SUDO_COMMAND"] {
1837                     session_cmd.env_remove(key);
1838                 }
1839 
1840                 let username_c = std::ffi::CString::new(username.clone()).unwrap();
1841                 unsafe {
1842                     session_cmd.pre_exec(move || {
1843                         if libc::initgroups(username_c.as_ptr(), user_gid as libc::gid_t) != 0 {
1844                             return Err(std::io::Error::last_os_error());
1845                         }
1846                         if libc::setgid(user_gid as libc::gid_t) != 0 {
1847                             return Err(std::io::Error::last_os_error());
1848                         }
1849                         if libc::setuid(user_uid as libc::uid_t) != 0 {
1850                             return Err(std::io::Error::last_os_error());
1851                         }
1852                         Ok(())
1853                     });
1854                 }
1855 
1856                 match session_cmd.spawn() {
1857                     Ok(mut child_proc) => {
1858                         let _ = child_proc.wait();
1859                     }
1860                     Err(e) => {
1861                         log::error!("Failed to launch session: {}", e);
1862                     }
1863                 }
1864                 log::info!("User session ended.");
1865                 std::mem::drop(auth);
1866                 std::process::exit(0);
1867     } else {
1868         // Parent process: block until the session worker child terminates
1869         let mut status: libc::c_int = 0;
1870         unsafe {
1871             libc::waitpid(pid, &mut status, 0);
1872         }
1873         log::info!("Session worker child (PID {}) exited with status: {}", pid, status);
1874 
1875         // Compositor-requested restart: honor the flag only when it is a
1876         // regular file owned by the session user (anyone can create names in
1877         // /tmp). symlink_metadata, not metadata: a plain stat follows
1878         // symlinks, so another user's link pointing at any file the session
1879         // user owns would pass the owner check.
1880         if let Ok(meta) = std::fs::symlink_metadata(&flag_path) {
1881             use std::os::unix::fs::MetadataExt;
1882             let owner_ok = meta.file_type().is_file()
1883                 && users::get_user_by_name(&username)
1884                     .map_or(false, |u| u.uid() == meta.uid());
1885             let _ = std::fs::remove_file(&flag_path);
1886             if owner_ok {
1887                 *pending_relaunch = Some((username, exec, is_wayland));
1888                 return; // relaunching immediately — no VT switch back
1889             }
1890             log::warn!("Ignoring restart flag {} with wrong owner", flag_path);
1891         }
1892 
1893         if let Some(vt) = tty_name.strip_prefix("tty").and_then(|s| s.parse::<u32>().ok()) {
1894             log::info!("Switching back to VT {}...", vt);
1895             let _ = std::process::Command::new("chvt")
1896                 .arg(vt.to_string())
1897                 .status();
1898         }
1899     }
1900 }
1901 
1902 fn main() {
1903     if std::env::var("RUST_LOG").is_err() {
1904         std::env::set_var("RUST_LOG", "info");
1905     }
1906     env_logger::init();
1907     let args: Vec<String> = std::env::args().collect();
1908     if args.len() > 1 && args[1] == "--greeter" {
1909         run_greeter();
1910     } else if args.len() > 2 && args[1] == "--fprint-auth" {
1911         run_fprint_helper(&args[2]);
1912     } else {
1913         run_daemon();
1914     }
1915 }
1916 
1917 
1918 
1919 
1920 #[cfg(test)]
1921 mod tests {
1922     use super::parse_auth_success;
1923 
1924     #[test]
1925     fn auth_success_plain() {
1926         let got = parse_auth_success("AUTH_SUCCESS|lucas|startcce|true|hunter2");
1927         assert_eq!(
1928             got,
1929             Some(("lucas".into(), "startcce".into(), true, "hunter2".into()))
1930         );
1931     }
1932 
1933     #[test]
1934     fn auth_success_password_with_pipes() {
1935         // The password is the last field and may contain the separator.
1936         let got = parse_auth_success("AUTH_SUCCESS|lucas|startcce|true|a|b|c");
1937         assert_eq!(
1938             got,
1939             Some(("lucas".into(), "startcce".into(), true, "a|b|c".into()))
1940         );
1941     }
1942 
1943     #[test]
1944     fn auth_success_empty_password_fingerprint_path() {
1945         let got = parse_auth_success("AUTH_SUCCESS|lucas|startcce|true|");
1946         assert_eq!(
1947             got,
1948             Some(("lucas".into(), "startcce".into(), true, String::new()))
1949         );
1950     }
1951 
1952     #[test]
1953     fn auth_success_malformed() {
1954         assert_eq!(parse_auth_success("AUTH_SUCCESS|lucas|startcce"), None);
1955         assert_eq!(parse_auth_success("AUTH_SUCCESS|"), None);
1956         assert_eq!(parse_auth_success("NOT_A_THING|x|y|z|w"), None);
1957     }
1958 
1959     #[test]
1960     fn auth_success_bad_bool_defaults_wayland() {
1961         let got = parse_auth_success("AUTH_SUCCESS|lucas|startcce|banana|pw");
1962         assert_eq!(got.map(|t| t.2), Some(true));
1963     }
1964 }
1965 
1966 
1967 #[cfg(test)]
1968 mod resume_parser_tests {
1969     use super::ResumeSignalParser;
1970 
1971     // A representative PrepareForSleep signal as `busctl monitor` prints it.
1972     fn feed_all(lines: &[&str]) -> usize {
1973         let mut p = ResumeSignalParser::new();
1974         lines.iter().filter(|l| p.feed(l)).count()
1975     }
1976 
1977     #[test]
1978     fn detects_resume_false() {
1979         let msg = [
1980             "\u{2023} Type=signal  Endian=l  Flags=1  Version=1  Cookie=42",
1981             "  Sender=:1.3  Path=/org/freedesktop/login1  Interface=org.freedesktop.login1.Manager  Member=PrepareForSleep",
1982             "  MESSAGE \"b\" {",
1983             "          BOOLEAN false;",
1984             "  };",
1985         ];
1986         assert_eq!(feed_all(&msg), 1, "resume (false) must fire once");
1987     }
1988 
1989     #[test]
1990     fn ignores_suspend_true() {
1991         let msg = [
1992             "\u{2023} Type=signal  Endian=l  Flags=1  Version=1  Cookie=41",
1993             "  Sender=:1.3  Path=/org/freedesktop/login1  Interface=org.freedesktop.login1.Manager  Member=PrepareForSleep",
1994             "  MESSAGE \"b\" {",
1995             "          BOOLEAN true;",
1996             "  };",
1997         ];
1998         assert_eq!(feed_all(&msg), 0, "suspend (true) must not fire");
1999     }
2000 
2001     #[test]
2002     fn ignores_other_signal_with_boolean() {
2003         // A different signal carrying a BOOLEAN false must not be mistaken for
2004         // a resume: the Type= header resets us and there is no PrepareForSleep.
2005         let msg = [
2006             "\u{2023} Type=signal  Endian=l  Flags=1  Version=1  Cookie=99",
2007             "  Sender=:1.3  Path=/org/freedesktop/login1  Interface=org.freedesktop.login1.Manager  Member=SessionRemoved",
2008             "  MESSAGE \"b\" {",
2009             "          BOOLEAN false;",
2010             "  };",
2011         ];
2012         assert_eq!(feed_all(&msg), 0, "unrelated signal must not fire");
2013     }
2014 
2015     #[test]
2016     fn full_cycle_fires_once_on_resume() {
2017         // Suspend then resume, back to back: exactly one fire, on resume.
2018         let mut p = ResumeSignalParser::new();
2019         let stream = [
2020             "\u{2023} Type=signal  Cookie=1",
2021             "  Interface=org.freedesktop.login1.Manager  Member=PrepareForSleep",
2022             "          BOOLEAN true;",
2023             "\u{2023} Type=signal  Cookie=2",
2024             "  Interface=org.freedesktop.login1.Manager  Member=PrepareForSleep",
2025             "          BOOLEAN false;",
2026         ];
2027         let fires: usize = stream.iter().filter(|l| p.feed(l)).count();
2028         assert_eq!(fires, 1);
2029     }
2030 }