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:
Generated
+2
@@ -7827,9 +7827,11 @@ dependencies = [
|
|||||||
name = "mirada-compositor"
|
name = "mirada-compositor"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"brahman-auth",
|
||||||
"mirada-body",
|
"mirada-body",
|
||||||
"mirada-brain",
|
"mirada-brain",
|
||||||
"mirada-link",
|
"mirada-link",
|
||||||
|
"nix 0.29.0",
|
||||||
"smithay",
|
"smithay",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -16,4 +16,6 @@ path = "src/main.rs"
|
|||||||
mirada-brain = { path = "../../modules/mirada/mirada-brain" }
|
mirada-brain = { path = "../../modules/mirada/mirada-brain" }
|
||||||
mirada-body = { path = "../../modules/mirada/mirada-body" }
|
mirada-body = { path = "../../modules/mirada/mirada-body" }
|
||||||
mirada-link = { path = "../../modules/mirada/mirada-link" }
|
mirada-link = { path = "../../modules/mirada/mirada-link" }
|
||||||
|
brahman-auth = { path = "../../protocol/brahman-auth" }
|
||||||
|
nix = { workspace = true }
|
||||||
smithay = "0.7"
|
smithay = "0.7"
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ Tiene **dos backends gráficos**:
|
|||||||
Sin argumentos elige solo: con `DISPLAY`/`WAYLAND_DISPLAY` → `winit`;
|
Sin argumentos elige solo: con `DISPLAY`/`WAYLAND_DISPLAY` → `winit`;
|
||||||
sin ellos → `drm`. O fuérzalo: `mirada-compositor --winit` / `--drm`.
|
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
|
## Backends
|
||||||
|
|
||||||
### winit — anidado
|
### 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
|
Dentro de la sesión, `Ctrl+Alt+F1…F12` salta a otra TTY y vuelve sin
|
||||||
romper carmen.
|
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
|
## Lanzador de aplicaciones
|
||||||
|
|
||||||
`mirada-launcher` escanea los `.desktop` del sistema y lanza el que
|
`mirada-launcher` escanea los `.desktop` del sistema y lanza el que
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ use smithay::backend::udev;
|
|||||||
use smithay::input::keyboard::FilterResult;
|
use smithay::input::keyboard::FilterResult;
|
||||||
use smithay::input::pointer::{AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent};
|
use smithay::input::pointer::{AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent};
|
||||||
use smithay::output::OutputModeSource;
|
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::generic::Generic;
|
||||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||||
use smithay::reexports::calloop::{EventLoop, Interest, Mode as CalloopMode, PostAction};
|
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,
|
DeviceFd, IsAlive, Logical, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use brahman_auth::SessionTicket;
|
||||||
use mirada_brain::{BodyEvent, CtlReply, Keymap, Rect};
|
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).
|
/// El `DrmCompositor` concreto para la salida (un solo GPU).
|
||||||
type Compositor =
|
type Compositor =
|
||||||
@@ -604,8 +609,9 @@ impl DrmState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Arranca el Cuerpo sobre DRM/KMS — fases 1, 2a y 2b.
|
/// Arranca el Cuerpo sobre DRM/KMS — fases 1, 2a y 2b. Con `greeter`,
|
||||||
pub fn run() -> Result<(), Box<dyn Error>> {
|
/// el compositor nace en modo DM: ver [`BodyMode`].
|
||||||
|
pub fn run(greeter: bool) -> Result<(), Box<dyn Error>> {
|
||||||
println!("mirada-compositor · backend DRM.");
|
println!("mirada-compositor · backend DRM.");
|
||||||
println!("──────────────────────────────────────────────────");
|
println!("──────────────────────────────────────────────────");
|
||||||
|
|
||||||
@@ -733,7 +739,8 @@ pub fn run() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
// 7 · El estado Wayland (Cerebro, teclado, keymap, control).
|
// 7 · El estado Wayland (Cerebro, teclado, keymap, control).
|
||||||
println!("[7/8] armando el estado Wayland …");
|
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
|
// Con el renderer ya creado, anuncia dmabuf — sin esto las apps que
|
||||||
// pintan por GPU (GPUI, navegadores acelerados) no pueden conectarse.
|
// pintan por GPU (GPUI, navegadores acelerados) no pueden conectarse.
|
||||||
crate::announce_dmabuf(&mut app, &display.handle(), &renderer);
|
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);
|
std::env::set_var("WAYLAND_DISPLAY", &socket_name);
|
||||||
println!(" escuchando en WAYLAND_DISPLAY={socket_name}");
|
println!(" escuchando en WAYLAND_DISPLAY={socket_name}");
|
||||||
|
|
||||||
|
// 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`.
|
// Autoarranque: los programas de `~/.config/mirada/autostart`.
|
||||||
crate::spawn_autostart();
|
crate::spawn_autostart(None);
|
||||||
|
// App de arranque: si `MIRADA_STARTUP` trae un comando, se lanza
|
||||||
// App de arranque: si `MIRADA_STARTUP` trae un comando, se lanza como
|
// como hijo (hereda `WAYLAND_DISPLAY`) — cómodo para probar sin
|
||||||
// hijo (hereda `WAYLAND_DISPLAY`) — cómodo para probar sin saltar de VT.
|
// saltar de VT.
|
||||||
if let Ok(cmd) = std::env::var("MIRADA_STARTUP") {
|
if let Ok(cmd) = std::env::var("MIRADA_STARTUP") {
|
||||||
crate::spawn_command(&cmd);
|
crate::spawn_command(&cmd, None);
|
||||||
}
|
}
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// 8 · El bucle `calloop`: VBlank, teclado, clientes y un timer.
|
// 8 · El bucle `calloop`: VBlank, teclado, clientes y un timer.
|
||||||
println!("[8/8] montando el bucle de eventos …");
|
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}"))?;
|
.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
|
// Tope de tiempo opcional: `MIRADA_DRM_TIMEOUT=<segundos>` cierra el
|
||||||
// compositor solo (0 o sin definir = sin tope). El teclado ya
|
// compositor solo (0 o sin definir = sin tope). El teclado ya
|
||||||
// funciona — `Super+Shift+e` o `Ctrl+C` son la salida normal.
|
// funciona — `Super+Shift+e` o `Ctrl+C` son la salida normal.
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ use smithay::{
|
|||||||
delegate_shm, delegate_xdg_decoration, delegate_xdg_shell,
|
delegate_shm, delegate_xdg_decoration, delegate_xdg_shell,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use brahman_auth::{SessionTicket, UserInfo};
|
||||||
use mirada_body::{BodyOp, BodyState};
|
use mirada_body::{BodyOp, BodyState};
|
||||||
use mirada_brain::{
|
use mirada_brain::{
|
||||||
BodyEvent, BrainCommand, CtlReply, CtlRequest, CtlServer, Desktop, Keymap, Rules,
|
BodyEvent, BrainCommand, CtlReply, CtlRequest, CtlServer, Desktop, Keymap, Rules,
|
||||||
@@ -89,6 +90,21 @@ enum Brain {
|
|||||||
Linked(BodyLink),
|
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
|
/// `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.
|
/// no la tesela: la acopla a una franja al pie de la pantalla.
|
||||||
const SHELL_APP_ID: &str = "carmen.shell";
|
const SHELL_APP_ID: &str = "carmen.shell";
|
||||||
@@ -169,6 +185,12 @@ struct App {
|
|||||||
body: BodyState,
|
body: BodyState,
|
||||||
/// El Cerebro: embebido o enlazado.
|
/// El Cerebro: embebido o enlazado.
|
||||||
brain: Brain,
|
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<UserInfo>,
|
||||||
/// Atajos globales a interceptar (los registra el Cerebro).
|
/// Atajos globales a interceptar (los registra el Cerebro).
|
||||||
grabs: Vec<String>,
|
grabs: Vec<String>,
|
||||||
/// Atajo capturado en el último evento de teclado, pendiente de enviar.
|
/// Atajo capturado en el último evento de teclado, pendiente de enviar.
|
||||||
@@ -292,7 +314,15 @@ impl App {
|
|||||||
}
|
}
|
||||||
BodyOp::SetGrabs(keys) => self.grabs = keys,
|
BodyOp::SetGrabs(keys) => self.grabs = keys,
|
||||||
BodyOp::SetCursor(_) => {}
|
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,
|
BodyOp::Shutdown => self.running = false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,6 +408,45 @@ impl App {
|
|||||||
self.brain_feed(ev);
|
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
|
/// 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
|
/// que GTK y Qt adopten el tema del escritorio. Lo usan la acción
|
||||||
/// `spawn:…` del keymap, la variable `MIRADA_STARTUP` y el autoarranque.
|
/// `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();
|
let cmd = cmd.trim();
|
||||||
if cmd.is_empty() {
|
if cmd.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match std::process::Command::new("sh")
|
let mut command = std::process::Command::new("sh");
|
||||||
.arg("-c")
|
command.arg("-c").arg(cmd).envs(THEME_ENV.iter().copied());
|
||||||
.arg(cmd)
|
if let Some(user) = as_user {
|
||||||
.envs(THEME_ENV.iter().copied())
|
if nix::unistd::geteuid().is_root() {
|
||||||
.spawn()
|
apply_user(&mut command, user);
|
||||||
{
|
}
|
||||||
|
}
|
||||||
|
match command.spawn() {
|
||||||
Ok(child) => println!("mirada-compositor · lanzado (pid {}): {cmd}", child.id()),
|
Ok(child) => println!("mirada-compositor · lanzado (pid {}): {cmd}", child.id()),
|
||||||
Err(e) => eprintln!("mirada-compositor · no pude lanzar «{cmd}»: {e}"),
|
Err(e) => eprintln!("mirada-compositor · no pude lanzar «{cmd}»: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// La ruta del archivo de autoarranque del usuario,
|
/// Prepara un `Command` para que el hijo corra como `user`: fija grupos
|
||||||
/// `~/.config/mirada/autostart` — junto al keymap y las reglas.
|
/// suplementarios, gid, uid y una sesión propia, hace `cd` a su home e
|
||||||
fn autostart_path() -> Option<std::path::PathBuf> {
|
/// inyecta las variables de identidad. Sólo se llama tras comprobar que
|
||||||
Keymap::default_path().and_then(|p| p.parent().map(|d| d.join("autostart")))
|
/// 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<Gid> = 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<std::path::PathBuf> {
|
||||||
|
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
|
/// Lanza los programas del archivo de autoarranque: un comando por
|
||||||
/// línea, `#` comenta y las líneas en blanco se saltan. Sin archivo, no
|
/// 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.
|
/// hace nada. Se llama una vez al arrancar (o tras el traspaso del DM),
|
||||||
fn spawn_autostart() {
|
/// con el socket ya abierto. `as_user` se propaga a [`spawn_command`].
|
||||||
let Some(path) = autostart_path() else {
|
fn spawn_autostart(as_user: Option<&UserInfo>) {
|
||||||
|
let Some(path) = autostart_path(as_user) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Ok(text) = std::fs::read_to_string(&path) else {
|
let Ok(text) = std::fs::read_to_string(&path) else {
|
||||||
@@ -776,7 +897,7 @@ fn spawn_autostart() {
|
|||||||
if line.is_empty() || line.starts_with('#') {
|
if line.is_empty() || line.starts_with('#') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
spawn_command(line);
|
spawn_command(line, as_user);
|
||||||
n += 1;
|
n += 1;
|
||||||
}
|
}
|
||||||
if n > 0 {
|
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<S>(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.
|
/// Carga las reglas de ventana del usuario, o ninguna si no hay archivo.
|
||||||
fn load_user_rules() -> Rules {
|
fn load_user_rules() -> Rules {
|
||||||
match Rules::default_path() {
|
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<std::path::PathBuf>) -> 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 —
|
/// Crea y anuncia un `wl_output` (un monitor) en el protocolo Wayland —
|
||||||
/// muchos clientes (`foot` entre ellos) se niegan a arrancar sin uno.
|
/// muchos clientes (`foot` entre ellos) se niegan a arrancar sin uno.
|
||||||
/// Devuelve el [`Output`](smithay::output::Output); hay que mantenerlo
|
/// 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
|
/// Arma el estado del compositor — todo lo independiente del backend
|
||||||
/// gráfico (Wayland, Cerebro, teclado, keymap, control). Cada backend
|
/// gráfico (Wayland, Cerebro, teclado, keymap, control). Cada backend
|
||||||
/// (winit o DRM) registra luego su propia salida y monta su bucle.
|
/// (winit o DRM) registra luego su propia salida y monta su bucle.
|
||||||
fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
|
fn build_app(greeter: bool) -> Result<Setup, Box<dyn std::error::Error>> {
|
||||||
let display: Display<App> = Display::new()?;
|
let display: Display<App> = Display::new()?;
|
||||||
let dh = display.handle();
|
let dh = display.handle();
|
||||||
|
|
||||||
@@ -867,8 +1045,13 @@ fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
|
|||||||
// el Cerebro embebido; con un Cerebro enlazado, el keymap es asunto suyo.
|
// el Cerebro embebido; con un Cerebro enlazado, el keymap es asunto suyo.
|
||||||
let keymap_path = Keymap::default_path();
|
let keymap_path = Keymap::default_path();
|
||||||
|
|
||||||
// Elige el Cerebro: enlazado si `MIRADA_SOCKET` está puesto.
|
// Elige el Cerebro. El modo greeter (DM) fuerza Cerebro embebido;
|
||||||
let brain = match std::env::var("MIRADA_SOCKET") {
|
// 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) => {
|
Ok(path) => {
|
||||||
println!("mirada-compositor · esperando al Cerebro en {path} …");
|
println!("mirada-compositor · esperando al Cerebro en {path} …");
|
||||||
let link = BodyLink::listen(&path)?;
|
let link = BodyLink::listen(&path)?;
|
||||||
@@ -877,13 +1060,8 @@ fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
println!("mirada-compositor · modo autónomo (Cerebro embebido).");
|
println!("mirada-compositor · modo autónomo (Cerebro embebido).");
|
||||||
let keymap = match &keymap_path {
|
embedded_brain(&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)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -904,6 +1082,8 @@ fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
|
|||||||
windows: Vec::new(),
|
windows: Vec::new(),
|
||||||
body: BodyState::new(),
|
body: BodyState::new(),
|
||||||
brain,
|
brain,
|
||||||
|
mode: if greeter { BodyMode::Greeter } else { BodyMode::Session },
|
||||||
|
session_user: None,
|
||||||
grabs: Vec::new(),
|
grabs: Vec::new(),
|
||||||
pending_keybind: None,
|
pending_keybind: None,
|
||||||
next_id: 1,
|
next_id: 1,
|
||||||
@@ -914,16 +1094,23 @@ fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
|
|||||||
app.keyboard = Some(keyboard);
|
app.keyboard = Some(keyboard);
|
||||||
app.pointer = Some(app.seat.add_pointer());
|
app.pointer = Some(app.seat.add_pointer());
|
||||||
|
|
||||||
// En modo embebido, el propio Desktop dicta los atajos a interceptar.
|
// 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 {
|
if let Brain::Embedded(desktop) = &app.brain {
|
||||||
let grab = desktop.grab_keys();
|
let grab = desktop.grab_keys();
|
||||||
app.apply_commands(vec![grab]);
|
app.apply_commands(vec![grab]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Vigilancia del keymap para recargarlo en caliente — sólo tiene
|
// 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) {
|
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,
|
_ => None,
|
||||||
};
|
};
|
||||||
if keymap_watch.is_some() {
|
if keymap_watch.is_some() {
|
||||||
@@ -953,14 +1140,14 @@ fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// El backend `winit`: corre anidado dentro de una sesión gráfica.
|
/// El backend `winit`: corre anidado dentro de una sesión gráfica.
|
||||||
fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
|
fn run_winit(greeter: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let Setup {
|
let Setup {
|
||||||
mut display,
|
mut display,
|
||||||
app: mut state,
|
app: mut state,
|
||||||
keymap_path,
|
keymap_path,
|
||||||
keymap_watch,
|
keymap_watch,
|
||||||
ctl,
|
ctl,
|
||||||
} = build_app()?;
|
} = build_app(greeter)?;
|
||||||
let keyboard = state.keyboard.clone().expect("teclado inicializado");
|
let keyboard = state.keyboard.clone().expect("teclado inicializado");
|
||||||
|
|
||||||
// El backend gráfico va primero. winit abre la ventana del compositor
|
// El backend gráfico va primero. winit abre la ventana del compositor
|
||||||
@@ -1016,6 +1203,18 @@ fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
state.output_size = (win_size.w, win_size.h);
|
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::<SessionTicket>();
|
||||||
|
spawn_greeter(move |ticket| {
|
||||||
|
let _ = tx.send(ticket);
|
||||||
|
})?;
|
||||||
|
Some(rx)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
while state.running {
|
while state.running {
|
||||||
// 1 · Eventos del backend (teclado, redimensión, cierre).
|
// 1 · Eventos del backend (teclado, redimensión, cierre).
|
||||||
let status = winit.dispatch_new_events(|event| match event {
|
let status = winit.dispatch_new_events(|event| match event {
|
||||||
@@ -1061,7 +1260,14 @@ fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// 2 · Comandos de un Cerebro enlazado.
|
// 2 · Comandos de un Cerebro enlazado.
|
||||||
state.brain_poll();
|
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 keymap_watch.as_ref().is_some_and(|w| w.changed()) {
|
||||||
if let Some(path) = &keymap_path {
|
if let Some(path) = &keymap_path {
|
||||||
match Keymap::load(path) {
|
match Keymap::load(path) {
|
||||||
@@ -1083,7 +1289,7 @@ fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2 ter · Peticiones del API de control (mirada-ctl).
|
// 2 quater · Peticiones del API de control (mirada-ctl).
|
||||||
if let Some(ctl) = &ctl {
|
if let Some(ctl) = &ctl {
|
||||||
while let Some(mut conn) = ctl.poll() {
|
while let Some(mut conn) = ctl.poll() {
|
||||||
let reply = match conn.read_request() {
|
let reply = match conn.read_request() {
|
||||||
@@ -1154,25 +1360,34 @@ fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let arg = std::env::args().nth(1);
|
// Banderas en cualquier orden: `--greeter` (modo DM) es ortogonal
|
||||||
let result = match arg.as_deref() {
|
// al backend (`--winit` anidado · `--drm` nativo · auto si falta).
|
||||||
Some("--drm") => drm_backend::run(),
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
Some("--winit") => run_winit(),
|
for a in &args {
|
||||||
Some(other) => {
|
if !matches!(a.as_str(), "--greeter" | "--winit" | "--drm") {
|
||||||
eprintln!("mirada-compositor: opción desconocida «{other}» — usa --drm o --winit");
|
eprintln!(
|
||||||
|
"mirada-compositor: opción desconocida «{a}» — usa --greeter, --winit o --drm"
|
||||||
|
);
|
||||||
std::process::exit(2);
|
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);
|
// Auto: con sesión gráfica anfitriona → winit (anidado);
|
||||||
// sin ella (una TTY pelada) → backend DRM.
|
// sin ella (una TTY pelada) → backend DRM.
|
||||||
let nested = std::env::var_os("WAYLAND_DISPLAY").is_some()
|
let nested = std::env::var_os("WAYLAND_DISPLAY").is_some()
|
||||||
|| std::env::var_os("DISPLAY").is_some();
|
|| std::env::var_os("DISPLAY").is_some();
|
||||||
if nested {
|
if nested {
|
||||||
println!("mirada-compositor · sesión gráfica detectada → backend winit.");
|
println!("mirada-compositor · sesión gráfica detectada → backend winit.");
|
||||||
run_winit()
|
run_winit(greeter)
|
||||||
} else {
|
} else {
|
||||||
println!("mirada-compositor · sin sesión gráfica → backend DRM.");
|
println!("mirada-compositor · sin sesión gráfica → backend DRM.");
|
||||||
drm_backend::run()
|
drm_backend::run(greeter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ anidado dentro de otro escritorio:
|
|||||||
MIRADA_GREETER_MOCK=demo:demo cargo run -p mirada-greeter
|
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 +
|
El consumo del tiquet ya está cableado. `mirada-compositor --greeter`
|
||||||
`BodyMode::Session` + arranque de la sesión con setuid) — siguiente
|
lanza este greeter, lee su stdout y, al recibir el `SessionTicket`,
|
||||||
slice del DM.
|
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)**.
|
||||||
|
|||||||
Reference in New Issue
Block a user