rewrote client, crashes currently

This commit is contained in:
Brad Stein 2025-06-08 04:11:58 -05:00
parent 2058014ac6
commit 53e9f69356
11 changed files with 372 additions and 61 deletions

View File

@ -1,16 +1,22 @@
[package]
name = "navka-client"
version = "0.1.0"
edition = "2024"
name = "navka_client"
version = "0.2.0"
edition = "2024"
[dependencies]
tokio = { version = "1.45", features = ["full", "fs"] }
tonic = "0.11"
tokio-stream = { version = "0.1", features = ["sync"] }
anyhow = "1.0"
navka-common = { path = "../common" }
tracing = { version = "0.1", features = ["std"] }
tracing-subscriber = "0.3"
tokio = { version = "1.45", features = ["full", "fs"] }
tonic = { version = "0.13", features = ["transport"] }
tokio-stream = { version = "0.1", features = ["sync"] }
anyhow = "1.0"
navka_common = { path = "../common" }
tracing = { version = "0.1", features = ["std"] }
tracing-subscriber = "0.3"
futures = "0.3"
evdev = "0.13"
[build-dependencies]
prost-build = "0.12"
prost-build = "0.13"
[lib]
name = "navka_client"
path = "src/lib.rs"

56
client/src/app.rs Normal file
View File

@ -0,0 +1,56 @@
use anyhow::{Context, Result};
use navka_common::navka::{relay_client::RelayClient, HidReport};
use tokio::sync::mpsc;
// use tokio_stream::{wrappers::ReceiverStream, StreamExt};
// use tonic::{Status, Request};
use tokio_stream::wrappers::ReceiverStream;
use tonic::Request;
use crate::input::keyboard::KeyboardAggregator;
pub struct NavkaClientApp {
server_addr: String,
}
impl NavkaClientApp {
pub fn new() -> Result<Self> {
let addr = std::env::args()
.nth(1)
.or_else(|| std::env::var("NAVKA_SERVER_ADDR").ok())
.unwrap_or_else(|| "http://127.0.0.1:50051".to_owned());
Ok(Self { server_addr: addr })
}
pub async fn run(&mut self) -> Result<()> {
// 1) Connect to navka-server
let mut client = RelayClient::connect(self.server_addr.clone())
.await
.with_context(|| format!("failed to connect to {}", self.server_addr))?;
// 2) Create a bidirectional streaming stub
let (tx, rx) = mpsc::channel::<HidReport>(32);
// let outbound = ReceiverStream::new(rx).map(|report| Ok(report) as Result<HidReport, Status>);
// let response = client.stream(Request::new(outbound)).await?;
let outbound = ReceiverStream::new(rx);
let response = client.stream(Request::new(outbound)).await?;
let mut inbound = response.into_inner();
// 3) Start reading from all keyboards in a background task
let mut aggregator = KeyboardAggregator::new(tx.clone());
aggregator.init_devices()?; // discover & grab
let input_task = tokio::spawn(async move {
if let Err(e) = aggregator.run().await {
tracing::error!("KeyboardAggregator failed: {e}");
}
});
// 4) Inbound loop: we do something with reports from the server, e.g. logging:
while let Some(report) = inbound.message().await? {
tracing::info!(?report.data, "echo from server");
}
// 5) If inbound stream ends, stop the input task
input_task.abort();
Ok(())
}
}

View File

