From 758f61f52a35514669064a1aefd44f22f07a9e9c Mon Sep 17 00:00:00 2001 From: sergio Date: Fri, 22 May 2026 00:06:59 +0000 Subject: [PATCH] =?UTF-8?q?feat(carmen):=20modo=20greeter=20=E2=80=94=20mi?= =?UTF-8?q?rada-compositor=20como=20DM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- Cargo.lock | 2 + crates/apps/mirada-compositor/Cargo.toml | 2 + crates/apps/mirada-compositor/README.md | 32 ++ .../apps/mirada-compositor/src/drm_backend.rs | 54 ++- crates/apps/mirada-compositor/src/main.rs | 321 +++++++++++++++--- crates/apps/mirada-greeter/README.md | 10 +- 6 files changed, 352 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df4b287..2c439fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7827,9 +7827,11 @@ dependencies = [ name = "mirada-compositor" version = "0.1.0" dependencies = [ + "brahman-auth", "mirada-body", "mirada-brain", "mirada-link", + "nix 0.29.0", "smithay", ] diff --git a/crates/apps/mirada-compositor/Cargo.toml b/crates/apps/mirada-compositor/Cargo.toml index e7547a0..522de8b 100644 --- a/crates/apps/mirada-compositor/Cargo.toml +++ b/crates/apps/mirada-compositor/Cargo.toml @@ -16,4 +16,6 @@ path = "src/main.rs" mirada-brain = { path = "../../modules/mirada/mirada-brain" } mirada-body = { path = "../../modules/mirada/mirada-body" } mirada-link = { path = "../../modules/mirada/mirada-link" } +brahman-auth = { path = "../../protocol/brahman-auth" } +nix = { workspace = true } smithay = "0.7" diff --git a/crates/apps/mirada-compositor/README.md b/crates/apps/mirada-compositor/README.md index eaa1fcc..f38f757 100644 --- a/crates/apps/mirada-compositor/README.md +++ b/crates/apps/mirada-compositor/README.md @@ -18,6 +18,9 @@ Tiene **dos backends gráficos**: Sin argumentos elige solo: con `DISPLAY`/`WAYLAND_DISPLAY` → `winit`; sin ellos → `drm`. O fuérzalo: `mirada-compositor --winit` / `--drm`. +La bandera `--greeter` (ortogonal al backend) arranca el compositor como +gestor de login — ver **Modo greeter (DM)** más abajo. + ## Backends ### winit — anidado @@ -87,6 +90,35 @@ sólo probarlo: Dentro de la sesión, `Ctrl+Alt+F1…F12` salta a otra TTY y vuelve sin romper carmen. +## Modo greeter (DM) + +`mirada-compositor --greeter` arranca el compositor como **gestor de +login**: en vez de la sesión, compone el greeter (`mirada-greeter`), +que lanza como proceso hijo. El usuario teclea sus credenciales; cuando +el login es válido el greeter emite un `SessionTicket` por su stdout y +el compositor **muta a modo sesión sin reiniciar el servidor Wayland** +— el mismo proceso, la misma GPU, las mismas ventanas («mutación +atómica»). Desde ahí baja privilegios al usuario autenticado +(`setuid`/`setgid` + grupos) para todo lo que lanza. + +La bandera es ortogonal al backend: `--greeter` solo (auto), o +`--greeter --drm` / `--greeter --winit`. + +```sh +# DM real, sobre una TTY — el compositor corre como root: PAM lo exige +sudo mirada-compositor --greeter --drm + +# iterar el greeter anidado, con credenciales de prueba +MIRADA_GREETER_MOCK=demo:demo \ + cargo run -p mirada-compositor -- --greeter --winit +``` + +En modo greeter no se registran atajos (todas las teclas van al +greeter — que el usuario no pueda lanzar nada ni cerrar el compositor), +se rechaza `spawn:` y no corre el autoarranque; los atajos y la sesión +arrancan sólo tras el traspaso. `MIRADA_GREETER_BIN` apunta a otro +binario de greeter (cómodo para señalar a `target/…` en desarrollo). + ## Lanzador de aplicaciones `mirada-launcher` escanea los `.desktop` del sistema y lanza el que diff --git a/crates/apps/mirada-compositor/src/drm_backend.rs b/crates/apps/mirada-compositor/src/drm_backend.rs index 4953de9..5be079f 100644 --- a/crates/apps/mirada-compositor/src/drm_backend.rs +++ b/crates/apps/mirada-compositor/src/drm_backend.rs @@ -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> { +/// 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> { println!("mirada-compositor · backend DRM."); println!("──────────────────────────────────────────────────"); @@ -733,7 +739,8 @@ pub fn run() -> Result<(), Box> { // 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> { 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::(); + 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> { }) .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=` cierra el // compositor solo (0 o sin definir = sin tope). El teclado ya // funciona — `Super+Shift+e` o `Ctrl+C` son la salida normal. diff --git a/crates/apps/mirada-compositor/src/main.rs b/crates/apps/mirada-compositor/src/main.rs index 4bb4602..02af6d1 100644 --- a/crates/apps/mirada-compositor/src/main.rs +++ b/crates/apps/mirada-compositor/src/main.rs @@ -69,6 +69,7 @@ use smithay::{ delegate_shm, delegate_xdg_decoration, delegate_xdg_shell, }; +use brahman_auth::{SessionTicket, UserInfo}; use mirada_body::{BodyOp, BodyState}; use mirada_brain::{ BodyEvent, BrainCommand, CtlReply, CtlRequest, CtlServer, Desktop, Keymap, Rules, @@ -89,6 +90,21 @@ enum Brain { Linked(BodyLink), } +/// La fase del ciclo de vida del Cuerpo. Es un eje **ortogonal** a +/// [`Brain`]: `Brain` dice de dónde sale la geometría; `BodyMode` dice +/// si el compositor está pidiendo credenciales o sirviendo una sesión. +/// Un arranque normal nace ya en [`BodyMode::Session`]; un arranque de +/// DM (`--greeter`) nace en [`BodyMode::Greeter`] y muta una sola vez, +/// al recibir el tiquet de un login válido — la «mutación atómica». +#[derive(Clone, Copy, PartialEq, Eq)] +enum BodyMode { + /// Pantalla de login: el único cliente es el greeter, no se + /// registran atajos, se rechaza `Spawn` y no hay autoarranque. + Greeter, + /// Sesión de usuario: el compositor funciona con normalidad. + Session, +} + /// `app_id` que distingue a la ventana del shell del escritorio. carmen /// no la tesela: la acopla a una franja al pie de la pantalla. const SHELL_APP_ID: &str = "carmen.shell"; @@ -169,6 +185,12 @@ struct App { body: BodyState, /// El Cerebro: embebido o enlazado. brain: Brain, + /// Fase del ciclo de vida — login o sesión (ver [`BodyMode`]). + mode: BodyMode, + /// Identidad a la que rebajar privilegios al lanzar procesos de + /// sesión. `None` salvo tras el traspaso del DM — entonces cada + /// `spawn` hace `setuid`/`setgid` a este usuario (si somos root). + session_user: Option, /// Atajos globales a interceptar (los registra el Cerebro). grabs: Vec, /// Atajo capturado en el último evento de teclado, pendiente de enviar. @@ -292,7 +314,15 @@ impl App { } BodyOp::SetGrabs(keys) => self.grabs = keys, BodyOp::SetCursor(_) => {} - BodyOp::Spawn(cmd) => spawn_command(&cmd), + BodyOp::Spawn(cmd) => { + // En modo greeter no se lanza nada: la pantalla de login + // no es un sitio desde donde abrir programas. + if self.mode == BodyMode::Greeter { + eprintln!("mirada-compositor · «{cmd}» rechazado — modo greeter."); + } else { + spawn_command(&cmd, self.session_user.as_ref()); + } + } BodyOp::Shutdown => self.running = false, } } @@ -378,6 +408,45 @@ impl App { self.brain_feed(ev); } } + + /// El traspaso del DM — la «mutación atómica». Llega el tiquet de un + /// login válido y el compositor pasa de la pantalla de greeter a la + /// sesión del usuario **sin reiniciar el servidor Wayland**: el mismo + /// proceso, la misma GPU, las mismas ventanas. Idempotente — un + /// segundo tiquet (no debería llegar) se ignora. + fn complete_greeter_handoff(&mut self, ticket: SessionTicket) { + if self.mode == BodyMode::Session { + return; // ya en sesión — un tiquet de más, se ignora + } + println!( + "mirada-compositor · traspaso a la sesión de «{}» (uid {}).", + ticket.user.name, ticket.user.uid + ); + if !nix::unistd::geteuid().is_root() { + eprintln!( + "mirada-compositor · aviso: no corro como root — la sesión \ + heredará mis privilegios, sin setuid al usuario." + ); + } + self.mode = BodyMode::Session; + self.session_user = Some(ticket.user.clone()); + + // Ya en sesión: registra los atajos del escritorio (en modo + // greeter se omitieron a propósito — ver `build_app`). + if let Brain::Embedded(desktop) = &self.brain { + let grab = desktop.grab_keys(); + self.apply_commands(vec![grab]); + } + + // Arranca la sesión: el comando del tiquet, o el autoarranque + // del usuario si el tiquet no trae ninguno. + let user = self.session_user.clone(); + if ticket.session.trim().is_empty() { + spawn_autostart(user.as_ref()); + } else { + spawn_command(&ticket.session, user.as_ref()); + } + } } // --------------------------------------------------------------------- @@ -738,33 +807,85 @@ const THEME_ENV: &[(&str, &str)] = &[ /// conecta a este compositor; además se le inyecta [`THEME_ENV`] para /// que GTK y Qt adopten el tema del escritorio. Lo usan la acción /// `spawn:…` del keymap, la variable `MIRADA_STARTUP` y el autoarranque. -fn spawn_command(cmd: &str) { +/// +/// `as_user`: si viene una identidad y el compositor corre como root +/// (modo DM, tras el traspaso), el hijo baja a ese usuario — ver +/// [`apply_user`]. Con `None`, o sin ser root, lanza con la identidad +/// actual del compositor. +fn spawn_command(cmd: &str, as_user: Option<&UserInfo>) { let cmd = cmd.trim(); if cmd.is_empty() { return; } - match std::process::Command::new("sh") - .arg("-c") - .arg(cmd) - .envs(THEME_ENV.iter().copied()) - .spawn() - { + let mut command = std::process::Command::new("sh"); + command.arg("-c").arg(cmd).envs(THEME_ENV.iter().copied()); + if let Some(user) = as_user { + if nix::unistd::geteuid().is_root() { + apply_user(&mut command, user); + } + } + match command.spawn() { Ok(child) => println!("mirada-compositor · lanzado (pid {}): {cmd}", child.id()), Err(e) => eprintln!("mirada-compositor · no pude lanzar «{cmd}»: {e}"), } } -/// La ruta del archivo de autoarranque del usuario, -/// `~/.config/mirada/autostart` — junto al keymap y las reglas. -fn autostart_path() -> Option { - Keymap::default_path().and_then(|p| p.parent().map(|d| d.join("autostart"))) +/// Prepara un `Command` para que el hijo corra como `user`: fija grupos +/// suplementarios, gid, uid y una sesión propia, hace `cd` a su home e +/// inyecta las variables de identidad. Sólo se llama tras comprobar que +/// el compositor es root. +/// +/// La lista de grupos se calcula **en el padre**: `getgrouplist` +/// consulta NSS (abre `/etc/group`), y eso no es seguro entre `fork` y +/// `exec`; en `pre_exec` quedan sólo syscalls async-signal-safe. +fn apply_user(command: &mut std::process::Command, user: &UserInfo) { + use nix::unistd::{setgid, setgroups, setuid, Gid, Uid}; + use std::os::unix::process::CommandExt; + + let uid = Uid::from_raw(user.uid); + let gid = Gid::from_raw(user.gid); + let groups: Vec = std::ffi::CString::new(user.name.as_bytes()) + .ok() + .and_then(|name| nix::unistd::getgrouplist(&name, gid).ok()) + .unwrap_or_else(|| vec![gid]); + + command + .env("HOME", &user.home) + .env("USER", &user.name) + .env("LOGNAME", &user.name) + .env("SHELL", &user.shell) + .current_dir(&user.home); + + // SAFETY: corre en el hijo, entre `fork` y `exec`. Sólo syscalls + // async-signal-safe. El orden es obligatorio: grupos y gid ANTES que + // uid — al rebajar el uid se pierde el privilegio para fijarlos. + unsafe { + command.pre_exec(move || { + setgroups(&groups)?; + setgid(gid)?; + setuid(uid)?; + let _ = nix::unistd::setsid(); // sesión propia; no es crítico + Ok(()) + }); + } +} + +/// La ruta del archivo de autoarranque, `…/mirada/autostart` — junto al +/// keymap y las reglas. Con un usuario (tras el traspaso del DM) se +/// resuelve bajo su home; sin él, bajo la config del proceso actual. +fn autostart_path(user: Option<&UserInfo>) -> Option { + match user { + Some(u) => Some(u.home.join(".config/mirada/autostart")), + None => Keymap::default_path().and_then(|p| p.parent().map(|d| d.join("autostart"))), + } } /// Lanza los programas del archivo de autoarranque: un comando por /// línea, `#` comenta y las líneas en blanco se saltan. Sin archivo, no -/// hace nada. Se llama una vez al arrancar, con el socket ya abierto. -fn spawn_autostart() { - let Some(path) = autostart_path() else { +/// hace nada. Se llama una vez al arrancar (o tras el traspaso del DM), +/// con el socket ya abierto. `as_user` se propaga a [`spawn_command`]. +fn spawn_autostart(as_user: Option<&UserInfo>) { + let Some(path) = autostart_path(as_user) else { return; }; let Ok(text) = std::fs::read_to_string(&path) else { @@ -776,7 +897,7 @@ fn spawn_autostart() { if line.is_empty() || line.starts_with('#') { continue; } - spawn_command(line); + spawn_command(line, as_user); n += 1; } if n > 0 { @@ -784,6 +905,49 @@ fn spawn_autostart() { } } +/// Nombre o ruta del binario del greeter. `MIRADA_GREETER_BIN` lo +/// sobreescribe — cómodo en desarrollo para apuntar a `target/…`. +fn greeter_bin() -> String { + std::env::var("MIRADA_GREETER_BIN").unwrap_or_else(|_| "mirada-greeter".to_string()) +} + +/// Lanza `mirada-greeter` como proceso hijo, en modo DM, con el stdout +/// capturado. Un hilo lee sus líneas: la que sea un [`SessionTicket`] se +/// entrega por `send` (el bucle de eventos hará el traspaso); el resto +/// del stdout se reenvía a la consola con el prefijo `greeter ·`. El +/// hilo es dueño del `Child` y lo cosecha cuando el greeter termina. +fn spawn_greeter(send: S) -> std::io::Result<()> +where + S: Fn(SessionTicket) + Send + 'static, +{ + use std::io::{BufRead, BufReader}; + use std::process::{Command, Stdio}; + + let mut child = Command::new(greeter_bin()) + .envs(THEME_ENV.iter().copied()) + .stdout(Stdio::piped()) + .spawn()?; + let stdout = child.stdout.take().expect("stdout pedido con Stdio::piped"); + println!("mirada-compositor · greeter lanzado (pid {}).", child.id()); + + std::thread::spawn(move || { + for line in BufReader::new(stdout).lines().map_while(Result::ok) { + match SessionTicket::from_line(&line) { + Some(ticket) => { + println!("mirada-compositor · tiquet de sesión recibido del greeter."); + send(ticket); + } + None => println!("greeter · {line}"), + } + } + match child.wait() { + Ok(status) => println!("mirada-compositor · el greeter terminó ({status})."), + Err(e) => eprintln!("mirada-compositor · wait(greeter): {e}"), + } + }); + Ok(()) +} + /// Carga las reglas de ventana del usuario, o ninguna si no hay archivo. fn load_user_rules() -> Rules { match Rules::default_path() { @@ -792,6 +956,20 @@ fn load_user_rules() -> Rules { } } +/// Arma un Cerebro embebido: un `Desktop` con el keymap del usuario y +/// sus reglas de ventana. Lo usan tanto el modo autónomo como el modo +/// greeter (el DM es siempre autónomo — un Cerebro externo no tiene +/// sentido en la pantalla de login). +fn embedded_brain(keymap_path: &Option) -> Brain { + let keymap = match keymap_path { + Some(p) => Keymap::load_or_init(p), + None => Keymap::default(), + }; + let mut desktop = Desktop::with_keymap(keymap); + desktop.set_rules(load_user_rules()); + Brain::Embedded(desktop) +} + /// Crea y anuncia un `wl_output` (un monitor) en el protocolo Wayland — /// muchos clientes (`foot` entre ellos) se niegan a arrancar sin uno. /// Devuelve el [`Output`](smithay::output::Output); hay que mantenerlo @@ -851,7 +1029,7 @@ struct Setup { /// Arma el estado del compositor — todo lo independiente del backend /// gráfico (Wayland, Cerebro, teclado, keymap, control). Cada backend /// (winit o DRM) registra luego su propia salida y monta su bucle. -fn build_app() -> Result> { +fn build_app(greeter: bool) -> Result> { let display: Display = Display::new()?; let dh = display.handle(); @@ -867,23 +1045,23 @@ fn build_app() -> Result> { // el Cerebro embebido; con un Cerebro enlazado, el keymap es asunto suyo. let keymap_path = Keymap::default_path(); - // Elige el Cerebro: enlazado si `MIRADA_SOCKET` está puesto. - let brain = match std::env::var("MIRADA_SOCKET") { - Ok(path) => { - println!("mirada-compositor · esperando al Cerebro en {path} …"); - let link = BodyLink::listen(&path)?; - println!("mirada-compositor · Cerebro conectado."); - Brain::Linked(link) - } - Err(_) => { - println!("mirada-compositor · modo autónomo (Cerebro embebido)."); - let keymap = match &keymap_path { - Some(p) => Keymap::load_or_init(p), - None => Keymap::default(), - }; - let mut desktop = Desktop::with_keymap(keymap); - desktop.set_rules(load_user_rules()); - Brain::Embedded(desktop) + // Elige el Cerebro. El modo greeter (DM) fuerza Cerebro embebido; + // si no, enlazado cuando `MIRADA_SOCKET` está puesto, autónomo si no. + let brain = if greeter { + println!("mirada-compositor · modo greeter (DM) — Cerebro embebido."); + embedded_brain(&keymap_path) + } else { + match std::env::var("MIRADA_SOCKET") { + Ok(path) => { + println!("mirada-compositor · esperando al Cerebro en {path} …"); + let link = BodyLink::listen(&path)?; + println!("mirada-compositor · Cerebro conectado."); + Brain::Linked(link) + } + Err(_) => { + println!("mirada-compositor · modo autónomo (Cerebro embebido)."); + embedded_brain(&keymap_path) + } } }; @@ -904,6 +1082,8 @@ fn build_app() -> Result> { windows: Vec::new(), body: BodyState::new(), brain, + mode: if greeter { BodyMode::Greeter } else { BodyMode::Session }, + session_user: None, grabs: Vec::new(), pending_keybind: None, next_id: 1, @@ -914,16 +1094,23 @@ fn build_app() -> Result> { app.keyboard = Some(keyboard); app.pointer = Some(app.seat.add_pointer()); - // En modo embebido, el propio Desktop dicta los atajos a interceptar. - if let Brain::Embedded(desktop) = &app.brain { - let grab = desktop.grab_keys(); - app.apply_commands(vec![grab]); + // En modo embebido, el propio Desktop dicta los atajos a + // interceptar — salvo en modo greeter: en la pantalla de login + // todas las teclas van al greeter (que el usuario no pueda lanzar + // nada ni cerrar el compositor). Los atajos se registran luego, en + // el traspaso a la sesión (`complete_greeter_handoff`). + if !greeter { + if let Brain::Embedded(desktop) = &app.brain { + let grab = desktop.grab_keys(); + app.apply_commands(vec![grab]); + } } // Vigilancia del keymap para recargarlo en caliente — sólo tiene - // sentido con el Cerebro embebido. + // sentido con el Cerebro embebido y fuera del modo greeter (donde + // no hay atajos registrados que recargar). let keymap_watch = match (&app.brain, &keymap_path) { - (Brain::Embedded(_), Some(p)) => Keymap::watch(p).ok(), + (Brain::Embedded(_), Some(p)) if !greeter => Keymap::watch(p).ok(), _ => None, }; if keymap_watch.is_some() { @@ -953,14 +1140,14 @@ fn build_app() -> Result> { } /// El backend `winit`: corre anidado dentro de una sesión gráfica. -fn run_winit() -> Result<(), Box> { +fn run_winit(greeter: bool) -> Result<(), Box> { let Setup { mut display, app: mut state, keymap_path, keymap_watch, ctl, - } = build_app()?; + } = build_app(greeter)?; let keyboard = state.keyboard.clone().expect("teclado inicializado"); // El backend gráfico va primero. winit abre la ventana del compositor @@ -1016,6 +1203,18 @@ fn run_winit() -> Result<(), Box> { state.output_size = (win_size.w, win_size.h); } + // Modo greeter (DM anidado — útil para iterar la UI del login): + // lanza el greeter y recibe su tiquet por un canal que el bucle sondea. + let greeter_rx = if state.mode == BodyMode::Greeter { + let (tx, rx) = std::sync::mpsc::channel::(); + spawn_greeter(move |ticket| { + let _ = tx.send(ticket); + })?; + Some(rx) + } else { + None + }; + while state.running { // 1 · Eventos del backend (teclado, redimensión, cierre). let status = winit.dispatch_new_events(|event| match event { @@ -1061,7 +1260,14 @@ fn run_winit() -> Result<(), Box> { // 2 · Comandos de un Cerebro enlazado. state.brain_poll(); - // 2 bis · Recarga del keymap si el archivo cambió en disco. + // 2 bis · El tiquet del greeter (modo DM): dispara el traspaso. + if let Some(rx) = &greeter_rx { + while let Ok(ticket) = rx.try_recv() { + state.complete_greeter_handoff(ticket); + } + } + + // 2 ter · Recarga del keymap si el archivo cambió en disco. if keymap_watch.as_ref().is_some_and(|w| w.changed()) { if let Some(path) = &keymap_path { match Keymap::load(path) { @@ -1083,7 +1289,7 @@ fn run_winit() -> Result<(), Box> { } } - // 2 ter · Peticiones del API de control (mirada-ctl). + // 2 quater · Peticiones del API de control (mirada-ctl). if let Some(ctl) = &ctl { while let Some(mut conn) = ctl.poll() { let reply = match conn.read_request() { @@ -1154,25 +1360,34 @@ fn run_winit() -> Result<(), Box> { } fn main() { - let arg = std::env::args().nth(1); - let result = match arg.as_deref() { - Some("--drm") => drm_backend::run(), - Some("--winit") => run_winit(), - Some(other) => { - eprintln!("mirada-compositor: opción desconocida «{other}» — usa --drm o --winit"); + // Banderas en cualquier orden: `--greeter` (modo DM) es ortogonal + // al backend (`--winit` anidado · `--drm` nativo · auto si falta). + let args: Vec = std::env::args().skip(1).collect(); + for a in &args { + if !matches!(a.as_str(), "--greeter" | "--winit" | "--drm") { + eprintln!( + "mirada-compositor: opción desconocida «{a}» — usa --greeter, --winit o --drm" + ); std::process::exit(2); } - None => { + } + let greeter = args.iter().any(|a| a == "--greeter"); + let backend = args.iter().find(|a| matches!(a.as_str(), "--winit" | "--drm")); + + let result = match backend.map(String::as_str) { + Some("--drm") => drm_backend::run(greeter), + Some("--winit") => run_winit(greeter), + _ => { // Auto: con sesión gráfica anfitriona → winit (anidado); // sin ella (una TTY pelada) → backend DRM. let nested = std::env::var_os("WAYLAND_DISPLAY").is_some() || std::env::var_os("DISPLAY").is_some(); if nested { println!("mirada-compositor · sesión gráfica detectada → backend winit."); - run_winit() + run_winit(greeter) } else { println!("mirada-compositor · sin sesión gráfica → backend DRM."); - drm_backend::run() + drm_backend::run(greeter) } } }; diff --git a/crates/apps/mirada-greeter/README.md b/crates/apps/mirada-greeter/README.md index fd1330b..ba844de 100644 --- a/crates/apps/mirada-greeter/README.md +++ b/crates/apps/mirada-greeter/README.md @@ -35,8 +35,10 @@ anidado dentro de otro escritorio: MIRADA_GREETER_MOCK=demo:demo cargo run -p mirada-greeter ``` -## Pendiente +## Integración con el compositor -El consumo del tiquet en `mirada-compositor` (modo greeter + -`BodyMode::Session` + arranque de la sesión con setuid) — siguiente -slice del DM. +El consumo del tiquet ya está cableado. `mirada-compositor --greeter` +lanza este greeter, lee su stdout y, al recibir el `SessionTicket`, +muta de `BodyMode::Greeter` a `BodyMode::Session` y arranca la sesión +del usuario con `setuid`/`setgid` — sin reiniciar el servidor Wayland. +Ver el README de `mirada-compositor`, sección **Modo greeter (DM)**.