mirror of
https://github.com/weyne85/rustdesk.git
synced 2025-10-29 17:00:05 +00:00
1114 lines
43 KiB
Rust
1114 lines
43 KiB
Rust
use crate::ipc::{self, new_listener, Connection, Data, DataKeyboard, DataMouse};
|
|
use enigo::{Key, KeyboardControllable, MouseButton, MouseControllable};
|
|
use evdev::{
|
|
uinput::{VirtualDevice, VirtualDeviceBuilder},
|
|
AttributeSet, EventType, InputEvent,
|
|
};
|
|
use hbb_common::{
|
|
allow_err, bail, log,
|
|
tokio::{self, runtime::Runtime},
|
|
ResultType,
|
|
};
|
|
|
|
static IPC_CONN_TIMEOUT: u64 = 1000;
|
|
static IPC_REQUEST_TIMEOUT: u64 = 1000;
|
|
static IPC_POSTFIX_KEYBOARD: &str = "_uinput_keyboard";
|
|
static IPC_POSTFIX_MOUSE: &str = "_uinput_mouse";
|
|
static IPC_POSTFIX_CONTROL: &str = "_uinput_control";
|
|
|
|
pub mod client {
|
|
use super::*;
|
|
|
|
pub struct UInputKeyboard {
|
|
conn: Connection,
|
|
rt: Runtime,
|
|
}
|
|
|
|
impl UInputKeyboard {
|
|
pub async fn new() -> ResultType<Self> {
|
|
let conn = ipc::connect(IPC_CONN_TIMEOUT, IPC_POSTFIX_KEYBOARD).await?;
|
|
let rt = Runtime::new()?;
|
|
Ok(Self { conn, rt })
|
|
}
|
|
|
|
fn send(&mut self, data: Data) -> ResultType<()> {
|
|
self.rt.block_on(self.conn.send(&data))
|
|
}
|
|
|
|
fn send_get_key_state(&mut self, data: Data) -> ResultType<bool> {
|
|
self.rt.block_on(self.conn.send(&data))?;
|
|
|
|
match self
|
|
.rt
|
|
.block_on(self.conn.next_timeout(IPC_REQUEST_TIMEOUT))
|
|
{
|
|
Ok(Some(Data::KeyboardResponse(ipc::DataKeyboardResponse::GetKeyState(state)))) => {
|
|
Ok(state)
|
|
}
|
|
Ok(Some(resp)) => {
|
|
// FATAL error!!!
|
|
bail!(
|
|
"FATAL error, wait keyboard result other response: {:?}",
|
|
&resp
|
|
);
|
|
}
|
|
Ok(None) => {
|
|
// FATAL error!!!
|
|
// Maybe wait later
|
|
bail!("FATAL error, wait keyboard result, receive None",);
|
|
}
|
|
Err(e) => {
|
|
// FATAL error!!!
|
|
bail!(
|
|
"FATAL error, wait keyboard result timeout {}, {}",
|
|
&e,
|
|
IPC_REQUEST_TIMEOUT
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl KeyboardControllable for UInputKeyboard {
|
|
fn as_any(&self) -> &dyn std::any::Any {
|
|
self
|
|
}
|
|
|
|
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
|
|
self
|
|
}
|
|
|
|
fn get_key_state(&mut self, key: Key) -> bool {
|
|
match self.send_get_key_state(Data::Keyboard(DataKeyboard::GetKeyState(key))) {
|
|
Ok(state) => state,
|
|
Err(e) => {
|
|
// unreachable!()
|
|
log::error!("Failed to get key state {}", &e);
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
fn key_sequence(&mut self, sequence: &str) {
|
|
allow_err!(self.send(Data::Keyboard(DataKeyboard::Sequence(sequence.to_string()))));
|
|
}
|
|
|
|
// TODO: handle error???
|
|
fn key_down(&mut self, key: Key) -> enigo::ResultType {
|
|
allow_err!(self.send(Data::Keyboard(DataKeyboard::KeyDown(key))));
|
|
Ok(())
|
|
}
|
|
fn key_up(&mut self, key: Key) {
|
|
allow_err!(self.send(Data::Keyboard(DataKeyboard::KeyUp(key))));
|
|
}
|
|
fn key_click(&mut self, key: Key) {
|
|
allow_err!(self.send(Data::Keyboard(DataKeyboard::KeyClick(key))));
|
|
}
|
|
}
|
|
|
|
pub struct UInputMouse {
|
|
conn: Connection,
|
|
rt: Runtime,
|
|
}
|
|
|
|
impl UInputMouse {
|
|
pub async fn new() -> ResultType<Self> {
|
|
let conn = ipc::connect(IPC_CONN_TIMEOUT, IPC_POSTFIX_MOUSE).await?;
|
|
let rt = Runtime::new()?;
|
|
Ok(Self { conn, rt })
|
|
}
|
|
|
|
fn send(&mut self, data: Data) -> ResultType<()> {
|
|
self.rt.block_on(self.conn.send(&data))
|
|
}
|
|
|
|
pub fn send_refresh(&mut self) -> ResultType<()> {
|
|
self.send(Data::Mouse(DataMouse::Refresh))
|
|
}
|
|
}
|
|
|
|
impl MouseControllable for UInputMouse {
|
|
fn as_any(&self) -> &dyn std::any::Any {
|
|
self
|
|
}
|
|
|
|
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
|
|
self
|
|
}
|
|
|
|
fn mouse_move_to(&mut self, x: i32, y: i32) {
|
|
allow_err!(self.send(Data::Mouse(DataMouse::MoveTo(x, y))));
|
|
}
|
|
fn mouse_move_relative(&mut self, x: i32, y: i32) {
|
|
allow_err!(self.send(Data::Mouse(DataMouse::MoveRelative(x, y))));
|
|
}
|
|
// TODO: handle error???
|
|
fn mouse_down(&mut self, button: MouseButton) -> enigo::ResultType {
|
|
allow_err!(self.send(Data::Mouse(DataMouse::Down(button))));
|
|
Ok(())
|
|
}
|
|
fn mouse_up(&mut self, button: MouseButton) {
|
|
allow_err!(self.send(Data::Mouse(DataMouse::Up(button))));
|
|
}
|
|
fn mouse_click(&mut self, button: MouseButton) {
|
|
allow_err!(self.send(Data::Mouse(DataMouse::Click(button))));
|
|
}
|
|
fn mouse_scroll_x(&mut self, length: i32) {
|
|
allow_err!(self.send(Data::Mouse(DataMouse::ScrollX(length))));
|
|
}
|
|
fn mouse_scroll_y(&mut self, length: i32) {
|
|
allow_err!(self.send(Data::Mouse(DataMouse::ScrollY(length))));
|
|
}
|
|
}
|
|
|
|
pub async fn set_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> {
|
|
let mut conn = ipc::connect(IPC_CONN_TIMEOUT, IPC_POSTFIX_CONTROL).await?;
|
|
conn.send(&Data::Control(ipc::DataControl::Resolution {
|
|
minx,
|
|
maxx,
|
|
miny,
|
|
maxy,
|
|
}))
|
|
.await?;
|
|
let _ = conn.next().await?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub mod service {
|
|
use super::*;
|
|
use hbb_common::lazy_static;
|
|
use std::{collections::HashMap, sync::Mutex};
|
|
|
|
lazy_static::lazy_static! {
|
|
static ref KEY_MAP: HashMap<enigo::Key, evdev::Key> = HashMap::from(
|
|
[
|
|
(enigo::Key::Alt, evdev::Key::KEY_LEFTALT),
|
|
(enigo::Key::Backspace, evdev::Key::KEY_BACKSPACE),
|
|
(enigo::Key::CapsLock, evdev::Key::KEY_CAPSLOCK),
|
|
(enigo::Key::Control, evdev::Key::KEY_LEFTCTRL),
|
|
(enigo::Key::Delete, evdev::Key::KEY_DELETE),
|
|
(enigo::Key::DownArrow, evdev::Key::KEY_DOWN),
|
|
(enigo::Key::End, evdev::Key::KEY_END),
|
|
(enigo::Key::Escape, evdev::Key::KEY_ESC),
|
|
(enigo::Key::F1, evdev::Key::KEY_F1),
|
|
(enigo::Key::F10, evdev::Key::KEY_F10),
|
|
(enigo::Key::F11, evdev::Key::KEY_F11),
|
|
(enigo::Key::F12, evdev::Key::KEY_F12),
|
|
(enigo::Key::F2, evdev::Key::KEY_F2),
|
|
(enigo::Key::F3, evdev::Key::KEY_F3),
|
|
(enigo::Key::F4, evdev::Key::KEY_F4),
|
|
(enigo::Key::F5, evdev::Key::KEY_F5),
|
|
(enigo::Key::F6, evdev::Key::KEY_F6),
|
|
(enigo::Key::F7, evdev::Key::KEY_F7),
|
|
(enigo::Key::F8, evdev::Key::KEY_F8),
|
|
(enigo::Key::F9, evdev::Key::KEY_F9),
|
|
(enigo::Key::Home, evdev::Key::KEY_HOME),
|
|
(enigo::Key::LeftArrow, evdev::Key::KEY_LEFT),
|
|
(enigo::Key::Meta, evdev::Key::KEY_LEFTMETA),
|
|
(enigo::Key::Option, evdev::Key::KEY_OPTION),
|
|
(enigo::Key::PageDown, evdev::Key::KEY_PAGEDOWN),
|
|
(enigo::Key::PageUp, evdev::Key::KEY_PAGEUP),
|
|
(enigo::Key::Return, evdev::Key::KEY_ENTER),
|
|
(enigo::Key::RightArrow, evdev::Key::KEY_RIGHT),
|
|
(enigo::Key::Shift, evdev::Key::KEY_LEFTSHIFT),
|
|
(enigo::Key::Space, evdev::Key::KEY_SPACE),
|
|
(enigo::Key::Tab, evdev::Key::KEY_TAB),
|
|
(enigo::Key::UpArrow, evdev::Key::KEY_UP),
|
|
(enigo::Key::Numpad0, evdev::Key::KEY_KP0), // check if correct?
|
|
(enigo::Key::Numpad1, evdev::Key::KEY_KP1),
|
|
(enigo::Key::Numpad2, evdev::Key::KEY_KP2),
|
|
(enigo::Key::Numpad3, evdev::Key::KEY_KP3),
|
|
(enigo::Key::Numpad4, evdev::Key::KEY_KP4),
|
|
(enigo::Key::Numpad5, evdev::Key::KEY_KP5),
|
|
(enigo::Key::Numpad6, evdev::Key::KEY_KP6),
|
|
(enigo::Key::Numpad7, evdev::Key::KEY_KP7),
|
|
(enigo::Key::Numpad8, evdev::Key::KEY_KP8),
|
|
(enigo::Key::Numpad9, evdev::Key::KEY_KP9),
|
|
(enigo::Key::Cancel, evdev::Key::KEY_CANCEL),
|
|
(enigo::Key::Clear, evdev::Key::KEY_CLEAR),
|
|
(enigo::Key::Alt, evdev::Key::KEY_LEFTALT),
|
|
(enigo::Key::Pause, evdev::Key::KEY_PAUSE),
|
|
(enigo::Key::Kana, evdev::Key::KEY_KATAKANA), // check if correct?
|
|
(enigo::Key::Hangul, evdev::Key::KEY_HANGEUL), // check if correct?
|
|
// (enigo::Key::Junja, evdev::Key::KEY_JUNJA), // map?
|
|
// (enigo::Key::Final, evdev::Key::KEY_FINAL), // map?
|
|
(enigo::Key::Hanja, evdev::Key::KEY_HANJA),
|
|
// (enigo::Key::Kanji, evdev::Key::KEY_KANJI), // map?
|
|
// (enigo::Key::Convert, evdev::Key::KEY_CONVERT),
|
|
(enigo::Key::Select, evdev::Key::KEY_SELECT),
|
|
(enigo::Key::Print, evdev::Key::KEY_PRINT),
|
|
// (enigo::Key::Execute, evdev::Key::KEY_EXECUTE),
|
|
// (enigo::Key::Snapshot, evdev::Key::KEY_SNAPSHOT),
|
|
(enigo::Key::Insert, evdev::Key::KEY_INSERT),
|
|
(enigo::Key::Help, evdev::Key::KEY_HELP),
|
|
(enigo::Key::Sleep, evdev::Key::KEY_SLEEP),
|
|
// (enigo::Key::Separator, evdev::Key::KEY_SEPARATOR),
|
|
(enigo::Key::Scroll, evdev::Key::KEY_SCROLLLOCK),
|
|
(enigo::Key::NumLock, evdev::Key::KEY_NUMLOCK),
|
|
(enigo::Key::RWin, evdev::Key::KEY_RIGHTMETA),
|
|
(enigo::Key::Apps, evdev::Key::KEY_CONTEXT_MENU),
|
|
(enigo::Key::Multiply, evdev::Key::KEY_KPASTERISK),
|
|
(enigo::Key::Add, evdev::Key::KEY_KPPLUS),
|
|
(enigo::Key::Subtract, evdev::Key::KEY_KPMINUS),
|
|
(enigo::Key::Decimal, evdev::Key::KEY_KPCOMMA), // KEY_KPDOT and KEY_KPCOMMA are exchanged?
|
|
(enigo::Key::Divide, evdev::Key::KEY_KPSLASH),
|
|
(enigo::Key::Equals, evdev::Key::KEY_KPEQUAL),
|
|
(enigo::Key::NumpadEnter, evdev::Key::KEY_KPENTER),
|
|
(enigo::Key::RightAlt, evdev::Key::KEY_RIGHTALT),
|
|
(enigo::Key::RightControl, evdev::Key::KEY_RIGHTCTRL),
|
|
(enigo::Key::RightShift, evdev::Key::KEY_RIGHTSHIFT),
|
|
]);
|
|
|
|
static ref KEY_MAP_LAYOUT: HashMap<char, (evdev::Key, bool)> = HashMap::from(
|
|
[
|
|
('a', (evdev::Key::KEY_A, false)),
|
|
('b', (evdev::Key::KEY_B, false)),
|
|
('c', (evdev::Key::KEY_C, false)),
|
|
('d', (evdev::Key::KEY_D, false)),
|
|
('e', (evdev::Key::KEY_E, false)),
|
|
('f', (evdev::Key::KEY_F, false)),
|
|
('g', (evdev::Key::KEY_G, false)),
|
|
('h', (evdev::Key::KEY_H, false)),
|
|
('i', (evdev::Key::KEY_I, false)),
|
|
('j', (evdev::Key::KEY_J, false)),
|
|
('k', (evdev::Key::KEY_K, false)),
|
|
('l', (evdev::Key::KEY_L, false)),
|
|
('m', (evdev::Key::KEY_M, false)),
|
|
('n', (evdev::Key::KEY_N, false)),
|
|
('o', (evdev::Key::KEY_O, false)),
|
|
('p', (evdev::Key::KEY_P, false)),
|
|
('q', (evdev::Key::KEY_Q, false)),
|
|
('r', (evdev::Key::KEY_R, false)),
|
|
('s', (evdev::Key::KEY_S, false)),
|
|
('t', (evdev::Key::KEY_T, false)),
|
|
('u', (evdev::Key::KEY_U, false)),
|
|
('v', (evdev::Key::KEY_V, false)),
|
|
('w', (evdev::Key::KEY_W, false)),
|
|
('x', (evdev::Key::KEY_X, false)),
|
|
('y', (evdev::Key::KEY_Y, false)),
|
|
('z', (evdev::Key::KEY_Z, false)),
|
|
('0', (evdev::Key::KEY_0, false)),
|
|
('1', (evdev::Key::KEY_1, false)),
|
|
('2', (evdev::Key::KEY_2, false)),
|
|
('3', (evdev::Key::KEY_3, false)),
|
|
('4', (evdev::Key::KEY_4, false)),
|
|
('5', (evdev::Key::KEY_5, false)),
|
|
('6', (evdev::Key::KEY_6, false)),
|
|
('7', (evdev::Key::KEY_7, false)),
|
|
('8', (evdev::Key::KEY_8, false)),
|
|
('9', (evdev::Key::KEY_9, false)),
|
|
('`', (evdev::Key::KEY_GRAVE, false)),
|
|
('-', (evdev::Key::KEY_MINUS, false)),
|
|
('=', (evdev::Key::KEY_EQUAL, false)),
|
|
('[', (evdev::Key::KEY_LEFTBRACE, false)),
|
|
(']', (evdev::Key::KEY_RIGHTBRACE, false)),
|
|
('\\', (evdev::Key::KEY_BACKSLASH, false)),
|
|
(',', (evdev::Key::KEY_COMMA, false)),
|
|
('.', (evdev::Key::KEY_DOT, false)),
|
|
('/', (evdev::Key::KEY_SLASH, false)),
|
|
(';', (evdev::Key::KEY_SEMICOLON, false)),
|
|
('\'', (evdev::Key::KEY_APOSTROPHE, false)),
|
|
|
|
// Shift + key
|
|
('A', (evdev::Key::KEY_A, true)),
|
|
('B', (evdev::Key::KEY_B, true)),
|
|
('C', (evdev::Key::KEY_C, true)),
|
|
('D', (evdev::Key::KEY_D, true)),
|
|
('E', (evdev::Key::KEY_E, true)),
|
|
('F', (evdev::Key::KEY_F, true)),
|
|
('G', (evdev::Key::KEY_G, true)),
|
|
('H', (evdev::Key::KEY_H, true)),
|
|
('I', (evdev::Key::KEY_I, true)),
|
|
('J', (evdev::Key::KEY_J, true)),
|
|
('K', (evdev::Key::KEY_K, true)),
|
|
('L', (evdev::Key::KEY_L, true)),
|
|
('M', (evdev::Key::KEY_M, true)),
|
|
('N', (evdev::Key::KEY_N, true)),
|
|
('O', (evdev::Key::KEY_O, true)),
|
|
('P', (evdev::Key::KEY_P, true)),
|
|
('Q', (evdev::Key::KEY_Q, true)),
|
|
('R', (evdev::Key::KEY_R, true)),
|
|
('S', (evdev::Key::KEY_S, true)),
|
|
('T', (evdev::Key::KEY_T, true)),
|
|
('U', (evdev::Key::KEY_U, true)),
|
|
('V', (evdev::Key::KEY_V, true)),
|
|
('W', (evdev::Key::KEY_W, true)),
|
|
('X', (evdev::Key::KEY_X, true)),
|
|
('Y', (evdev::Key::KEY_Y, true)),
|
|
('Z', (evdev::Key::KEY_Z, true)),
|
|
(')', (evdev::Key::KEY_0, true)),
|
|
('!', (evdev::Key::KEY_1, true)),
|
|
('@', (evdev::Key::KEY_2, true)),
|
|
('#', (evdev::Key::KEY_3, true)),
|
|
('$', (evdev::Key::KEY_4, true)),
|
|
('%', (evdev::Key::KEY_5, true)),
|
|
('^', (evdev::Key::KEY_6, true)),
|
|
('&', (evdev::Key::KEY_7, true)),
|
|
('*', (evdev::Key::KEY_8, true)),
|
|
('(', (evdev::Key::KEY_9, true)),
|
|
('~', (evdev::Key::KEY_GRAVE, true)),
|
|
('_', (evdev::Key::KEY_MINUS, true)),
|
|
('+', (evdev::Key::KEY_EQUAL, true)),
|
|
('{', (evdev::Key::KEY_LEFTBRACE, true)),
|
|
('}', (evdev::Key::KEY_RIGHTBRACE, true)),
|
|
('|', (evdev::Key::KEY_BACKSLASH, true)),
|
|
('<', (evdev::Key::KEY_COMMA, true)),
|
|
('>', (evdev::Key::KEY_DOT, true)),
|
|
('?', (evdev::Key::KEY_SLASH, true)),
|
|
(':', (evdev::Key::KEY_SEMICOLON, true)),
|
|
('"', (evdev::Key::KEY_APOSTROPHE, true)),
|
|
]);
|
|
|
|
// ((minx, maxx), (miny, maxy))
|
|
static ref RESOLUTION: Mutex<((i32, i32), (i32, i32))> = Mutex::new(((0, 0), (0, 0)));
|
|
}
|
|
|
|
fn create_uinput_keyboard() -> ResultType<VirtualDevice> {
|
|
// TODO: ensure keys here
|
|
let mut keys = AttributeSet::<evdev::Key>::new();
|
|
for i in evdev::Key::KEY_ESC.code()..(evdev::Key::BTN_TRIGGER_HAPPY40.code() + 1) {
|
|
let key = evdev::Key::new(i);
|
|
if !format!("{:?}", &key).contains("unknown key") {
|
|
keys.insert(key);
|
|
}
|
|
}
|
|
let mut leds = AttributeSet::<evdev::LedType>::new();
|
|
leds.insert(evdev::LedType::LED_NUML);
|
|
leds.insert(evdev::LedType::LED_CAPSL);
|
|
leds.insert(evdev::LedType::LED_SCROLLL);
|
|
let mut miscs = AttributeSet::<evdev::MiscType>::new();
|
|
miscs.insert(evdev::MiscType::MSC_SCAN);
|
|
let keyboard = VirtualDeviceBuilder::new()?
|
|
.name("RustDesk UInput Keyboard")
|
|
.with_keys(&keys)?
|
|
.with_leds(&leds)?
|
|
.with_miscs(&miscs)?
|
|
.build()?;
|
|
Ok(keyboard)
|
|
}
|
|
|
|
pub fn map_key(key: &enigo::Key) -> ResultType<(evdev::Key, bool)> {
|
|
if let Some(k) = KEY_MAP.get(&key) {
|
|
log::trace!("mapkey {:?}, get {:?}", &key, &k);
|
|
return Ok((k.clone(), false));
|
|
} else {
|
|
match key {
|
|
enigo::Key::Layout(c) => {
|
|
if let Some((k, is_shift)) = KEY_MAP_LAYOUT.get(&c) {
|
|
log::trace!("mapkey {:?}, get {:?}", &key, k);
|
|
return Ok((k.clone(), is_shift.clone()));
|
|
}
|
|
}
|
|
// enigo::Key::Raw(c) => {
|
|
// let k = evdev::Key::new(c);
|
|
// if !format!("{:?}", &k).contains("unknown key") {
|
|
// return Ok(k.clone());
|
|
// }
|
|
// }
|
|
_ => {}
|
|
}
|
|
}
|
|
bail!("Failed to map key {:?}", &key);
|
|
}
|
|
|
|
async fn ipc_send_data(stream: &mut Connection, data: &Data) {
|
|
allow_err!(stream.send(data).await);
|
|
}
|
|
|
|
async fn handle_keyboard(
|
|
stream: &mut Connection,
|
|
keyboard: &mut VirtualDevice,
|
|
data: &DataKeyboard,
|
|
) {
|
|
log::trace!("handle_keyboard {:?}", &data);
|
|
match data {
|
|
DataKeyboard::Sequence(_seq) => {
|
|
// ignore
|
|
}
|
|
DataKeyboard::KeyDown(enigo::Key::Raw(code)) => {
|
|
let down_event = InputEvent::new(EventType::KEY, *code - 8, 1);
|
|
allow_err!(keyboard.emit(&[down_event]));
|
|
}
|
|
DataKeyboard::KeyUp(enigo::Key::Raw(code)) => {
|
|
let down_event = InputEvent::new(EventType::KEY, *code - 8, 0);
|
|
allow_err!(keyboard.emit(&[down_event]));
|
|
}
|
|
DataKeyboard::KeyDown(key) => {
|
|
if let Ok((k, is_shift)) = map_key(key) {
|
|
if is_shift {
|
|
let down_event =
|
|
InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 1);
|
|
allow_err!(keyboard.emit(&[down_event]));
|
|
}
|
|
let down_event = InputEvent::new(EventType::KEY, k.code(), 1);
|
|
allow_err!(keyboard.emit(&[down_event]));
|
|
}
|
|
}
|
|
DataKeyboard::KeyUp(key) => {
|
|
if let Ok((k, _)) = map_key(key) {
|
|
let up_event = InputEvent::new(EventType::KEY, k.code(), 0);
|
|
allow_err!(keyboard.emit(&[up_event]));
|
|
}
|
|
}
|
|
DataKeyboard::KeyClick(key) => {
|
|
if let Ok((k, _)) = map_key(key) {
|
|
let down_event = InputEvent::new(EventType::KEY, k.code(), 1);
|
|
let up_event = InputEvent::new(EventType::KEY, k.code(), 0);
|
|
allow_err!(keyboard.emit(&[down_event, up_event]));
|
|
}
|
|
}
|
|
DataKeyboard::GetKeyState(key) => {
|
|
let key_state = if enigo::Key::CapsLock == *key {
|
|
match keyboard.get_led_state() {
|
|
Ok(leds) => leds.contains(evdev::LedType::LED_CAPSL),
|
|
Err(_e) => {
|
|
// log::debug!("Failed to get led state {}", &_e);
|
|
false
|
|
}
|
|
}
|
|
} else if enigo::Key::NumLock == *key {
|
|
match keyboard.get_led_state() {
|
|
Ok(leds) => leds.contains(evdev::LedType::LED_NUML),
|
|
Err(_e) => {
|
|
// log::debug!("Failed to get led state {}", &_e);
|
|
false
|
|
}
|
|
}
|
|
} else {
|
|
match keyboard.get_key_state() {
|
|
Ok(keys) => match key {
|
|
enigo::Key::Shift => {
|
|
keys.contains(evdev::Key::KEY_LEFTSHIFT)
|
|
|| keys.contains(evdev::Key::KEY_RIGHTSHIFT)
|
|
}
|
|
enigo::Key::Control => {
|
|
keys.contains(evdev::Key::KEY_LEFTCTRL)
|
|
|| keys.contains(evdev::Key::KEY_RIGHTCTRL)
|
|
}
|
|
enigo::Key::Alt => {
|
|
keys.contains(evdev::Key::KEY_LEFTALT)
|
|
|| keys.contains(evdev::Key::KEY_RIGHTALT)
|
|
}
|
|
enigo::Key::Meta => {
|
|
keys.contains(evdev::Key::KEY_LEFTMETA)
|
|
|| keys.contains(evdev::Key::KEY_RIGHTMETA)
|
|
}
|
|
_ => false,
|
|
},
|
|
Err(_e) => {
|
|
// log::debug!("Failed to get key state: {}", &_e);
|
|
false
|
|
}
|
|
}
|
|
};
|
|
ipc_send_data(
|
|
stream,
|
|
&Data::KeyboardResponse(ipc::DataKeyboardResponse::GetKeyState(key_state)),
|
|
)
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_mouse(mouse: &mut mouce::UInputMouseManager, data: &DataMouse) {
|
|
log::trace!("handle_mouse {:?}", &data);
|
|
match data {
|
|
DataMouse::MoveTo(x, y) => {
|
|
allow_err!(mouse.move_to(*x as _, *y as _))
|
|
}
|
|
DataMouse::MoveRelative(x, y) => {
|
|
allow_err!(mouse.move_relative(*x, *y))
|
|
}
|
|
DataMouse::Down(button) => {
|
|
let btn = match button {
|
|
enigo::MouseButton::Left => mouce::MouseButton::Left,
|
|
enigo::MouseButton::Middle => mouce::MouseButton::Middle,
|
|
enigo::MouseButton::Right => mouce::MouseButton::Right,
|
|
_ => {
|
|
return;
|
|
}
|
|
};
|
|
allow_err!(mouse.press_button(&btn))
|
|
}
|
|
DataMouse::Up(button) => {
|
|
let btn = match button {
|
|
enigo::MouseButton::Left => mouce::MouseButton::Left,
|
|
enigo::MouseButton::Middle => mouce::MouseButton::Middle,
|
|
enigo::MouseButton::Right => mouce::MouseButton::Right,
|
|
_ => {
|
|
return;
|
|
}
|
|
};
|
|
allow_err!(mouse.release_button(&btn))
|
|
}
|
|
DataMouse::Click(button) => {
|
|
let btn = match button {
|
|
enigo::MouseButton::Left => mouce::MouseButton::Left,
|
|
enigo::MouseButton::Middle => mouce::MouseButton::Middle,
|
|
enigo::MouseButton::Right => mouce::MouseButton::Right,
|
|
_ => {
|
|
return;
|
|
}
|
|
};
|
|
allow_err!(mouse.click_button(&btn))
|
|
}
|
|
DataMouse::ScrollX(_length) => {
|
|
// TODO: not supported for now
|
|
}
|
|
DataMouse::ScrollY(length) => {
|
|
let mut length = *length;
|
|
|
|
let scroll = if length < 0 {
|
|
mouce::ScrollDirection::Up
|
|
} else {
|
|
mouce::ScrollDirection::Down
|
|
};
|
|
|
|
if length < 0 {
|
|
length = -length;
|
|
}
|
|
|
|
for _ in 0..length {
|
|
allow_err!(mouse.scroll_wheel(&scroll))
|
|
}
|
|
}
|
|
DataMouse::Refresh => {
|
|
// unreachable!()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn spawn_keyboard_handler(mut stream: Connection) {
|
|
tokio::spawn(async move {
|
|
let mut keyboard = match create_uinput_keyboard() {
|
|
Ok(keyboard) => keyboard,
|
|
Err(e) => {
|
|
log::error!("Failed to create keyboard {}", e);
|
|
return;
|
|
}
|
|
};
|
|
loop {
|
|
tokio::select! {
|
|
res = stream.next() => {
|
|
match res {
|
|
Err(err) => {
|
|
log::info!("UInput keyboard ipc connection closed: {}", err);
|
|
break;
|
|
}
|
|
Ok(Some(data)) => {
|
|
match data {
|
|
Data::Keyboard(data) => {
|
|
handle_keyboard(&mut stream, &mut keyboard, &data).await;
|
|
}
|
|
_ => {
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn spawn_mouse_handler(mut stream: ipc::Connection) {
|
|
let resolution = RESOLUTION.lock().unwrap();
|
|
if resolution.0 .0 == resolution.0 .1 || resolution.1 .0 == resolution.1 .1 {
|
|
return;
|
|
}
|
|
let rng_x = resolution.0.clone();
|
|
let rng_y = resolution.1.clone();
|
|
tokio::spawn(async move {
|
|
log::info!(
|
|
"Create uinput mouce with rng_x: ({}, {}), rng_y: ({}, {})",
|
|
rng_x.0,
|
|
rng_x.1,
|
|
rng_y.0,
|
|
rng_y.1
|
|
);
|
|
let mut mouse = match mouce::UInputMouseManager::new(rng_x, rng_y) {
|
|
Ok(mouse) => mouse,
|
|
Err(e) => {
|
|
log::error!("Failed to create mouse, {}", e);
|
|
return;
|
|
}
|
|
};
|
|
loop {
|
|
tokio::select! {
|
|
res = stream.next() => {
|
|
match res {
|
|
Err(err) => {
|
|
log::info!("UInput mouse ipc connection closed: {}", err);
|
|
break;
|
|
}
|
|
Ok(Some(data)) => {
|
|
match data {
|
|
Data::Mouse(data) => {
|
|
if let DataMouse::Refresh = data {
|
|
let resolution = RESOLUTION.lock().unwrap();
|
|
let rng_x = resolution.0.clone();
|
|
let rng_y = resolution.1.clone();
|
|
log::info!(
|
|
"Refresh uinput mouce with rng_x: ({}, {}), rng_y: ({}, {})",
|
|
rng_x.0,
|
|
rng_x.1,
|
|
rng_y.0,
|
|
rng_y.1
|
|
);
|
|
mouse = match mouce::UInputMouseManager::new(rng_x, rng_y) {
|
|
Ok(mouse) => mouse,
|
|
Err(e) => {
|
|
log::error!("Failed to create mouse, {}", e);
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
handle_mouse(&mut mouse, &data);
|
|
}
|
|
}
|
|
_ => {
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn spawn_controller_handler(mut stream: ipc::Connection) {
|
|
tokio::spawn(async move {
|
|
loop {
|
|
tokio::select! {
|
|
res = stream.next() => {
|
|
match res {
|
|
Err(_err) => {
|
|
// log::info!("UInput controller ipc connection closed: {}", err);
|
|
break;
|
|
}
|
|
Ok(Some(data)) => {
|
|
match data {
|
|
Data::Control(data) => match data {
|
|
ipc::DataControl::Resolution{
|
|
minx,
|
|
maxx,
|
|
miny,
|
|
maxy,
|
|
} => {
|
|
*RESOLUTION.lock().unwrap() = ((minx, maxx), (miny, maxy));
|
|
allow_err!(stream.send(&Data::Empty).await);
|
|
}
|
|
}
|
|
_ => {
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Start uinput service.
|
|
async fn start_service<F: FnOnce(ipc::Connection) + Copy>(postfix: &str, handler: F) {
|
|
match new_listener(postfix).await {
|
|
Ok(mut incoming) => {
|
|
while let Some(result) = incoming.next().await {
|
|
match result {
|
|
Ok(stream) => {
|
|
log::debug!("Got new connection of uinput ipc {}", postfix);
|
|
handler(Connection::new(stream));
|
|
}
|
|
Err(err) => {
|
|
log::error!("Couldn't get uinput mouse client: {:?}", err);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
log::error!("Failed to start uinput mouse ipc service: {}", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Start uinput keyboard service.
|
|
#[tokio::main(flavor = "current_thread")]
|
|
pub async fn start_service_keyboard() {
|
|
log::info!("start uinput keyboard service");
|
|
start_service(IPC_POSTFIX_KEYBOARD, spawn_keyboard_handler).await;
|
|
}
|
|
|
|
/// Start uinput mouse service.
|
|
#[tokio::main(flavor = "current_thread")]
|
|
pub async fn start_service_mouse() {
|
|
log::info!("start uinput mouse service");
|
|
start_service(IPC_POSTFIX_MOUSE, spawn_mouse_handler).await;
|
|
}
|
|
|
|
/// Start uinput mouse service.
|
|
#[tokio::main(flavor = "current_thread")]
|
|
pub async fn start_service_control() {
|
|
log::info!("start uinput control service");
|
|
start_service(IPC_POSTFIX_CONTROL, spawn_controller_handler).await;
|
|
}
|
|
|
|
pub fn stop_service_keyboard() {
|
|
log::info!("stop uinput keyboard service");
|
|
}
|
|
pub fn stop_service_mouse() {
|
|
log::info!("stop uinput mouse service");
|
|
}
|
|
pub fn stop_service_control() {
|
|
log::info!("stop uinput control service");
|
|
}
|
|
}
|
|
|
|
// https://github.com/emrebicer/mouce
|
|
mod mouce {
|
|
use std::{
|
|
fs::File,
|
|
io::{Error, ErrorKind, Result},
|
|
mem::size_of,
|
|
os::{
|
|
raw::{c_char, c_int, c_long, c_uint, c_ulong, c_ushort},
|
|
unix::{fs::OpenOptionsExt, io::AsRawFd},
|
|
},
|
|
thread,
|
|
time::Duration,
|
|
};
|
|
|
|
pub const O_NONBLOCK: c_int = 2048;
|
|
|
|
/// ioctl and uinput definitions
|
|
const UI_ABS_SETUP: c_ulong = 1075598596;
|
|
const UI_SET_EVBIT: c_ulong = 1074025828;
|
|
const UI_SET_KEYBIT: c_ulong = 1074025829;
|
|
const UI_SET_RELBIT: c_ulong = 1074025830;
|
|
const UI_SET_ABSBIT: c_ulong = 1074025831;
|
|
const UI_DEV_SETUP: c_ulong = 1079792899;
|
|
const UI_DEV_CREATE: c_ulong = 21761;
|
|
const UI_DEV_DESTROY: c_uint = 21762;
|
|
|
|
pub const EV_KEY: c_int = 0x01;
|
|
pub const EV_REL: c_int = 0x02;
|
|
pub const EV_ABS: c_int = 0x03;
|
|
pub const REL_X: c_uint = 0x00;
|
|
pub const REL_Y: c_uint = 0x01;
|
|
pub const ABS_X: c_uint = 0x00;
|
|
pub const ABS_Y: c_uint = 0x01;
|
|
pub const REL_WHEEL: c_uint = 0x08;
|
|
pub const REL_HWHEEL: c_uint = 0x06;
|
|
pub const BTN_LEFT: c_int = 0x110;
|
|
pub const BTN_RIGHT: c_int = 0x111;
|
|
pub const BTN_MIDDLE: c_int = 0x112;
|
|
pub const BTN_SIDE: c_int = 0x113;
|
|
pub const BTN_EXTRA: c_int = 0x114;
|
|
pub const BTN_FORWARD: c_int = 0x115;
|
|
pub const BTN_BACK: c_int = 0x116;
|
|
pub const BTN_TASK: c_int = 0x117;
|
|
const SYN_REPORT: c_int = 0x00;
|
|
const EV_SYN: c_int = 0x00;
|
|
const BUS_USB: c_ushort = 0x03;
|
|
|
|
/// uinput types
|
|
#[repr(C)]
|
|
struct UInputSetup {
|
|
id: InputId,
|
|
name: [c_char; UINPUT_MAX_NAME_SIZE],
|
|
ff_effects_max: c_ulong,
|
|
}
|
|
|
|
#[repr(C)]
|
|
struct InputId {
|
|
bustype: c_ushort,
|
|
vendor: c_ushort,
|
|
product: c_ushort,
|
|
version: c_ushort,
|
|
}
|
|
|
|
#[repr(C)]
|
|
pub struct InputEvent {
|
|
pub time: TimeVal,
|
|
pub r#type: c_ushort,
|
|
pub code: c_ushort,
|
|
pub value: c_int,
|
|
}
|
|
|
|
#[repr(C)]
|
|
pub struct TimeVal {
|
|
pub tv_sec: c_ulong,
|
|
pub tv_usec: c_ulong,
|
|
}
|
|
|
|
#[repr(C)]
|
|
pub struct UinputAbsSetup {
|
|
pub code: c_ushort,
|
|
pub absinfo: InputAbsinfo,
|
|
}
|
|
|
|
#[repr(C)]
|
|
pub struct InputAbsinfo {
|
|
pub value: c_int,
|
|
pub minimum: c_int,
|
|
pub maximum: c_int,
|
|
pub fuzz: c_int,
|
|
pub flat: c_int,
|
|
pub resolution: c_int,
|
|
}
|
|
|
|
extern "C" {
|
|
fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int;
|
|
fn write(fd: c_int, buf: *mut InputEvent, count: usize) -> c_long;
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
pub enum MouseButton {
|
|
Left,
|
|
Middle,
|
|
Side,
|
|
Extra,
|
|
Right,
|
|
Back,
|
|
Forward,
|
|
Task,
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
pub enum ScrollDirection {
|
|
Up,
|
|
Down,
|
|
Right,
|
|
Left,
|
|
}
|
|
|
|
const UINPUT_MAX_NAME_SIZE: usize = 80;
|
|
|
|
pub struct UInputMouseManager {
|
|
uinput_file: File,
|
|
}
|
|
|
|
impl UInputMouseManager {
|
|
pub fn new(rng_x: (i32, i32), rng_y: (i32, i32)) -> Result<Self> {
|
|
let manager = UInputMouseManager {
|
|
uinput_file: File::options()
|
|
.write(true)
|
|
.custom_flags(O_NONBLOCK)
|
|
.open("/dev/uinput")?,
|
|
};
|
|
let fd = manager.uinput_file.as_raw_fd();
|
|
unsafe {
|
|
// For press events (also needed for mouse movement)
|
|
ioctl(fd, UI_SET_EVBIT, EV_KEY);
|
|
ioctl(fd, UI_SET_KEYBIT, BTN_LEFT);
|
|
ioctl(fd, UI_SET_KEYBIT, BTN_RIGHT);
|
|
ioctl(fd, UI_SET_KEYBIT, BTN_MIDDLE);
|
|
|
|
// For mouse movement
|
|
ioctl(fd, UI_SET_EVBIT, EV_ABS);
|
|
ioctl(fd, UI_SET_ABSBIT, ABS_X);
|
|
ioctl(
|
|
fd,
|
|
UI_ABS_SETUP,
|
|
&UinputAbsSetup {
|
|
code: ABS_X as _,
|
|
absinfo: InputAbsinfo {
|
|
value: 0,
|
|
minimum: rng_x.0,
|
|
maximum: rng_x.1,
|
|
fuzz: 0,
|
|
flat: 0,
|
|
resolution: 0,
|
|
},
|
|
},
|
|
);
|
|
ioctl(fd, UI_SET_ABSBIT, ABS_Y);
|
|
ioctl(
|
|
fd,
|
|
UI_ABS_SETUP,
|
|
&UinputAbsSetup {
|
|
code: ABS_Y as _,
|
|
absinfo: InputAbsinfo {
|
|
value: 0,
|
|
minimum: rng_y.0,
|
|
maximum: rng_y.1,
|
|
fuzz: 0,
|
|
flat: 0,
|
|
resolution: 0,
|
|
},
|
|
},
|
|
);
|
|
|
|
ioctl(fd, UI_SET_EVBIT, EV_REL);
|
|
ioctl(fd, UI_SET_RELBIT, REL_X);
|
|
ioctl(fd, UI_SET_RELBIT, REL_Y);
|
|
ioctl(fd, UI_SET_RELBIT, REL_WHEEL);
|
|
ioctl(fd, UI_SET_RELBIT, REL_HWHEEL);
|
|
}
|
|
|
|
let mut usetup = UInputSetup {
|
|
id: InputId {
|
|
bustype: BUS_USB,
|
|
// Random vendor and product
|
|
vendor: 0x2222,
|
|
product: 0x3333,
|
|
version: 0,
|
|
},
|
|
name: [0; UINPUT_MAX_NAME_SIZE],
|
|
ff_effects_max: 0,
|
|
};
|
|
|
|
let mut device_bytes: Vec<c_char> = "mouce-library-fake-mouse"
|
|
.chars()
|
|
.map(|ch| ch as c_char)
|
|
.collect();
|
|
|
|
// Fill the rest of the name buffer with empty chars
|
|
for _ in 0..UINPUT_MAX_NAME_SIZE - device_bytes.len() {
|
|
device_bytes.push('\0' as c_char);
|
|
}
|
|
|
|
usetup.name.copy_from_slice(&device_bytes);
|
|
|
|
unsafe {
|
|
ioctl(fd, UI_DEV_SETUP, &usetup);
|
|
ioctl(fd, UI_DEV_CREATE);
|
|
}
|
|
|
|
// On UI_DEV_CREATE the kernel will create the device node for this
|
|
// device. We are inserting a pause here so that userspace has time
|
|
// to detect, initialize the new device, and can start listening to
|
|
// the event, otherwise it will not notice the event we are about to send.
|
|
thread::sleep(Duration::from_millis(300));
|
|
|
|
Ok(manager)
|
|
}
|
|
|
|
/// Write the given event to the uinput file
|
|
fn emit(&self, r#type: c_int, code: c_int, value: c_int) -> Result<()> {
|
|
let mut event = InputEvent {
|
|
time: TimeVal {
|
|
tv_sec: 0,
|
|
tv_usec: 0,
|
|
},
|
|
r#type: r#type as c_ushort,
|
|
code: code as c_ushort,
|
|
value,
|
|
};
|
|
let fd = self.uinput_file.as_raw_fd();
|
|
|
|
unsafe {
|
|
let count = size_of::<InputEvent>();
|
|
let written_bytes = write(fd, &mut event, count);
|
|
if written_bytes == -1 || written_bytes != count as c_long {
|
|
return Err(Error::new(
|
|
ErrorKind::Other,
|
|
format!("failed while trying to write to a file"),
|
|
));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Syncronize the device
|
|
fn syncronize(&self) -> Result<()> {
|
|
self.emit(EV_SYN, SYN_REPORT, 0)?;
|
|
// Give uinput some time to update the mouse location,
|
|
// otherwise it fails to move the mouse on release mode
|
|
// A delay of 1 milliseconds seems to be enough for it
|
|
thread::sleep(Duration::from_millis(1));
|
|
Ok(())
|
|
}
|
|
|
|
/// Move the mouse relative to the current position
|
|
fn move_relative_(&self, x: i32, y: i32) -> Result<()> {
|
|
// uinput does not move the mouse in pixels but uses `units`. I couldn't
|
|
// find information regarding to this uinput `unit`, but according to
|
|
// my findings 1 unit corresponds to exactly 2 pixels.
|
|
//
|
|
// To achieve the expected behavior; divide the parameters by 2
|
|
//
|
|
// This seems like there is a bug in this crate, but the
|
|
// behavior is the same on other projects that make use of
|
|
// uinput. e.g. `ydotool`. When you try to move your mouse,
|
|
// it will move 2x further pixels
|
|
self.emit(EV_REL, REL_X as c_int, (x as f32 / 2.).ceil() as c_int)?;
|
|
self.emit(EV_REL, REL_Y as c_int, (y as f32 / 2.).ceil() as c_int)?;
|
|
self.syncronize()
|
|
}
|
|
|
|
fn map_btn(button: &MouseButton) -> c_int {
|
|
match button {
|
|
MouseButton::Left => BTN_LEFT,
|
|
MouseButton::Right => BTN_RIGHT,
|
|
MouseButton::Middle => BTN_MIDDLE,
|
|
MouseButton::Side => BTN_SIDE,
|
|
MouseButton::Extra => BTN_EXTRA,
|
|
MouseButton::Forward => BTN_FORWARD,
|
|
MouseButton::Back => BTN_BACK,
|
|
MouseButton::Task => BTN_TASK,
|
|
}
|
|
}
|
|
|
|
pub fn move_to(&self, x: usize, y: usize) -> Result<()> {
|
|
// // For some reason, absolute mouse move events are not working on uinput
|
|
// // (as I understand those events are intended for touch events)
|
|
// //
|
|
// // As a work around solution; first set the mouse to top left, then
|
|
// // call relative move function to simulate an absolute move event
|
|
//self.move_relative(i32::MIN, i32::MIN)?;
|
|
//self.move_relative(x as i32, y as i32)
|
|
|
|
self.emit(EV_ABS, ABS_X as c_int, x as c_int)?;
|
|
self.emit(EV_ABS, ABS_Y as c_int, y as c_int)?;
|
|
self.syncronize()
|
|
}
|
|
|
|
pub fn move_relative(&self, x_offset: i32, y_offset: i32) -> Result<()> {
|
|
self.move_relative_(x_offset, y_offset)
|
|
}
|
|
|
|
pub fn press_button(&self, button: &MouseButton) -> Result<()> {
|
|
self.emit(EV_KEY, Self::map_btn(button), 1)?;
|
|
self.syncronize()
|
|
}
|
|
|
|
pub fn release_button(&self, button: &MouseButton) -> Result<()> {
|
|
self.emit(EV_KEY, Self::map_btn(button), 0)?;
|
|
self.syncronize()
|
|
}
|
|
|
|
pub fn click_button(&self, button: &MouseButton) -> Result<()> {
|
|
self.press_button(button)?;
|
|
self.release_button(button)
|
|
}
|
|
|
|
pub fn scroll_wheel(&self, direction: &ScrollDirection) -> Result<()> {
|
|
let (code, scroll_value) = match direction {
|
|
ScrollDirection::Up => (REL_WHEEL, 1),
|
|
ScrollDirection::Down => (REL_WHEEL, -1),
|
|
ScrollDirection::Left => (REL_HWHEEL, -1),
|
|
ScrollDirection::Right => (REL_HWHEEL, 1),
|
|
};
|
|
self.emit(EV_REL, code as c_int, scroll_value)?;
|
|
self.syncronize()
|
|
}
|
|
}
|
|
|
|
impl Drop for UInputMouseManager {
|
|
fn drop(&mut self) {
|
|
let fd = self.uinput_file.as_raw_fd();
|
|
unsafe {
|
|
// Destroy the device, the file is closed automatically by the File module
|
|
ioctl(fd, UI_DEV_DESTROY as c_ulong);
|
|
}
|
|
}
|
|
}
|
|
}
|