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 }