@ -0,0 +1,160 @@
use std::collections::HashSet;
use anyhow::{bail, Context, Result};
use evdev::{Device, InputEvent, KeyCode, EventType};
use tokio::sync::mpsc::Sender;
use tokio::time::{interval, Duration};
use navka_common::navka::HidReport;
use crate::input::keymap::{keycode_to_usage, is_modifier};
/// Up to 6 normal keys. Byte[0] = modifiers, Byte[1] = reserved, Byte[2..7] = pressed keys.
/// Magic chord example: LeftCtrl + LeftAlt + LeftShift + Esc
const MAGIC_CHORD: &[KeyCode] = &[
KeyCode::KEY_LEFTCTRL,
KeyCode::KEY_LEFTSHIFT,
KeyCode::KEY_LEFTALT,
KeyCode::KEY_ESC,
];
pub struct KeyboardAggregator {
// MPSC channel to server
tx: Sender<HidReport>,
// The list of evdev devices we are reading
devices: Vec<Device>,
// Pressed keys for building HID reports
pressed_keys: HashSet<KeyCode>,
}
impl KeyboardAggregator {
pub fn new(tx: Sender<HidReport>) -> Self {
Self {
tx,
devices: Vec::new(),
pressed_keys: HashSet::new(),
}
}
/// Discover all /dev/input/event* devices, filter ones with EV_KEY, and grab them.
pub fn init_devices(&mut self) -> Result<()> {
let paths = std::fs::read_dir("/dev/input")
.context("Failed to read /dev/input")?;
for entry in paths {
let entry = entry?;
let path = entry.path();
// skip anything that isn't "event*"
if !path.file_name().unwrap_or_default().to_string_lossy().starts_with("event") {
continue;
}
let mut dev = Device::open(&path).with_context(|| format!("opening {:?}", path))?;
let maybe_keys = dev.supported_keys();
if let Some(supported) = maybe_keys {
if supported.iter().next().is_none() {
// no real keys
continue;
}
} else {
// Not a keyboard at all
continue;
}
// Attempt to grab
dev.grab().with_context(|| format!("grabbing {:?}", path))?;
tracing::info!("Grabbed keyboard device: {:?}", path);
self.devices.push(dev);
}
if self.devices.is_empty() {
bail!("No keyboard devices found or none grabbed successfully.");
}
Ok(())
}
/// Main loop: read events from all devices in a round-robin style
/// building HID reports as needed. If MAGIC_CHORD is detected, exit.
pub async fn run(&mut self) -> Result<()> {
let mut tick = interval(Duration::from_millis(10));
loop {
// We'll poll each device in turn
for i in 0..self.devices.len() {
// Non-blocking fetch
let evs = match self.devices[i].fetch_events() {
Ok(iter) => iter.collect::<Vec<_>>(),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Vec::new(),
Err(e) => {
tracing::error!("Device read error: {e}");
Vec::new()
}
};
for ev in evs {
self.handle_event(ev)?;
}
}
if self.is_magic_chord() {
tracing::warn!("Magic chord pressed; stopping navka-client.");
// ungrab all before exit
for dev in &mut self.devices {
let _ = dev.ungrab();
}
std::process::exit(0);
}
tick.tick().await;
}
}
fn handle_event(&mut self, ev: InputEvent) -> Result<()> {
// We only care about KEY events
if ev.event_type() == EventType::KEY {
let code = KeyCode::new(ev.code());
let value = ev.value();
match value {
1 => { self.pressed_keys.insert(code); }
0 => { self.pressed_keys.remove(&code); }
2 => { /* repeats, if needed */ }
_ => {}
}
// Build new HID report
let report_bytes = self.build_hid_report();
let _ = self.tx.try_send(HidReport {
data: report_bytes.to_vec(),
});
}
Ok(())
}
fn build_hid_report(&self) -> [u8; 8] {
// Byte 0: modifiers (bitmask)
// Byte 1: reserved
// Byte 2..7: up to 6 keys
// We'll do a naive approach: gather up to 6 non-modifier codes.
let mut report = [0u8; 8];
let mut normal_keys = Vec::new();
let mut modifier_mask = 0u8;
for &key in &self.pressed_keys {
if let Some(modbits) = is_modifier(key) {
// e.g. KEY_LEFTSHIFT => 0x02
modifier_mask |= modbits;
} else if let Some(usage) = keycode_to_usage(key) {
// Normal key
normal_keys.push(usage);
}
}
report[0] = modifier_mask;
// Byte[1] is reserved 0
for (i, code) in normal_keys.into_iter().take(6).enumerate() {
report[2 + i] = code;
}
report
}
fn is_magic_chord(&self) -> bool {
// All the keys in MAGIC_CHORD must be pressed
MAGIC_CHORD.iter().all(|k| self.pressed_keys.contains(k))
}
}

View File

