From a9bb88d9f352c43fde90b2e8f3ede8a8d44c312c Mon Sep 17 00:00:00 2001 From: Votre Nom Date: Sat, 9 May 2026 11:22:54 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Phase=205=20=E2=80=94=20input=20?= =?UTF-8?q?backend=20Redox=20valid=C3=A9=20runtime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Crates ajoutés : - redox-wl-input (lib) : InputBackend wrappe Arc + enum InputEvent { Key, TextInput, PointerMotion(Relative)?, PointerButton, PointerScroll, Quit, Unhandled, Handoff } - redox-wl-test-input (bin) : ouvre display + take CRTC + peint bleu marine + polle events 30s en logguant chaque event Modifs redox-wl-display : - _consumer: ConsumerHandle → consumer: Arc - + pub fn consumer() -> Arc pour partage avec input Validation runtime sur Redox bootée via QEMU + monitor unix socket : 20 events injectés via `sendkey` et `mouse_button` HMP commands, tous reçus et traduits correctement : - a/b/c PRESS+RELEASE — keymap directe - shift+a → 'A' uppercase — modificateur fonctionnel - ctrl+c → ctrl PRESS + 'c' PRESS — composition fonctionnelle - mouse_button 1/0 → PointerButton L=true/false - Esc, Enter, Shift, Ctrl reçus avec scancode brut Décision architecturale : un seul ConsumerHandle partagé via Arc entre RedoxOutput (pour vie du VT) et InputBackend (lecteur unique d'events). Sinon deux consumers = deux VTs distincts dont un seul reçoit les events. Capture preuve : docs/phase5-blue-screen-with-input.png — bleu marine plein écran 1280x800 confirmant que display + input fonctionnent ensemble dans le même binaire. docs/phase5-input-backend.md : compte-rendu complet. Image restaurée à boot Orbital normal après session. Leyoda 2026 – GPLv3 --- crates/redox-wl-display/src/lib.rs | 15 +- crates/redox-wl-input/Cargo.toml | 9 ++ crates/redox-wl-input/src/lib.rs | 134 ++++++++++++++++ crates/redox-wl-test-input/Cargo.toml | 8 + crates/redox-wl-test-input/src/main.rs | 176 +++++++++++++++++++++ docs/phase5-blue-screen-with-input.png | Bin 0 -> 449 bytes docs/phase5-input-backend.md | 202 +++++++++++++++++++++++++ 7 files changed, 542 insertions(+), 2 deletions(-) create mode 100644 crates/redox-wl-input/Cargo.toml create mode 100644 crates/redox-wl-input/src/lib.rs create mode 100644 crates/redox-wl-test-input/Cargo.toml create mode 100644 crates/redox-wl-test-input/src/main.rs create mode 100644 docs/phase5-blue-screen-with-input.png create mode 100644 docs/phase5-input-backend.md diff --git a/crates/redox-wl-display/src/lib.rs b/crates/redox-wl-display/src/lib.rs index 719139c..edd1d03 100644 --- a/crates/redox-wl-display/src/lib.rs +++ b/crates/redox-wl-display/src/lib.rs @@ -18,6 +18,7 @@ use std::io; use std::os::fd::AsRawFd; use std::slice; +use std::sync::Arc; use drm::Device as _; use drm::buffer::{Buffer as _, DrmFourcc}; @@ -32,7 +33,10 @@ pub struct RedoxOutput { /// Consumer handle GARDÉ EN VIE pour que le VT alloué par inputd persiste. /// Si on drop ce handle, inputd retire le VT de sa table et toute opération /// `inputd -A ` ultérieure dira "non-existent VT". - _consumer: ConsumerHandle, + /// Exposé via `consumer()` pour partage avec un éventuel `InputBackend` + /// (qui polle les events sur le même fd, sans réouvrir un consumer + /// supplémentaire — sinon on aurait deux VTs et un seul recevrait les events). + consumer: Arc, handle: V2GraphicsHandle, width: u32, height: u32, @@ -101,7 +105,7 @@ impl RedoxOutput { .ok_or_else(|| io::Error::other("no crtc compatible with encoder"))?; Ok(Self { - _consumer: consumer, + consumer: Arc::new(consumer), handle, width: w as u32, height: h as u32, @@ -128,6 +132,13 @@ impl RedoxOutput { pub fn vt(&self) -> usize { self.vt } + /// Renvoie un Arc partagé du `ConsumerHandle` pour un `InputBackend` qui + /// veut polle les events sur le même VT que le display. + /// Le caller ne doit PAS lire les events depuis ce handle (RedoxOutput + /// ne lit pas non plus) — c'est l'`InputBackend` qui sera lecteur unique. + pub fn consumer(&self) -> Arc { + Arc::clone(&self.consumer) + } /// Alloue un CpuBackedBuffer ARGB8888 plein écran, l'ajoute comme /// framebuffer DRM et appelle `set_crtc` pour qu'il soit affiché. diff --git a/crates/redox-wl-input/Cargo.toml b/crates/redox-wl-input/Cargo.toml new file mode 100644 index 0000000..f9383a1 --- /dev/null +++ b/crates/redox-wl-input/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "redox-wl-input" +version = "0.1.0" +edition = "2021" +description = "Input backend wrapper around inputd ConsumerHandle for Redox" + +[dependencies] +inputd = { git = "https://gitlab.redox-os.org/redox-os/base.git" } +orbclient = "0.3" diff --git a/crates/redox-wl-input/src/lib.rs b/crates/redox-wl-input/src/lib.rs new file mode 100644 index 0000000..de909b7 --- /dev/null +++ b/crates/redox-wl-input/src/lib.rs @@ -0,0 +1,134 @@ +//! Backend input pour le compositor Redox. +//! +//! Encapsule `inputd::ConsumerHandle` et traduit les `orbclient::Event` +//! bruts en un enum `InputEvent` neutre que le compositor peut consommer +//! sans être couplé à orbclient ou à inputd directement. +//! +//! Conception : un seul `ConsumerHandle` est partagé entre `RedoxOutput` +//! (qui le tient en vie pour préserver le VT) et `InputBackend` (qui polle +//! les events). `RedoxOutput` ne lit pas les events ; `InputBackend` est +//! lecteur unique. Ils partagent via `Arc`. + +use std::io; +use std::os::fd::{AsRawFd, BorrowedFd}; +use std::sync::Arc; + +use inputd::{ConsumerHandle, ConsumerHandleEvent}; +use orbclient::{Event, EventOption}; + +/// Event compositor neutre, traduit depuis `orbclient::Event`. +/// +/// Reste indépendant de Wayland : un compositor peut le mapper vers +/// `wl_keyboard.key`, `wl_pointer.motion`, etc. ; un autre frontend +/// peut le mapper vers tout autre protocole. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InputEvent { + /// Touche clavier pressée ou relâchée. + Key { + /// Caractère résolu via la keymap active. `'\0'` si touche non-imprimable. + character: char, + /// Scancode brut envoyé par le driver clavier. + scancode: u8, + /// `true` = press, `false` = release. + pressed: bool, + }, + /// Caractère issu d'une méthode IME (compose, dead key, etc.). + TextInput { character: char }, + /// Position absolue de la souris dans les coordonnées display. + PointerMotion { x: i32, y: i32 }, + /// Mouvement relatif (utile pour les jeux ou modes "grab"). + PointerMotionRelative { dx: i32, dy: i32 }, + /// État des boutons de la souris (toujours envoyé en bloc côté Redox). + PointerButton { + left: bool, + middle: bool, + right: bool, + }, + /// Événement de scroll (delta). + PointerScroll { dx: i32, dy: i32 }, + /// L'utilisateur veut sortir (Quit / kill window). + Quit, + /// Event reçu mais non encore mappé. Conserve le code orbclient pour debug. + Unhandled { code: i64, a: i64, b: i64 }, + /// inputd nous a retiré le contrôle du VT (handoff vers un autre VT). + /// Le compositor doit relâcher le display jusqu'au prochain Resume. + Handoff, +} + +/// Capacité maximale du buffer interne de read_events. Aligné sur Orbital. +const READ_BUF: usize = 16; + +pub struct InputBackend { + consumer: Arc, +} + +impl InputBackend { + /// Construit un backend depuis le `ConsumerHandle` partagé avec le + /// `RedoxOutput`. Le caller ne doit pas appeler `read_events` lui-même + /// sur le consumer, sinon les events seront répartis entre les lecteurs. + pub fn new(consumer: Arc) -> Self { + Self { consumer } + } + + /// Borrow le fd des events pour subscription dans une `EventQueue`. + pub fn event_fd(&self) -> BorrowedFd<'_> { + self.consumer.event_handle() + } + + pub fn event_raw_fd(&self) -> i32 { + self.consumer.event_handle().as_raw_fd() + } + + /// Récupère tous les events disponibles maintenant. Non-bloquant + /// (`ConsumerHandle::new_vt` ouvre avec `O_NONBLOCK`). + /// Retourne aussi `InputEvent::Handoff` si inputd nous notifie un switch VT. + pub fn poll(&self) -> io::Result> { + let mut out = Vec::new(); + let mut buf = [Event::new(); READ_BUF]; + loop { + match self.consumer.read_events(&mut buf)? { + ConsumerHandleEvent::Events(&[]) => break, + ConsumerHandleEvent::Events(events) => { + for ev in events { + out.push(translate(ev)); + } + } + ConsumerHandleEvent::Handoff => { + out.push(InputEvent::Handoff); + break; // après un handoff on arrête de poller cette fois-ci + } + } + } + Ok(out) + } +} + +fn translate(ev: &Event) -> InputEvent { + match ev.to_option() { + EventOption::Key(k) => InputEvent::Key { + character: k.character, + scancode: k.scancode, + pressed: k.pressed, + }, + EventOption::TextInput(t) => InputEvent::TextInput { + character: t.character, + }, + EventOption::Mouse(m) => InputEvent::PointerMotion { x: m.x, y: m.y }, + EventOption::MouseRelative(m) => InputEvent::PointerMotionRelative { + dx: m.dx, + dy: m.dy, + }, + EventOption::Button(b) => InputEvent::PointerButton { + left: b.left, + middle: b.middle, + right: b.right, + }, + EventOption::Scroll(s) => InputEvent::PointerScroll { dx: s.x, dy: s.y }, + EventOption::Quit(_) => InputEvent::Quit, + _ => InputEvent::Unhandled { + code: ev.code, + a: ev.a, + b: ev.b, + }, + } +} diff --git a/crates/redox-wl-test-input/Cargo.toml b/crates/redox-wl-test-input/Cargo.toml new file mode 100644 index 0000000..415a5a7 --- /dev/null +++ b/crates/redox-wl-test-input/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "redox-wl-test-input" +version = "0.1.0" +edition = "2021" + +[dependencies] +redox-wl-display = { path = "../redox-wl-display" } +redox-wl-input = { path = "../redox-wl-input" } diff --git a/crates/redox-wl-test-input/src/main.rs b/crates/redox-wl-test-input/src/main.rs new file mode 100644 index 0000000..820121a --- /dev/null +++ b/crates/redox-wl-test-input/src/main.rs @@ -0,0 +1,176 @@ +//! Phase 5 — Test input backend. +//! +//! Boote en remplaçant Orbital sur le VT 3 (cf init), prend le CRTC, peint +//! un fond uniforme bleu marine pour confirmer qu'on contrôle le display, +//! puis polle les events input pendant 30 secondes en logguant chaque +//! event reçu vers /scheme/debug (visible côté host via le serial QEMU). +//! +//! Côté host on peut envoyer des touches via le monitor QEMU : +//! printf "sendkey a\n" | ncat -U /tmp/qmp.sock +//! printf "sendkey ret\n" | ncat -U /tmp/qmp.sock +//! printf "mouse_move 100 100\n" | ncat -U /tmp/qmp.sock (HMP) +//! printf "mouse_button 1\n" | ncat -U /tmp/qmp.sock + +use std::env; +use std::fs::OpenOptions; +use std::io::Write; +use std::process::{Command, ExitCode}; +use std::sync::{Mutex, OnceLock}; +use std::thread; +use std::time::{Duration, Instant}; + +use redox_wl_display::RedoxOutput; +use redox_wl_input::{InputBackend, InputEvent}; + +struct DebugSink(Mutex>); +impl DebugSink { + fn new() -> Self { + Self(Mutex::new( + OpenOptions::new().write(true).open("/scheme/debug").ok(), + )) + } + fn writeln(&self, s: &str) { + println!("{s}"); + if let Ok(mut g) = self.0.lock() { + if let Some(f) = g.as_mut() { + let _ = writeln!(f, "{s}"); + } + } + } +} +fn dlog(s: &str) { + static SINK: OnceLock = OnceLock::new(); + SINK.get_or_init(DebugSink::new).writeln(s); +} + +fn run() -> Result<(), Box> { + dlog("[input] Phase 5 — test input backend"); + + // Ouvre le display (ce qui alloue notre VT côté inputd) + let mut output = RedoxOutput::open().map_err(|e| format!("RedoxOutput::open: {e}"))?; + let our_vt = output.vt(); + dlog(&format!( + "[input] display {}x{}, VT={our_vt}", + output.width(), + output.height() + )); + + // Active notre VT (sinon set_crtc no-op cf phase 4) + let _ = Command::new("inputd") + .arg("-A") + .arg(our_vt.to_string()) + .status(); + thread::sleep(Duration::from_millis(300)); + + // Prend le CRTC, peint un fond bleu marine pour signaler "on est là" + output + .take_crtc() + .map_err(|e| format!("take_crtc: {e}"))?; + { + let pixels = output.pixels_mut()?; + for p in pixels.iter_mut() { + *p = 0xFF_10_30_60; // ARGB bleu marine sombre + } + } + output + .present_with_takeover() + .map_err(|e| format!("present init: {e}"))?; + dlog("[input] CRTC pris, fond bleu marine peint"); + + // Crée le backend input partageant le ConsumerHandle de RedoxOutput + let input = InputBackend::new(output.consumer()); + dlog(&format!( + "[input] InputBackend prêt, fd events={}", + input.event_raw_fd() + )); + + // Boucle de polling pendant 30 secondes + let start = Instant::now(); + let total_duration = Duration::from_secs(30); + let mut event_count: usize = 0; + let mut quit_requested = false; + + while start.elapsed() < total_duration && !quit_requested { + let events = input.poll().map_err(|e| format!("poll: {e}"))?; + for ev in events { + event_count += 1; + match &ev { + InputEvent::Key { + character, + scancode, + pressed, + } => { + let c = if character.is_control() || *character == '\0' { + '·' + } else { + *character + }; + dlog(&format!( + "[input] #{event_count:04} Key '{c}' scan={scancode:#x} {}", + if *pressed { "PRESS" } else { "RELEASE" } + )); + } + InputEvent::TextInput { character } => { + dlog(&format!("[input] #{event_count:04} TextInput '{character}'")); + } + InputEvent::PointerMotion { x, y } => { + dlog(&format!("[input] #{event_count:04} PointerMotion abs=({x},{y})")); + } + InputEvent::PointerMotionRelative { dx, dy } => { + dlog(&format!( + "[input] #{event_count:04} PointerMotionRelative d=({dx},{dy})" + )); + } + InputEvent::PointerButton { + left, + middle, + right, + } => { + dlog(&format!( + "[input] #{event_count:04} PointerButton L={left} M={middle} R={right}" + )); + } + InputEvent::PointerScroll { dx, dy } => { + dlog(&format!("[input] #{event_count:04} PointerScroll d=({dx},{dy})")); + } + InputEvent::Quit => { + dlog(&format!("[input] #{event_count:04} Quit demandé")); + quit_requested = true; + } + InputEvent::Unhandled { code, a, b } => { + dlog(&format!( + "[input] #{event_count:04} Unhandled code={code} a={a} b={b}" + )); + } + InputEvent::Handoff => { + dlog(&format!("[input] #{event_count:04} Handoff (VT switch)")); + } + } + } + // Re-flush pour tenir l'écran si fbcond/fbbootlogd tente de reprendre + let _ = output.present_with_takeover(); + thread::sleep(Duration::from_millis(50)); + } + + dlog(&format!( + "[input] fin — {event_count} events reçus en {:.1}s", + start.elapsed().as_secs_f32() + )); + let _ = env::var("VT"); + drop(output); + thread::sleep(Duration::from_millis(500)); + Ok(()) +} + +fn main() -> ExitCode { + match run() { + Ok(()) => { + dlog("[input] PASS"); + ExitCode::SUCCESS + } + Err(e) => { + dlog(&format!("[input] FAIL: {e}")); + ExitCode::FAILURE + } + } +} diff --git a/docs/phase5-blue-screen-with-input.png b/docs/phase5-blue-screen-with-input.png new file mode 100644 index 0000000000000000000000000000000000000000..d632f96781e994a95d1fad7401da8c46f03b88e4 GIT binary patch literal 449 zcmeAS@N?(olHy`uVBq!ia0y~yU=ES4z)+>ez|hdb!0-zw z)bN6Vq11qZ;Z*_ygVhWM2JwP9y8>;15^MoJA+7=j3IG59x8b;S7%0M+BQ;SOya| Document produit le 2026-05-09 dans le cadre du plan directeur +> `REDOX_COSMIC_XWAYLAND_RS_PLAN.md`. +> +> **Périmètre** : encapsuler `inputd` proprement, exposer un enum +> `InputEvent` neutre côté compositor, valider runtime sur Redox bootée +> avec injection d'events keyboard/mouse via le monitor QEMU. + +## Verdict global + +**✅ Pipeline input complet validé runtime.** + +Le binaire `redox-wl-test-input` a : +1. Ouvert le display via `RedoxOutput` (VT alloué = 2) +2. Activé le VT via `inputd -A` +3. Pris le CRTC et peint un fond bleu marine `0xFF103060` (preuve display) +4. Créé un `InputBackend` partageant le même `ConsumerHandle` +5. Pollé en boucle pendant 30 s, traduisant chaque event en `InputEvent` +6. Logué chaque event sur `/scheme/debug` (capté côté host via serial QEMU) + +20 events injectés via le monitor QEMU (`sendkey`, `mouse_button`) ont +tous été reçus, avec les **modificateurs interprétés correctement par la +keymap** Redox (shift+a → `'A'`, ctrl+c → ctrl press + `c` press). + +Capture preuve display : ![](phase5-blue-screen-with-input.png) + +## Architecture + +``` +┌─────────────────────────────────────────┐ +│ binaire compositor │ +│ │ +│ ┌──────────────┐ Arc │ +│ │ RedoxOutput │ ─────┐ │ +│ │ (display) │ ▼ │ +│ └──────────────┘ ┌─────────────────┐ │ +│ │ InputBackend │ │ +│ │ poll() ────────┼──▶ Vec +│ └─────────────────┘ │ +└─────────────────────────────────────────┘ + │ + ▼ + /scheme/input/consumer (inputd, un seul VT, un seul fd) +``` + +**Décision architecturale clé** : un seul `ConsumerHandle` partagé via +`Arc`, pour 2 raisons : + +1. **Un consumer = un VT**. Si on en ouvrait deux, on aurait deux VTs + distincts et inputd routerait les events vers le VT actif (un seul + à la fois). Le second consumer serait perpétuellement muet. +2. **`RedoxOutput` doit garder le consumer en vie** pour préserver le VT + (cf phase 4 — sinon `inputd -A ` retourne "non-existent"). Mais + c'est `InputBackend` qui consomme les events. Le partage Arc résout + cette tension. + +## API publique + +### `redox-wl-input::InputEvent` + +```rust +pub enum InputEvent { + Key { character: char, scancode: u8, pressed: bool }, + TextInput { character: char }, + PointerMotion { x: i32, y: i32 }, // absolu + PointerMotionRelative { dx: i32, dy: i32 }, + PointerButton { left: bool, middle: bool, right: bool }, + PointerScroll { dx: i32, dy: i32 }, + Quit, + Unhandled { code: i64, a: i64, b: i64 }, // events orbclient pas encore mappés + Handoff, // VT switch côté inputd +} +``` + +L'enum est volontairement **agnostique vis-à-vis de Wayland**. Un +compositor Wayland le mappera en `wl_keyboard.key`, `wl_pointer.motion`, +etc. ; un autre frontend pourra le mapper différemment. + +### `redox-wl-input::InputBackend` + +```rust +impl InputBackend { + pub fn new(consumer: Arc) -> Self; + pub fn event_fd(&self) -> BorrowedFd<'_>; // pour subscribe à une EventQueue + pub fn event_raw_fd(&self) -> i32; + pub fn poll(&self) -> io::Result>; +} +``` + +`poll()` est non-bloquant et draine tous les events disponibles. À +intégrer plus tard dans une `event::EventQueue` Redox pour event-driven. + +## Validation runtime — extrait du log + +Injection via le monitor QEMU : +```bash +for k in a b c ret; do printf "sendkey $k\n" | ncat -U /tmp/qmp.sock; done +printf "sendkey shift-a\n" | ncat -U /tmp/qmp.sock +printf "sendkey ctrl-c\n" | ncat -U /tmp/qmp.sock +printf "sendkey esc\n" | ncat -U /tmp/qmp.sock +printf "mouse_button 1\n" | ncat -U /tmp/qmp.sock +printf "mouse_button 0\n" | ncat -U /tmp/qmp.sock +``` + +Sortie côté binaire (lue via /scheme/debug → serial → stdout host) : +``` +[input] display 1280x800, VT=2 +[input] CRTC pris, fond bleu marine peint +[input] InputBackend prêt, fd events=4 +[input] #0001 Key 'a' scan=0x1e PRESS +[input] #0002 Key 'a' scan=0x1e RELEASE +[input] #0003 Key 'b' scan=0x30 PRESS +[input] #0004 Key 'b' scan=0x30 RELEASE +[input] #0005 Key 'c' scan=0x2e PRESS +[input] #0006 Key 'c' scan=0x2e RELEASE +[input] #0007 Key '·' scan=0x1c PRESS ← Enter +[input] #0008 Key '·' scan=0x1c RELEASE +[input] #0009 Key '·' scan=0x2a PRESS ← Shift down +[input] #0010 Key 'A' scan=0x1e PRESS ← keymap → 'A' (uppercase) +[input] #0011 Key 'A' scan=0x1e RELEASE +[input] #0012 Key '·' scan=0x2a RELEASE ← Shift up +[input] #0013 Key '·' scan=0x1d PRESS ← Ctrl down +[input] #0014 Key 'c' scan=0x2e PRESS +[input] #0015 Key 'c' scan=0x2e RELEASE +[input] #0016 Key '·' scan=0x1d RELEASE ← Ctrl up +[input] #0017 Key '·' scan=0x01 PRESS ← Esc +[input] #0018 Key '·' scan=0x01 RELEASE +[input] #0019 PointerButton L=true M=false R=false +[input] #0020 PointerButton L=false M=false R=false +``` + +Note : les caractères `'·'` représentent `'\0'` (touche non-imprimable +sans char associé : modificateurs, Enter, Esc...). Le scancode reste +disponible pour l'interpréter. + +## Limitations connues + +### Pas de `PointerMotion` testé +Le test n'a pas reçu d'event `PointerMotion` parce que la commande QEMU +utilisée n'incluait pas `-device usb-tablet` (qui fournit des coords +absolues). Avec `make qemu` standard, ce device est inclus. À retester +quand on aura un cas d'usage souris dans le compositor. + +### Mouse relatif via PS/2 non testé +Pareil, dépend de `-device ps2` ou similaire. + +### Doublement des lignes dans le log +Chaque ligne `[input]` apparaît deux fois dans le log capturé. Cause : +le `DebugSink` fait `println!` (→ stdout → fbcond → framebuffer → +captured-by-screendump-not-serial) ET `writeln!(/scheme/debug)` +(→ serial → captured-by-host-stdio). Les deux finissent par converger +dans le serial mux stdio du `make qemu`. Cosmétique, pas critique. + +## Ce qui est prêt pour la suite + +- Phase 6 (composition multi-surfaces shm) : peut consommer `InputEvent` + pour focus/clic-pour-amener-au-premier-plan +- Phase 7 (compositor utilisable) : peut wrapper `InputBackend` dans + une `EventQueue` Redox + traiter raccourcis globaux + +## Code source + +``` +crates/redox-wl-input/ # lib (140 lignes) +├── Cargo.toml +└── src/lib.rs # InputEvent + InputBackend + +crates/redox-wl-test-input/ # bin de test (130 lignes) +├── Cargo.toml +└── src/main.rs # display bleu + poll loop + log + +crates/redox-wl-display/src/lib.rs # MODIFIÉ + # _consumer → consumer: Arc + # + pub fn consumer() -> Arc +``` + +## Méthode de test reproductible + +Image patchée : `/usr/lib/init.d/20_orbital` lance +`redox-wl-test-input` en VT=2, `30_console` vidé pour ne pas créer de +getty conflictuel. + +```bash +qemu-system-x86_64 \ + ... \ + -chardev stdio,id=debug,signal=off,mux=on \ + -serial chardev:debug -mon chardev=debug \ + -monitor unix:/tmp/qmp.sock,server,nowait \ + -vga std -display none ... & +sleep 2 && printf "sendkey ret\n" | ncat -U /tmp/qmp.sock # bootloader +sleep 13 # boot + paint +printf "sendkey a\n" | ncat -U /tmp/qmp.sock # inject events +printf "mouse_button 1\n" | ncat -U /tmp/qmp.sock +printf "screendump /tmp/x.ppm\n" | ncat -U /tmp/qmp.sock # capture visuel +``` + +L'image a été restaurée à boot Orbital normal après la session. + +--- + +*Fin du document de phase 5.*