feat(carmen): modo greeter — mirada-compositor como DM

`mirada-compositor --greeter` arranca como gestor de login: lanza
mirada-greeter como proceso hijo, lee su stdout y, al recibir el
SessionTicket, muta de BodyMode::Greeter a BodyMode::Session sin
reiniciar el servidor Wayland — la «mutación atómica» del DM.

- BodyMode { Greeter, Session }: eje ortogonal a Brain (Embedded/Linked).
- modo greeter: sin atajos registrados, rechaza Spawn, sin autoarranque.
- traspaso (complete_greeter_handoff): registra los atajos y arranca la
  sesión — el comando del tiquet, o el autoarranque del usuario.
- privilegios: el compositor corre como root; spawn_command baja a
  setuid/setgid + grupos suplementarios del usuario autenticado.
- bandera ortogonal al backend (--greeter [--drm|--winit]); el tiquet
  llega por un canal calloop en DRM y por mpsc en winit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-22 00:06:59 +00:00
parent 634a43006a
commit 758f61f52a
6 changed files with 352 additions and 69 deletions
@@ -44,6 +44,7 @@ use smithay::backend::udev;
use smithay::input::keyboard::FilterResult;
use smithay::input::pointer::{AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent};
use smithay::output::OutputModeSource;
use smithay::reexports::calloop::channel::{channel as ticket_channel, Event as TicketEvent};
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay::reexports::calloop::{EventLoop, Interest, Mode as CalloopMode, PostAction};
@@ -56,9 +57,13 @@ use smithay::utils::{
DeviceFd, IsAlive, Logical, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER,
};
use brahman_auth::SessionTicket;
use mirada_brain::{BodyEvent, CtlReply, Keymap, Rect};
use crate::{combo_string, send_frames_surface_tree, App, Brain, ClientState, DragGrab, DragMode, Setup};
use crate::{
combo_string, send_frames_surface_tree, App, BodyMode, Brain, ClientState, DragGrab, DragMode,
Setup,
};
/// El `DrmCompositor` concreto para la salida (un solo GPU).
type Compositor =
@@ -604,8 +609,9 @@ impl DrmState {
}
}
/// Arranca el Cuerpo sobre DRM/KMS — fases 1, 2a y 2b.
pub fn run() -> Result<(), Box<dyn Error>> {
/// Arranca el Cuerpo sobre DRM/KMS — fases 1, 2a y 2b. Con `greeter`,
/// el compositor nace en modo DM: ver [`BodyMode`].
pub fn run(greeter: bool) -> Result<(), Box<dyn Error>> {
println!("mirada-compositor · backend DRM.");
println!("──────────────────────────────────────────────────");
@@ -733,7 +739,8 @@ pub fn run() -> Result<(), Box<dyn Error>> {
// 7 · El estado Wayland (Cerebro, teclado, keymap, control).
println!("[7/8] armando el estado Wayland …");
let Setup { mut display, mut app, keymap_path, keymap_watch, ctl } = crate::build_app()?;
let Setup { mut display, mut app, keymap_path, keymap_watch, ctl } =
crate::build_app(greeter)?;
// Con el renderer ya creado, anuncia dmabuf — sin esto las apps que
// pintan por GPU (GPUI, navegadores acelerados) no pueden conectarse.
crate::announce_dmabuf(&mut app, &display.handle(), &renderer);
@@ -762,14 +769,25 @@ pub fn run() -> Result<(), Box<dyn Error>> {
std::env::set_var("WAYLAND_DISPLAY", &socket_name);
println!(" escuchando en WAYLAND_DISPLAY={socket_name}");
// Autoarranque: los programas de `~/.config/mirada/autostart`.
crate::spawn_autostart();
// App de arranque: si `MIRADA_STARTUP` trae un comando, se lanza como
// hijo (hereda `WAYLAND_DISPLAY`) — cómodo para probar sin saltar de VT.
if let Ok(cmd) = std::env::var("MIRADA_STARTUP") {
crate::spawn_command(&cmd);
}
// Modo DM: lanza el greeter y recibe su tiquet por un canal de
// `calloop`. Modo normal: autoarranque + `MIRADA_STARTUP`.
let greeter_rx = if app.mode == BodyMode::Greeter {
let (tx, rx) = ticket_channel::<SessionTicket>();
crate::spawn_greeter(move |ticket| {
let _ = tx.send(ticket);
})?;
Some(rx)
} else {
// Autoarranque: los programas de `~/.config/mirada/autostart`.
crate::spawn_autostart(None);
// App de arranque: si `MIRADA_STARTUP` trae un comando, se lanza
// como hijo (hereda `WAYLAND_DISPLAY`) — cómodo para probar sin
// saltar de VT.
if let Ok(cmd) = std::env::var("MIRADA_STARTUP") {
crate::spawn_command(&cmd, None);
}
None
};
// 8 · El bucle `calloop`: VBlank, teclado, clientes y un timer.
println!("[8/8] montando el bucle de eventos …");
@@ -853,6 +871,18 @@ pub fn run() -> Result<(), Box<dyn Error>> {
})
.map_err(|e| format!("insert timer: {e}"))?;
// Tiquet del greeter (modo DM): al llegar, el traspaso a la sesión.
// El hilo lector del greeter despierta el bucle por este canal.
if let Some(rx) = greeter_rx {
handle
.insert_source(rx, |event, _, state: &mut DrmState| {
if let TicketEvent::Msg(ticket) = event {
state.app.complete_greeter_handoff(ticket);
}
})
.map_err(|e| format!("insert greeter: {e}"))?;
}
// Tope de tiempo opcional: `MIRADA_DRM_TIMEOUT=<segundos>` cierra el
// compositor solo (0 o sin definir = sin tope). El teclado ya
// funciona — `Super+Shift+e` o `Ctrl+C` son la salida normal.