@ -0,0 +1,99 @@
// client/src/input/keymap.rs
use evdev::KeyCode;
/// Return Some(usage) if we have a known mapping from evdev::KeyCode -> HID usage code
pub fn keycode_to_usage(key: KeyCode) -> Option<u8> {
match key {
// Letters
KeyCode::KEY_A => Some(0x04),
KeyCode::KEY_B => Some(0x05),
KeyCode::KEY_C => Some(0x06),
KeyCode::KEY_D => Some(0x07),
KeyCode::KEY_E => Some(0x08),
KeyCode::KEY_F => Some(0x09),
KeyCode::KEY_G => Some(0x0A),
KeyCode::KEY_H => Some(0x0B),
KeyCode::KEY_I => Some(0x0C),
KeyCode::KEY_J => Some(0x0D),
KeyCode::KEY_K => Some(0x0E),
KeyCode::KEY_L => Some(0x0F),
KeyCode::KEY_M => Some(0x10),
KeyCode::KEY_N => Some(0x11),
KeyCode::KEY_O => Some(0x12),
KeyCode::KEY_P => Some(0x13),
KeyCode::KEY_Q => Some(0x14),
KeyCode::KEY_R => Some(0x15),
KeyCode::KEY_S => Some(0x16),
KeyCode::KEY_T => Some(0x17),
KeyCode::KEY_U => Some(0x18),
KeyCode::KEY_V => Some(0x19),
KeyCode::KEY_W => Some(0x1A),
KeyCode::KEY_X => Some(0x1B),
KeyCode::KEY_Y => Some(0x1C),
KeyCode::KEY_Z => Some(0x1D),
// Number row
KeyCode::KEY_1 => Some(0x1E),
KeyCode::KEY_2 => Some(0x1F),
KeyCode::KEY_3 => Some(0x20),
KeyCode::KEY_4 => Some(0x21),
KeyCode::KEY_5 => Some(0x22),
KeyCode::KEY_6 => Some(0x23),
KeyCode::KEY_7 => Some(0x24),
KeyCode::KEY_8 => Some(0x25),
KeyCode::KEY_9 => Some(0x26),
KeyCode::KEY_0 => Some(0x27),
// Common punctuation
KeyCode::KEY_ENTER => Some(0x28),
KeyCode::KEY_ESC => Some(0x29),
KeyCode::KEY_BACKSPACE => Some(0x2A),
KeyCode::KEY_TAB => Some(0x2B),
KeyCode::KEY_SPACE => Some(0x2C),
KeyCode::KEY_MINUS => Some(0x2D),
KeyCode::KEY_EQUAL => Some(0x2E),
KeyCode::KEY_LEFTBRACE => Some(0x2F),
KeyCode::KEY_RIGHTBRACE => Some(0x30),
KeyCode::KEY_BACKSLASH => Some(0x31),
KeyCode::KEY_SEMICOLON => Some(0x33),
KeyCode::KEY_APOSTROPHE => Some(0x34),
KeyCode::KEY_GRAVE => Some(0x35),
KeyCode::KEY_COMMA => Some(0x36),
KeyCode::KEY_DOT => Some(0x37),
KeyCode::KEY_SLASH => Some(0x38),
// Function keys
KeyCode::KEY_CAPSLOCK => Some(0x39),
KeyCode::KEY_F1 => Some(0x3A),
KeyCode::KEY_F2 => Some(0x3B),
KeyCode::KEY_F3 => Some(0x3C),
KeyCode::KEY_F4 => Some(0x3D),
KeyCode::KEY_F5 => Some(0x3E),
KeyCode::KEY_F6 => Some(0x3F),
KeyCode::KEY_F7 => Some(0x40),
KeyCode::KEY_F8 => Some(0x41),
KeyCode::KEY_F9 => Some(0x42),
KeyCode::KEY_F10 => Some(0x43),
KeyCode::KEY_F11 => Some(0x44),
KeyCode::KEY_F12 => Some(0x45),
// We'll handle modifiers (ctrl, shift, alt, meta) in `is_modifier()`
_ => None,
}
}
/// If a key is a modifier, return the bit(s) to set in HID byte[0].
pub fn is_modifier(key: KeyCode) -> Option<u8> {
match key {
KeyCode::KEY_LEFTCTRL => Some(0x01),
KeyCode::KEY_LEFTSHIFT => Some(0x02),
KeyCode::KEY_LEFTALT => Some(0x04),
KeyCode::KEY_LEFTMETA => Some(0x08),
KeyCode::KEY_RIGHTCTRL => Some(0x10),
KeyCode::KEY_RIGHTSHIFT => Some(0x20),
KeyCode::KEY_RIGHTALT => Some(0x40),
KeyCode::KEY_RIGHTMETA => Some(0x80),
_ => None,
}
}

