Skip to main content

pixelflux/wayland/
eiclient.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7//! libei (EIS) input injection for the portal host-capture rung.
8//!
9//! GNOME and KDE hand a RemoteDesktop session an EIS socket through
10//! `ConnectToEIS`; this drives keyboard, pointer, and touch over that socket
11//! instead of the portal's `Notify*` D-Bus methods. It is the lower-latency
12//! channel — one socket write per event, no bus round trip — and the one the
13//! backends develop, so it is preferred wherever the portal offers it. The
14//! frontend makes the choice permanent: once `ConnectToEIS` succeeds the
15//! `Notify*` methods are refused for the life of the session, so the injector
16//! is taken only after its handshake binds a usable device, and a portal that
17//! never answers `ConnectToEIS` keeps the `Notify*` path (`portal.rs`).
18//!
19//! All socket I/O runs on one thread: it completes the handshake, binds the
20//! seat's capabilities, tracks the devices the server creates, and emits queued
21//! events, waking from a pipe the moment an event is queued so injection does
22//! not wait on a poll tick. Keyboard keys arrive as xkb keycodes under selkies'
23//! managed keymap; the compositor's own keymap (delivered over EIS) is
24//! read-only here, so a key's base keysym is resolved against it and the
25//! modifiers its level needs are synthesized, with a raw-keycode fallback for a
26//! keysym that keymap does not carry.
27
28use std::collections::HashMap;
29use std::io::ErrorKind;
30use std::os::fd::{AsRawFd, OwnedFd};
31use std::sync::atomic::{AtomicBool, Ordering};
32use std::sync::{Arc, Mutex};
33use std::time::{SystemTime, UNIX_EPOCH};
34
35use reis::PendingRequestResult;
36use reis::ei;
37use reis::event::{DeviceCapability, EiEvent, EiEventConverter};
38use reis::handshake::ei_handshake_blocking;
39
40use crate::wayland::keymap::{KeymapPolicy, compile_keymap};
41use crate::wayland::wlclient::wake_pipe;
42
43/// evdev keycodes carry an 8-count offset from xkb keycodes.
44const EVDEV_OFFSET: u32 = 8;
45
46/// The worker's one-shot readiness report to `spawn`: ready once a device bound, else the reason
47/// the session yielded no injector.
48type Ready = Arc<(Mutex<Option<Result<(), String>>>, std::sync::Condvar)>;
49
50/// One injection request, produced on selkies' input threads and drained on the EIS thread.
51enum Cmd {
52    MotionAbsolute {
53        x: f32,
54        y: f32,
55    },
56    MotionRelative {
57        dx: f32,
58        dy: f32,
59    },
60    Button {
61        button: u32,
62        pressed: bool,
63    },
64    Scroll {
65        dx: f32,
66        dy: f32,
67        finish: bool,
68    },
69    ScrollDiscrete {
70        x: i32,
71        y: i32,
72    },
73    /// A key by xkb keycode, with the base keysym selkies resolved for it (used to place the
74    /// key in the compositor's own keymap), or `None` for a key without one.
75    Key {
76        xkb_keycode: u32,
77        keysym: Option<u32>,
78        pressed: bool,
79    },
80}
81
82/// A handle to a live EIS session: input methods push commands and wake the worker.
83pub(crate) struct EiInjector {
84    queue: Arc<Mutex<Vec<Cmd>>>,
85    wake: OwnedFd,
86    alive: Arc<AtomicBool>,
87}
88
89impl EiInjector {
90    /// Bring up an injector over an EIS socket (from the portal's `ConnectToEIS`), returning it
91    /// only once the handshake has bound a keyboard or a pointer. The worker thread owns the
92    /// socket for the session's life.
93    pub(crate) fn spawn(fd: OwnedFd) -> Result<Self, String> {
94        let stream = std::os::unix::net::UnixStream::from(fd);
95        let context = ei::Context::new(stream).map_err(|e| format!("EIS context: {e}"))?;
96        let (wake_rd, wake_wr) = wake_pipe()?;
97        let queue = Arc::new(Mutex::new(Vec::new()));
98        let alive = Arc::new(AtomicBool::new(true));
99        let ready = Arc::new((
100            Mutex::new(None::<Result<(), String>>),
101            std::sync::Condvar::new(),
102        ));
103
104        let (q, a, r) = (queue.clone(), alive.clone(), ready.clone());
105        std::thread::Builder::new()
106            .name("pf-host-eis".into())
107            .spawn(move || Worker::run(context, wake_rd, q, a, r))
108            .map_err(|e| format!("spawn EIS thread: {e}"))?;
109
110        let (lock, cvar) = &*ready;
111        let mut slot = lock.lock().unwrap();
112        while slot.is_none() {
113            slot = cvar.wait(slot).unwrap();
114        }
115        slot.take().unwrap()?;
116        Ok(Self {
117            queue,
118            wake: wake_wr,
119            alive,
120        })
121    }
122
123    pub(crate) fn alive(&self) -> bool {
124        self.alive.load(Ordering::Relaxed)
125    }
126
127    fn push(&self, cmd: Cmd) {
128        if !self.alive() {
129            return;
130        }
131        self.queue.lock().unwrap().push(cmd);
132        let _ = nix_write(self.wake.as_raw_fd());
133    }
134
135    pub(crate) fn pointer_motion_abs(&self, x: f64, y: f64) {
136        self.push(Cmd::MotionAbsolute {
137            x: x as f32,
138            y: y as f32,
139        });
140    }
141
142    pub(crate) fn pointer_motion(&self, dx: f64, dy: f64) {
143        self.push(Cmd::MotionRelative {
144            dx: dx as f32,
145            dy: dy as f32,
146        });
147    }
148
149    pub(crate) fn pointer_button(&self, button: i32, pressed: bool) {
150        self.push(Cmd::Button {
151            button: button as u32,
152            pressed,
153        });
154    }
155
156    pub(crate) fn pointer_axis(&self, dx: f64, dy: f64, finish: bool) {
157        self.push(Cmd::Scroll {
158            dx: dx as f32,
159            dy: dy as f32,
160            finish,
161        });
162    }
163
164    pub(crate) fn pointer_axis_discrete(&self, axis: u32, steps: i32) {
165        // EIS scroll_discrete counts in 120ths of a wheel notch on each axis, like wl_pointer.
166        let v = steps * 120;
167        let (x, y) = if axis == 0 { (0, v) } else { (v, 0) };
168        self.push(Cmd::ScrollDiscrete { x, y });
169    }
170
171    pub(crate) fn key(&self, xkb_keycode: u32, keysym: Option<u32>, pressed: bool) {
172        self.push(Cmd::Key {
173            xkb_keycode,
174            keysym,
175            pressed,
176        });
177    }
178}
179
180/// A wake byte on the pipe; a full pipe already carries the pending signal.
181fn nix_write(fd: i32) -> std::io::Result<()> {
182    let one = [1u8];
183    let n = unsafe { libc::write(fd, one.as_ptr() as *const libc::c_void, 1) };
184    if n < 0 {
185        let e = std::io::Error::last_os_error();
186        if e.kind() != ErrorKind::WouldBlock {
187            return Err(e);
188        }
189    }
190    Ok(())
191}
192
193/// One device the server created: its proto object and the emulation state we keep for it.
194struct Device {
195    device: ei::Device,
196    resumed: bool,
197    emulating: bool,
198    sequence: u32,
199    pointer: Option<ei::Pointer>,
200    pointer_abs: Option<ei::PointerAbsolute>,
201    button: Option<ei::Button>,
202    scroll: Option<ei::Scroll>,
203    keyboard: Option<ei::Keyboard>,
204    keymap: Option<KeymapPolicy>,
205}
206
207struct Worker {
208    context: ei::Context,
209    converter: EiEventConverter,
210    devices: Vec<Device>,
211    last_serial: u32,
212    /// Held modifier evdev keycodes and their press count, so a key that synthesized a modifier
213    /// releases exactly what it added.
214    mod_refs: HashMap<u32, u32>,
215    /// Per xkb keycode, the evdev key and synthesized modifiers its press emitted, replayed on
216    /// release so a mid-stream keymap read cannot desync it.
217    pressed: HashMap<u32, (u32, Vec<u32>)>,
218}
219
220impl Worker {
221    fn run(
222        context: ei::Context,
223        wake_rd: OwnedFd,
224        queue: Arc<Mutex<Vec<Cmd>>>,
225        alive: Arc<AtomicBool>,
226        ready: Ready,
227    ) {
228        let resp = match ei_handshake_blocking(
229            &context,
230            "pixelflux",
231            ei::handshake::ContextType::Sender,
232        ) {
233            Ok(resp) => resp,
234            Err(e) => {
235                Self::signal(&ready, Err(format!("EIS handshake: {e}")));
236                alive.store(false, Ordering::Relaxed);
237                return;
238            }
239        };
240        let mut worker = Worker {
241            last_serial: resp.serial,
242            converter: EiEventConverter::new(&context, resp),
243            context,
244            devices: Vec::new(),
245            mod_refs: HashMap::new(),
246            pressed: HashMap::new(),
247        };
248        let _ = worker.context.flush();
249
250        // Give the server its round to advertise the seat and create devices before reporting
251        // what bound, so the caller's rung decision sees the real capability set.
252        let deadline = std::time::Instant::now() + std::time::Duration::from_millis(1500);
253        let mut reported = false;
254        loop {
255            if !reported && (worker.has(|d| d.resumed) || std::time::Instant::now() >= deadline) {
256                let bound = worker.devices.iter().any(|d| {
257                    d.keyboard.is_some() || d.pointer.is_some() || d.pointer_abs.is_some()
258                });
259                if !bound {
260                    Self::signal(
261                        &ready,
262                        Err("EIS session bound no keyboard or pointer".into()),
263                    );
264                    alive.store(false, Ordering::Relaxed);
265                    return;
266                }
267                Self::signal(&ready, Ok(()));
268                reported = true;
269            }
270            let timeout = if reported { -1 } else { 100 };
271            if !worker.poll(wake_rd.as_raw_fd(), timeout) {
272                break;
273            }
274            if worker.pump_socket().is_err() {
275                break;
276            }
277            if reported {
278                worker.drain(&queue);
279            }
280        }
281        alive.store(false, Ordering::Relaxed);
282        if !reported {
283            Self::signal(&ready, Err("EIS session closed before it was ready".into()));
284        }
285    }
286
287    fn signal(ready: &Ready, result: Result<(), String>) {
288        let (lock, cvar) = &**ready;
289        *lock.lock().unwrap() = Some(result);
290        cvar.notify_all();
291    }
292
293    fn has(&self, f: impl Fn(&Device) -> bool) -> bool {
294        self.devices.iter().any(f)
295    }
296
297    /// Block until the socket or the wake pipe is readable (or `timeout` ms elapses, `-1` for no
298    /// limit); `false` on a poll error that should end the session.
299    fn poll(&self, wake_fd: i32, timeout: i32) -> bool {
300        let mut fds = [
301            libc::pollfd {
302                fd: self.context.as_raw_fd(),
303                events: libc::POLLIN,
304                revents: 0,
305            },
306            libc::pollfd {
307                fd: wake_fd,
308                events: libc::POLLIN,
309                revents: 0,
310            },
311        ];
312        let n = unsafe { libc::poll(fds.as_mut_ptr(), 2, timeout) };
313        if n < 0 {
314            return std::io::Error::last_os_error().kind() == ErrorKind::Interrupted;
315        }
316        let broken = libc::POLLHUP | libc::POLLERR | libc::POLLNVAL;
317        // The wake pipe's write end lives in the injector handle: its hang-up is the session
318        // being torn down, and a broken socket ends the session too.
319        if (fds[0].revents | fds[1].revents) & broken != 0 {
320            return false;
321        }
322        if fds[1].revents & libc::POLLIN != 0 {
323            let mut buf = [0u8; 64];
324            while unsafe { libc::read(wake_fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) }
325                == buf.len() as isize
326            {}
327        }
328        true
329    }
330
331    /// Read and dispatch whatever the server sent; `Err` on a closed or broken socket.
332    fn pump_socket(&mut self) -> Result<(), ()> {
333        match self.context.read() {
334            Ok(_) => {}
335            Err(e) if e.kind() == ErrorKind::WouldBlock => return Ok(()),
336            Err(_) => return Err(()),
337        }
338        while let Some(result) = self.context.pending_event() {
339            match result {
340                PendingRequestResult::Request(event) => {
341                    if self.converter.handle_event(event).is_err() {
342                        return Err(());
343                    }
344                }
345                PendingRequestResult::ParseError(_) => return Err(()),
346                PendingRequestResult::InvalidObject(_) => {}
347            }
348        }
349        while let Some(event) = self.converter.next_event() {
350            self.handle(event);
351        }
352        Ok(())
353    }
354
355    fn handle(&mut self, event: EiEvent) {
356        match event {
357            EiEvent::SeatAdded(evt) => {
358                evt.seat.bind_capabilities(
359                    DeviceCapability::Pointer
360                        | DeviceCapability::PointerAbsolute
361                        | DeviceCapability::Keyboard
362                        | DeviceCapability::Button
363                        | DeviceCapability::Scroll
364                        | DeviceCapability::Touch,
365                );
366                let _ = self.context.flush();
367            }
368            EiEvent::DeviceAdded(evt) => {
369                let device = evt.device;
370                let keymap = device
371                    .keymap()
372                    .and_then(keymap_policy)
373                    .filter(|_| device.interface::<ei::Keyboard>().is_some());
374                self.devices.push(Device {
375                    pointer: device.interface(),
376                    pointer_abs: device.interface(),
377                    button: device.interface(),
378                    scroll: device.interface(),
379                    keyboard: device.interface(),
380                    keymap,
381                    device: device.device().clone(),
382                    resumed: false,
383                    emulating: false,
384                    sequence: 0,
385                });
386            }
387            EiEvent::DeviceRemoved(evt) => {
388                self.devices.retain(|d| d.device != *evt.device.device());
389            }
390            EiEvent::DevicePaused(evt) => {
391                if let Some(d) = self.find_mut(&evt.device) {
392                    d.resumed = false;
393                    d.emulating = false;
394                }
395            }
396            EiEvent::DeviceResumed(evt) => {
397                self.last_serial = evt.serial;
398                if let Some(d) = self.find_mut(&evt.device) {
399                    d.resumed = true;
400                    d.emulating = false;
401                }
402            }
403            EiEvent::KeyboardModifiers(evt) => {
404                self.last_serial = evt.serial;
405            }
406            EiEvent::Disconnected(_) => {
407                self.devices.clear();
408            }
409            _ => {}
410        }
411    }
412
413    fn find_mut(&mut self, device: &reis::event::Device) -> Option<&mut Device> {
414        self.devices
415            .iter_mut()
416            .find(|d| d.device == *device.device())
417    }
418
419    fn drain(&mut self, queue: &Arc<Mutex<Vec<Cmd>>>) {
420        let cmds = std::mem::take(&mut *queue.lock().unwrap());
421        for cmd in cmds {
422            self.emit(cmd);
423        }
424        let _ = self.context.flush();
425    }
426
427    fn emit(&mut self, cmd: Cmd) {
428        match cmd {
429            Cmd::MotionAbsolute { x, y } => {
430                if let Some(i) = self.ready_index(|d| d.pointer_abs.is_some()) {
431                    self.start(i);
432                    self.devices[i]
433                        .pointer_abs
434                        .as_ref()
435                        .unwrap()
436                        .motion_absolute(x, y);
437                    self.frame(i);
438                }
439            }
440            Cmd::MotionRelative { dx, dy } => {
441                if let Some(i) = self.ready_index(|d| d.pointer.is_some()) {
442                    self.start(i);
443                    self.devices[i]
444                        .pointer
445                        .as_ref()
446                        .unwrap()
447                        .motion_relative(dx, dy);
448                    self.frame(i);
449                }
450            }
451            Cmd::Button { button, pressed } => {
452                if let Some(i) = self.ready_index(|d| d.button.is_some()) {
453                    self.start(i);
454                    self.devices[i]
455                        .button
456                        .as_ref()
457                        .unwrap()
458                        .button(button, button_state(pressed));
459                    self.frame(i);
460                }
461            }
462            Cmd::Scroll { dx, dy, finish } => {
463                if let Some(i) = self.ready_index(|d| d.scroll.is_some()) {
464                    self.start(i);
465                    let scroll = self.devices[i].scroll.as_ref().unwrap();
466                    if dx != 0.0 || dy != 0.0 {
467                        scroll.scroll(dx, dy);
468                    }
469                    if finish {
470                        scroll.scroll_stop(1, 1, 0);
471                    }
472                    self.frame(i);
473                }
474            }
475            Cmd::ScrollDiscrete { x, y } => {
476                if let Some(i) = self.ready_index(|d| d.scroll.is_some()) {
477                    self.start(i);
478                    self.devices[i]
479                        .scroll
480                        .as_ref()
481                        .unwrap()
482                        .scroll_discrete(x, y);
483                    self.frame(i);
484                }
485            }
486            Cmd::Key {
487                xkb_keycode,
488                keysym,
489                pressed,
490            } => self.emit_key(xkb_keycode, keysym, pressed),
491        }
492    }
493
494    fn emit_key(&mut self, xkb_keycode: u32, keysym: Option<u32>, pressed: bool) {
495        let Some(i) = self.ready_index(|d| d.keyboard.is_some()) else {
496            return;
497        };
498        self.start(i);
499        if pressed {
500            let (evdev, mods) = self.resolve_key(i, xkb_keycode, keysym);
501            let kbd = self.devices[i].keyboard.as_ref().unwrap();
502            for &m in &mods {
503                if *self.mod_refs.entry(m).or_insert(0) == 0 {
504                    kbd.key(m, key_state(true));
505                }
506                *self.mod_refs.get_mut(&m).unwrap() += 1;
507            }
508            kbd.key(evdev, key_state(true));
509            self.pressed.insert(xkb_keycode, (evdev, mods));
510        } else if let Some((evdev, mods)) = self.pressed.remove(&xkb_keycode) {
511            let kbd = self.devices[i].keyboard.as_ref().unwrap();
512            kbd.key(evdev, key_state(false));
513            for m in mods.into_iter().rev() {
514                let count = self
515                    .mod_refs
516                    .get(&m)
517                    .copied()
518                    .unwrap_or(0)
519                    .saturating_sub(1);
520                if count == 0 {
521                    self.mod_refs.remove(&m);
522                    kbd.key(m, key_state(false));
523                } else {
524                    self.mod_refs.insert(m, count);
525                }
526            }
527        }
528        self.frame(i);
529    }
530
531    /// The evdev keycode and modifier evdev keycodes to press this key with: its base keysym
532    /// placed in the compositor's own keymap where that keymap carries it, else the raw keycode.
533    fn resolve_key(&self, index: usize, xkb_keycode: u32, keysym: Option<u32>) -> (u32, Vec<u32>) {
534        let raw = xkb_keycode.saturating_sub(EVDEV_OFFSET);
535        let (Some(sym), Some(policy)) = (keysym, self.devices[index].keymap.as_ref()) else {
536            return (raw, Vec::new());
537        };
538        match policy.resolve(sym) {
539            Some((kc, level)) => {
540                let mut mods = Vec::new();
541                if level & 1 != 0 {
542                    mods.push(policy_shift(policy));
543                }
544                if level & 2 != 0 {
545                    mods.push(policy_altgr(policy));
546                }
547                (
548                    kc.saturating_sub(EVDEV_OFFSET),
549                    mods.into_iter().flatten().collect(),
550                )
551            }
552            None => (raw, Vec::new()),
553        }
554    }
555
556    /// The lowest-indexed resumed device matching `want`.
557    fn ready_index(&self, want: impl Fn(&Device) -> bool) -> Option<usize> {
558        self.devices.iter().position(|d| d.resumed && want(d))
559    }
560
561    fn start(&mut self, index: usize) {
562        let d = &mut self.devices[index];
563        if !d.emulating {
564            d.device.start_emulating(self.last_serial, d.sequence);
565            d.sequence = d.sequence.wrapping_add(1);
566            d.emulating = true;
567        }
568    }
569
570    fn frame(&mut self, index: usize) {
571        self.devices[index]
572            .device
573            .frame(self.last_serial, now_micros());
574    }
575}
576
577/// The Shift keycode of a keymap, as an evdev code, if it carries one.
578fn policy_shift(policy: &KeymapPolicy) -> Option<u32> {
579    policy
580        .resolve(0xffe1)
581        .map(|(kc, _)| kc.saturating_sub(EVDEV_OFFSET))
582}
583
584/// The ISO_Level3_Shift (AltGr) keycode of a keymap, as an evdev code, if it carries one.
585fn policy_altgr(policy: &KeymapPolicy) -> Option<u32> {
586    policy
587        .resolve(0xfe03)
588        .map(|(kc, _)| kc.saturating_sub(EVDEV_OFFSET))
589}
590
591/// Compile the compositor's keymap into a resolver over its base layout. The mmap is read once;
592/// a keymap that does not compile leaves keys to the raw-keycode path.
593fn keymap_policy(keymap: &reis::event::Keymap) -> Option<KeymapPolicy> {
594    let map = unsafe {
595        memmap2::MmapOptions::new()
596            .len(keymap.size as usize)
597            .map(keymap.fd.as_raw_fd())
598            .ok()?
599    };
600    let text = std::ffi::CStr::from_bytes_until_nul(&map)
601        .ok()?
602        .to_str()
603        .ok()?;
604    policy_from_text(text)
605}
606
607/// A base-layout resolver over keymap text, or `None` when it does not compile.
608fn policy_from_text(text: &str) -> Option<KeymapPolicy> {
609    compile_keymap(text)?;
610    let mut policy = KeymapPolicy::empty();
611    policy.rebuild_base(text.to_string()).then_some(policy)
612}
613
614fn now_micros() -> u64 {
615    SystemTime::now()
616        .duration_since(UNIX_EPOCH)
617        .map(|d| d.as_micros() as u64)
618        .unwrap_or(0)
619}
620
621fn button_state(pressed: bool) -> ei::button::ButtonState {
622    if pressed {
623        ei::button::ButtonState::Press
624    } else {
625        ei::button::ButtonState::Released
626    }
627}
628
629fn key_state(pressed: bool) -> ei::keyboard::KeyState {
630    if pressed {
631        ei::keyboard::KeyState::Press
632    } else {
633        ei::keyboard::KeyState::Released
634    }
635}
636
637#[cfg(test)]
638mod tests {
639    use super::*;
640    use crate::wayland::keymap::compile_rmlvo;
641
642    /// A base keysym present in the compositor keymap resolves to that keymap's keycode with the
643    /// modifiers its level needs; a Shift-level glyph carries the keymap's own Shift keycode, and
644    /// a keysym the keymap lacks leaves the raw keycode untouched.
645    #[test]
646    fn keyboard_resolves_against_the_host_keymap() {
647        let text = compile_rmlvo("", "", "us", "", "").expect("us keymap");
648        let policy = policy_from_text(&text).expect("policy");
649        // 'a' sits at level 0 on AD01's neighbor; take it from the keymap itself.
650        let (a_kc, a_level) = policy.resolve(0x61).expect("a resolves");
651        assert_eq!(a_level, 0, "lowercase is unshifted");
652        // 'A' is the same key one level up, so it needs Shift.
653        let (upper_kc, upper_level) = policy.resolve(0x41).expect("A resolves");
654        assert_eq!(upper_kc, a_kc, "same physical key");
655        assert_eq!(upper_level & 1, 1, "uppercase needs Shift");
656        // The keymap carries a Shift keycode, returned as an evdev code (xkb - 8).
657        let shift = policy_shift(&policy).expect("shift keycode");
658        assert!(shift >= 1, "a real evdev keycode");
659        // A keysym US cannot type has no base hit.
660        assert_eq!(
661            policy.resolve(0x0439),
662            None,
663            "Cyrillic short-i absent from us"
664        );
665    }
666
667    /// The injector, run against a minimal in-process EIS server, emits the protocol a
668    /// compositor expects: it binds the seat, starts emulating before the first event, and
669    /// sends absolute motion, buttons, keys, and discrete scroll with the values handed to it,
670    /// each committed by a frame.
671    #[test]
672    fn injector_emits_the_expected_eis_protocol() {
673        use reis::eis;
674        use reis::handshake::EisHandshaker;
675        use reis::request::{EisRequest, EisRequestConverter};
676        use std::os::fd::OwnedFd;
677        use std::os::unix::net::UnixStream;
678        use std::time::Duration;
679
680        let (server_sock, client_sock) = UnixStream::pair().unwrap();
681        let seen = Arc::new(Mutex::new(Vec::<String>::new()));
682        let recorded = seen.clone();
683
684        let server = std::thread::spawn(move || {
685            let ctx = eis::Context::new(server_sock).unwrap();
686            let mut handshaker = EisHandshaker::new(&ctx, 1);
687            let resp = 'hs: loop {
688                if ctx.read().is_err() {
689                    return;
690                }
691                while let Some(pr) = ctx.pending_request() {
692                    if let PendingRequestResult::Request(req) = pr
693                        && let Ok(Some(resp)) = handshaker.handle_request(req)
694                    {
695                        break 'hs resp;
696                    }
697                }
698            };
699            let mut converter = EisRequestConverter::new(&ctx, resp, 1);
700            let handle = converter.handle().clone();
701            let seat = handle.add_seat(
702                Some("test"),
703                DeviceCapability::Pointer
704                    | DeviceCapability::PointerAbsolute
705                    | DeviceCapability::Button
706                    | DeviceCapability::Scroll
707                    | DeviceCapability::Keyboard,
708            );
709            let _ = handle.flush();
710            let mut device = None;
711            loop {
712                if ctx.read().is_err() {
713                    return;
714                }
715                while let Some(pr) = ctx.pending_request() {
716                    if let PendingRequestResult::Request(req) = pr {
717                        let _ = converter.handle_request(req);
718                    }
719                }
720                while let Some(req) = converter.next_request() {
721                    let mut log = recorded.lock().unwrap();
722                    match req {
723                        EisRequest::Bind(b) if device.is_none() => {
724                            let dev = seat.add_device(
725                                Some("virt"),
726                                eis::device::DeviceType::Virtual,
727                                b.capabilities,
728                                |_| {},
729                            );
730                            dev.resumed();
731                            let _ = handle.flush();
732                            device = Some(dev);
733                        }
734                        EisRequest::DeviceStartEmulating(_) => log.push("start".into()),
735                        EisRequest::PointerMotionAbsolute(m) => {
736                            log.push(format!(
737                                "abs {} {}",
738                                m.dx_absolute as i32, m.dy_absolute as i32
739                            ));
740                        }
741                        EisRequest::Button(b) => log.push(format!(
742                            "button {} {}",
743                            b.button,
744                            matches!(b.state, eis::button::ButtonState::Press)
745                        )),
746                        EisRequest::KeyboardKey(k) => log.push(format!(
747                            "key {} {}",
748                            k.key,
749                            matches!(k.state, eis::keyboard::KeyState::Press)
750                        )),
751                        EisRequest::ScrollDiscrete(s) => {
752                            log.push(format!("scroll {} {}", s.discrete_dx, s.discrete_dy));
753                        }
754                        _ => {}
755                    }
756                }
757            }
758        });
759
760        let injector =
761            EiInjector::spawn(OwnedFd::from(client_sock)).expect("injector binds a device");
762        injector.pointer_motion_abs(400.0, 300.0);
763        injector.pointer_button(0x110, true);
764        injector.pointer_button(0x110, false);
765        injector.key(38, None, true);
766        injector.key(38, None, false);
767        injector.pointer_axis_discrete(0, 1);
768        std::thread::sleep(Duration::from_millis(400));
769        drop(injector);
770        server.join().unwrap();
771
772        let got = seen.lock().unwrap().clone();
773        assert!(
774            got.contains(&"start".to_string()),
775            "start_emulating: {got:?}"
776        );
777        assert!(
778            got.contains(&"abs 400 300".to_string()),
779            "absolute motion: {got:?}"
780        );
781        assert!(
782            got.contains(&"button 272 true".to_string()),
783            "left press: {got:?}"
784        );
785        assert!(
786            got.contains(&"button 272 false".to_string()),
787            "left release: {got:?}"
788        );
789        assert!(
790            got.contains(&"key 30 true".to_string()),
791            "key press: {got:?}"
792        );
793        assert!(
794            got.contains(&"key 30 false".to_string()),
795            "key release: {got:?}"
796        );
797        assert!(
798            got.contains(&"scroll 0 120".to_string()),
799            "discrete scroll: {got:?}"
800        );
801    }
802}