2
client/src/input/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod keyboard;
pub mod keymap;

6
client/src/lib.rs Normal file
View File

@ -0,0 +1,6 @@
#![forbid(unsafe_code)]
pub mod app;
pub mod input;
pub use app::NavkaClientApp;

View File

@ -1,45 +1,13 @@
//! navka-client forward keyboard/mouse HID reports to navka-server
//! main.rs — entry point for navka-client
#![forbid(unsafe_code)]
use anyhow::Result;
use navka_common::navka::{relay_client::RelayClient, HidReport};
use tokio::{sync::mpsc, time::sleep};
use tokio_stream::wrappers::ReceiverStream;
use tonic::{transport::Channel, Request};
use navka_client::NavkaClientApp;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
// -- server address comes from CLI arg, env or falls back to localhost
let addr = std::env::args()
.nth(1)
.or_else(|| std::env::var("NAVKA_SERVER_ADDR").ok())
.unwrap_or_else(|| "http://127.0.0.1:50051".to_owned());
let channel: Channel = Channel::from_shared(addr)?
.connect()
.await?;
// mpsc -> ReceiverStream -> bidirectional gRPC
let (tx, rx) = mpsc::channel::<HidReport>(32);
let outbound = ReceiverStream::new(rx);
let mut inbound = RelayClient::new(channel)
.stream(Request::new(outbound))
.await?
.into_inner();
// demo: press & release a
tokio::spawn(async move {
let press_a = HidReport { data: vec![0, 0, 0x04, 0, 0, 0, 0, 0] };
let release = HidReport { data: vec![0; 8] };
tx.send(press_a).await.ok();
sleep(std::time::Duration::from_millis(100)).await;
tx.send(release).await.ok();
});
while let Some(report) = inbound.message().await? {
tracing::info!(bytes=?report.data, "echo from server");
}
Ok(())
let mut app = NavkaClientApp::new()?;
app.run().await
}

View File

@ -0,0 +1,13 @@
#[cfg(test)]
mod tests {
use crate::input::keymap::{keycode_to_usage, is_modifier};
use evdev::Key;
#[test]
fn test_keycode_mapping() {
assert_eq!(keycode_to_usage(Key::KEY_A), Some(0x04));
assert_eq!(keycode_to_usage(Key::KEY_Z), Some(0x1D));
assert_eq!(keycode_to_usage(Key::KEY_LEFTCTRL), Some(0)); // this is "handled" by is_modifier
assert!(is_modifier(Key::KEY_LEFTCTRL).is_some());
}
}

1
client/src/tests/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod integration_test;

View File

@ -1,16 +1,16 @@
[package]
name = "navka-common"
version = "0.1.0"
edition = "2021"
build = "build.rs"
name = "navka_common"
version = "0.1.0"
edition = "2024"
build = "build.rs"
[dependencies]
tonic = "0.11"
prost = "0.12"
tonic = "0.13"
prost = "0.13"
[build-dependencies]
tonic-build = "0.11"
tonic-build = "0.13"
[lib]
name = "navka_common"
path = "src/lib.rs"
name = "navka_common"
path = "src/lib.rs"

View File

@ -1,14 +1,14 @@
[package]
name = "navka-server"
version = "0.1.0"
edition = "2024"
name = "navka_server"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { version = "1.45", features = ["full", "fs"] }
tokio-stream = "0.1"
tonic = { version = "0.11", features = ["transport"] }
tonic = { version = "0.13", features = ["transport"] }
anyhow = "1.0"
navka-common = { path = "../common" }
navka_common = { path = "../common" }
tracing = { version = "0.1", features = ["std"] }
tracing-subscriber = "0.3"
libc = "0.2"