From 06725f4b1492aa51287c2afdab8a1d86559169ab Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Mon, 25 Sep 2023 11:55:22 +0200 Subject: [PATCH] Frontend improvement (#27) * removed redundant dns lookups * frontend now correctly reflects the state of the backend * config.toml is loaded when starting gtk frontend --- README.md | 7 +- de.feschber.LanMouse.yml | 14 ++ src/backend/producer/wayland.rs | 28 ++- src/client.rs | 213 ++++++++--------- src/config.rs | 39 +--- src/dns.rs | 19 +- src/event/server.rs | 319 +++++++++++++++++++------- src/frontend.rs | 161 +++++++++++-- src/frontend/cli.rs | 216 ++++++++++++----- src/frontend/gtk.rs | 142 ++++++++++-- src/frontend/gtk/client_object.rs | 8 +- src/frontend/gtk/client_object/imp.rs | 3 + src/frontend/gtk/client_row.rs | 31 ++- src/frontend/gtk/client_row/imp.rs | 10 +- src/frontend/gtk/window.rs | 99 +++++--- src/frontend/gtk/window/imp.rs | 5 +- src/main.rs | 26 +-- 17 files changed, 908 insertions(+), 432 deletions(-) create mode 100644 de.feschber.LanMouse.yml diff --git a/README.md b/README.md index ab163bf..e4c51a9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Lan Mouse Share +# Lan Mouse ![image](https://github.com/ferdinandschober/lan-mouse/assets/40996949/ccb33815-4357-4c8d-a5d2-8897ab626a08) @@ -19,7 +19,7 @@ which must be located in the current working directory when executing lan-mouse. ### Example config -A minimal config file could look like this: +A config file could look like this: ```toml # example configuration @@ -50,9 +50,6 @@ port = 4242 Where `left` can be either `left`, `right`, `top` or `bottom`. -> :warning: Note that clients from the config -> file are currently ignored when using the gtk frontend! - ## Build and Run Build in release mode: diff --git a/de.feschber.LanMouse.yml b/de.feschber.LanMouse.yml new file mode 100644 index 0000000..0778107 --- /dev/null +++ b/de.feschber.LanMouse.yml @@ -0,0 +1,14 @@ +app-id: de.feschber.LanMouse +runtime: org.freedesktop.Platform +runtime-version: '22.08' +sdk: org.freedesktop.Sdk +command: target/release/lan-mouse +modules: + - name: hello + buildsystem: simple + build-commands: + - cargo build --release + - install -D lan-mouse /app/bin/lan-mouse + sources: + - type: file + path: target/release/lan-mouse diff --git a/src/backend/producer/wayland.rs b/src/backend/producer/wayland.rs index 4c97c33..fcae3a3 100644 --- a/src/backend/producer/wayland.rs +++ b/src/backend/producer/wayland.rs @@ -528,17 +528,23 @@ impl EventProducer for WaylandEventProducer { } fn notify(&mut self, client_event: ClientEvent) { - if let ClientEvent::Create(handle, pos) = client_event { - self.state.add_client(handle, pos); - } - if let ClientEvent::Destroy(handle) = client_event { - loop { - // remove all windows corresponding to this client - if let Some(i) = self.state.client_for_window.iter().position(|(_,c)| *c == handle) { - self.state.client_for_window.remove(i); - self.state.focused = None; - } else { - break + match client_event { + ClientEvent::Create(handle, pos) => { + self.state.add_client(handle, pos); + } + ClientEvent::Destroy(handle) => { + loop { + // remove all windows corresponding to this client + if let Some(i) = self + .state + .client_for_window + .iter() + .position(|(_,c)| *c == handle) { + self.state.client_for_window.remove(i); + self.state.focused = None; + } else { + break + } } } } diff --git a/src/client.rs b/src/client.rs index 2c4c0bf..a2a90ee 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,4 +1,4 @@ -use std::{net::SocketAddr, collections::{HashSet, hash_set::Iter}, fmt::Display, time::{Instant, Duration}, iter::Cloned}; +use std::{net::{SocketAddr, IpAddr}, collections::HashSet, fmt::Display, time::Instant}; use serde::{Serialize, Deserialize}; @@ -10,6 +10,12 @@ pub enum Position { Bottom, } +impl Default for Position { + fn default() -> Self { + Self::Left + } +} + impl Position { pub fn opposite(&self) -> Self { match self { @@ -34,7 +40,9 @@ impl Display for Position { #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] pub struct Client { - /// handle to refer to the client. + /// hostname of this client + pub hostname: Option, + /// unique handle to refer to the client. /// This way any event consumer / producer backend does not /// need to know anything about a client other than its handle. pub handle: ClientHandle, @@ -46,6 +54,8 @@ pub struct Client { /// e.g. Laptops usually have at least an ethernet and a wifi port /// which have different ip addresses pub addrs: HashSet, + /// both active_addr and addrs can be None / empty so port needs to be stored seperately + pub port: u16, /// position of a client on screen pub pos: Position, } @@ -53,159 +63,118 @@ pub struct Client { pub enum ClientEvent { Create(ClientHandle, Position), Destroy(ClientHandle), - UpdatePos(ClientHandle, Position), - AddAddr(ClientHandle, SocketAddr), - RemoveAddr(ClientHandle, SocketAddr), } pub type ClientHandle = u32; +#[derive(Debug, Clone)] +pub struct ClientState { + pub client: Client, + pub active: bool, + pub last_ping: Option, + pub last_seen: Option, + pub last_replied: Option, +} + pub struct ClientManager { - /// probably not beneficial to use a hashmap here - clients: Vec, - last_ping: Vec<(ClientHandle, Option)>, - last_seen: Vec<(ClientHandle, Option)>, - last_replied: Vec<(ClientHandle, Option)>, - next_client_id: u32, + clients: Vec>, // HashMap likely not beneficial } impl ClientManager { pub fn new() -> Self { Self { clients: vec![], - next_client_id: 0, - last_ping: vec![], - last_seen: vec![], - last_replied: vec![], } } /// add a new client to this manager - pub fn add_client(&mut self, addrs: HashSet, pos: Position) -> ClientHandle { - let handle = self.next_id(); + pub fn add_client( + &mut self, + hostname: Option, + addrs: HashSet, + port: u16, + pos: Position, + ) -> ClientHandle { + // get a new client_handle + let handle = self.free_id(); + // we dont know, which IP is initially active let active_addr = None; + // map ip addresses to socket addresses + let addrs = HashSet::from_iter( + addrs + .into_iter() + .map(|ip| SocketAddr::new(ip, port)) + ); + // store the client - let client = Client { handle, active_addr, addrs, pos }; - self.clients.push(client); - self.last_ping.push((handle, None)); - self.last_seen.push((handle, None)); - self.last_replied.push((handle, None)); + let client = Client { hostname, handle, active_addr, addrs, port, pos }; + + // client was never seen, nor pinged + let client_state = ClientState { + client, + last_ping: None, + last_seen: None, + last_replied: None, + active: false, + }; + + if handle as usize >= self.clients.len() { + assert_eq!(handle as usize, self.clients.len()); + self.clients.push(Some(client_state)); + } else { + self.clients[handle as usize] = Some(client_state); + } handle } - /// add a socket address to the given client - pub fn add_addr(&mut self, client: ClientHandle, addr: SocketAddr) { - if let Some(client) = self.get_mut(client) { - client.addrs.insert(addr); - } - } - - /// remove socket address from the given client - pub fn remove_addr(&mut self, client: ClientHandle, addr: SocketAddr) { - if let Some(client) = self.get_mut(client) { - client.addrs.remove(&addr); - } - } - - pub fn set_default_addr(&mut self, client: ClientHandle, addr: SocketAddr) { - if let Some(client) = self.get_mut(client) { - client.active_addr = Some(addr) - } - } - - /// update the position of a client - pub fn update_pos(&mut self, client: ClientHandle, pos: Position) { - if let Some(client) = self.get_mut(client) { - client.pos = pos; - } - } - - pub fn get_active_addr(&self, client: ClientHandle) -> Option { - self.get(client)?.active_addr - } - - pub fn get_addrs(&self, client: ClientHandle) -> Option>> { - Some(self.get(client)?.addrs.iter().cloned()) - } - - pub fn last_ping(&self, client: ClientHandle) -> Option { - let last_ping = self.last_ping - .iter() - .find(|(c,_)| *c == client)?.1; - last_ping.map(|p| p.elapsed()) - } - - pub fn last_seen(&self, client: ClientHandle) -> Option { - let last_seen = self.last_seen - .iter() - .find(|(c, _)| *c == client)?.1; - last_seen.map(|t| t.elapsed()) - } - - pub fn last_replied(&self, client: ClientHandle) -> Option { - let last_replied = self.last_replied - .iter() - .find(|(c, _)| *c == client)?.1; - last_replied.map(|t| t.elapsed()) - } - - pub fn reset_last_ping(&mut self, client: ClientHandle) { - if let Some(c) = self.last_ping - .iter_mut() - .find(|(c, _)| *c == client) { - c.1 = Some(Instant::now()); - } - } - - pub fn reset_last_seen(&mut self, client: ClientHandle) { - if let Some(c) = self.last_seen - .iter_mut() - .find(|(c, _)| *c == client) { - c.1 = Some(Instant::now()); - } - } - - pub fn reset_last_replied(&mut self, client: ClientHandle) { - if let Some(c) = self.last_replied - .iter_mut() - .find(|(c, _)| *c == client) { - c.1 = Some(Instant::now()); - } - } - + /// find a client by its address pub fn get_client(&self, addr: SocketAddr) -> Option { + // since there shouldn't be more than a handful of clients at any given + // time this is likely faster than using a HashMap self.clients .iter() - .find(|c| c.addrs.contains(&addr)) - .map(|c| c.handle) + .position(|c| if let Some(c) = c { + c.active && c.client.addrs.contains(&addr) + } else { + false + }) + .map(|p| p as ClientHandle) } - pub fn remove_client(&mut self, client: ClientHandle) { - if let Some(i) = self.clients.iter().position(|c| c.handle == client) { - self.clients.remove(i); - self.last_ping.remove(i); - self.last_seen.remove(i); - self.last_replied.remove(i); + /// remove a client from the list + pub fn remove_client(&mut self, client: ClientHandle) -> Option { + // remove id from occupied ids + self.clients.get_mut(client as usize)?.take() + } + + /// get a free slot in the client list + fn free_id(&mut self) -> ClientHandle { + for i in 0..u32::MAX { + if self.clients.get(i as usize).is_none() + || self.clients.get(i as usize).unwrap().is_none() { + return i; + } } + panic!("Out of client ids"); } - fn next_id(&mut self) -> ClientHandle { - let handle = self.next_client_id; - self.next_client_id += 1; - handle + // returns an immutable reference to the client state corresponding to `client` + pub fn get<'a>(&'a self, client: ClientHandle) -> Option<&'a ClientState> { + self.clients.get(client as usize)?.as_ref() } - fn get<'a>(&'a self, client: ClientHandle) -> Option<&'a Client> { + /// returns a mutable reference to the client state corresponding to `client` + pub fn get_mut<'a>(&'a mut self, client: ClientHandle) -> Option<&'a mut ClientState> { + self.clients.get_mut(client as usize)?.as_mut() + } + + pub fn enumerate(&self) -> Vec<(Client, bool)> { self.clients .iter() - .find(|c| c.handle == client) - } - - fn get_mut<'a>(&'a mut self, client: ClientHandle) -> Option<&'a mut Client> { - self.clients - .iter_mut() - .find(|c| c.handle == client) + .filter_map(|s|s.as_ref()) + .map(|s| (s.client.clone(), s.active)) + .collect() } } diff --git a/src/config.rs b/src/config.rs index fef7966..ea131cc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,14 +1,13 @@ use serde::{Deserialize, Serialize}; use core::fmt; use std::collections::HashSet; -use std::net::{IpAddr, SocketAddr}; +use std::net::IpAddr; use std::{error::Error, fs}; use std::env; use toml; use crate::client::Position; -use crate::dns; pub const DEFAULT_PORT: u16 = 4242; @@ -136,40 +135,16 @@ impl Config { Ok(Config { frontend, clients, port }) } - pub fn get_clients(&self) -> Vec<(HashSet, Option, Position)> { + pub fn get_clients(&self) -> Vec<(HashSet, Option, u16, Position)> { self.clients.iter().map(|(c,p)| { let port = c.port.unwrap_or(DEFAULT_PORT); - // add ips from config - let config_ips: Vec = if let Some(ips) = c.ips.as_ref() { - ips.iter().cloned().collect() + let ips: HashSet = if let Some(ips) = c.ips.as_ref() { + HashSet::from_iter(ips.iter().cloned()) } else { - vec![] + HashSet::new() }; let host_name = c.host_name.clone(); - // add ips from dns lookup - let dns_ips = match host_name.as_ref() { - None => vec![], - Some(host_name) => match dns::resolve(host_name) { - Err(e) => { - log::warn!("{host_name}: could not resolve host: {e}"); - vec![] - } - Ok(l) if l.is_empty() => { - log::warn!("{host_name}: could not resolve host"); - vec![] - } - Ok(l) => l, - } - }; - if config_ips.is_empty() && dns_ips.is_empty() { - log::error!("no ips found for client {p:?}, ignoring!"); - log::error!("You can manually specify ip addresses via the `ips` config option"); - } - let ips = config_ips.into_iter().chain(dns_ips.into_iter()); - - // map ip addresses to socket addresses - let addrs: HashSet = ips.map(|ip| SocketAddr::new(ip, port)).collect(); - (addrs, host_name, *p) - }).filter(|(a, _, _)| !a.is_empty()).collect() + (ips, host_name, port, *p) + }).collect() } } diff --git a/src/dns.rs b/src/dns.rs index 6e535f1..bef5d5e 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -1,9 +1,20 @@ +use anyhow::Result; use std::{error::Error, net::IpAddr}; use trust_dns_resolver::Resolver; -pub fn resolve(host: &str) -> Result, Box> { - log::info!("resolving {host} ..."); - let response = Resolver::from_system_conf()?.lookup_ip(host)?; - Ok(response.iter().collect()) +pub(crate) struct DnsResolver { + resolver: Resolver, +} +impl DnsResolver { + pub(crate) fn new() -> Result { + let resolver = Resolver::from_system_conf()?; + Ok(Self { resolver }) + } + + pub(crate) fn resolve(&self, host: &str) -> Result, Box> { + log::info!("resolving {host} ..."); + let response = self.resolver.lookup_ip(host)?; + Ok(response.iter().collect()) + } } diff --git a/src/event/server.rs b/src/event/server.rs index 823f336..273f1e5 100644 --- a/src/event/server.rs +++ b/src/event/server.rs @@ -1,12 +1,12 @@ -use std::{error::Error, io::Result, collections::HashSet, time::Duration}; +use std::{error::Error, io::Result, collections::HashSet, time::{Duration, Instant}, net::IpAddr}; use log; -use mio::{Events, Poll, Interest, Token, net::UdpSocket}; +use mio::{Events, Poll, Interest, Token, net::UdpSocket, event::Source}; #[cfg(not(windows))] use mio_signals::{Signals, Signal, SignalSet}; use std::{net::SocketAddr, io::ErrorKind}; -use crate::{client::{ClientEvent, ClientManager, Position}, consumer::EventConsumer, producer::EventProducer, frontend::{FrontendEvent, FrontendAdapter}, dns}; +use crate::{client::{ClientEvent, ClientManager, Position, ClientHandle}, consumer::EventConsumer, producer::EventProducer, frontend::{FrontendEvent, FrontendListener, FrontendNotify}, dns::{self, DnsResolver}}; use super::Event; /// keeps track of state to prevent a feedback loop @@ -22,11 +22,13 @@ pub struct Server { socket: UdpSocket, producer: Box, consumer: Box, + resolver: DnsResolver, #[cfg(not(windows))] signals: Signals, - frontend: FrontendAdapter, + frontend: FrontendListener, client_manager: ClientManager, state: State, + next_token: usize, } const UDP_RX: Token = Token(0); @@ -35,17 +37,22 @@ const PRODUCER_RX: Token = Token(2); #[cfg(not(windows))] const SIGNAL: Token = Token(3); +const MAX_TOKEN: usize = 4; + impl Server { pub fn new( port: u16, mut producer: Box, consumer: Box, - mut frontend: FrontendAdapter, - ) -> Result { + mut frontend: FrontendListener, + ) -> anyhow::Result { // bind the udp socket let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), port); let mut socket = UdpSocket::bind(listen_addr)?; + // create dns resolver + let resolver = dns::DnsResolver::new()?; + // register event sources let poll = Poll::new()?; @@ -55,7 +62,7 @@ impl Server { #[cfg(not(windows))] poll.registry().register(&mut signals, SIGNAL, Interest::READABLE)?; - poll.registry().register(&mut socket, UDP_RX, Interest::READABLE | Interest::WRITABLE)?; + poll.registry().register(&mut socket, UDP_RX, Interest::READABLE)?; poll.registry().register(&mut producer, PRODUCER_RX, Interest::READABLE)?; poll.registry().register(&mut frontend, FRONTEND_RX, Interest::READABLE)?; @@ -63,10 +70,12 @@ impl Server { let client_manager = ClientManager::new(); Ok(Server { poll, socket, consumer, producer, + resolver, #[cfg(not(windows))] signals, frontend, client_manager, state: State::Receiving, + next_token: MAX_TOKEN, }) } @@ -83,34 +92,105 @@ impl Server { match event.token() { UDP_RX => self.handle_udp_rx(), PRODUCER_RX => self.handle_producer_rx(), - FRONTEND_RX => if self.handle_frontend_rx() { return Ok(()) }, + FRONTEND_RX => self.handle_frontend_incoming(), #[cfg(not(windows))] SIGNAL => if self.handle_signal() { return Ok(()) }, - _ => panic!("what happened here?") + _ => if self.handle_frontend_event(event.token()) { return Ok(()) }, } } } } - pub fn add_client(&mut self, addr: HashSet, pos: Position) { - let client = self.client_manager.add_client(addr, pos); + pub fn add_client(&mut self, hostname: Option, mut addr: HashSet, port: u16, pos: Position) -> ClientHandle { + let ips = if let Some(hostname) = hostname.as_ref() { + HashSet::from_iter(self.resolver.resolve(hostname.as_str()).ok().iter().flatten().cloned()) + } else { + HashSet::new() + }; + addr.extend(ips.iter()); + log::info!("adding client [{}]{} @ {:?}", pos, hostname.as_deref().unwrap_or(""), &ips); + let client = self.client_manager.add_client(hostname.clone(), addr, port, pos); log::debug!("add_client {client}"); - self.producer.notify(ClientEvent::Create(client, pos)); - self.consumer.notify(ClientEvent::Create(client, pos)); + let notify = FrontendNotify::NotifyClientCreate(client, hostname, port, pos); + if let Err(e) = self.frontend.notify_all(notify) { + log::error!("{e}"); + }; + client } - pub fn remove_client(&mut self, host: String, port: u16) { - if let Ok(ips) = dns::resolve(host.as_str()) { - if let Some(ip) = ips.iter().next() { - let addr = SocketAddr::new(*ip, port); - if let Some(handle) = self.client_manager.get_client(addr) { - log::debug!("remove_client {handle}"); - self.client_manager.remove_client(handle); - self.producer.notify(ClientEvent::Destroy(handle)); - self.consumer.notify(ClientEvent::Destroy(handle)); + pub fn activate_client(&mut self, client: ClientHandle, active: bool) { + if let Some(state) = self.client_manager.get_mut(client) { + state.active = active; + if state.active { + self.producer.notify(ClientEvent::Create(client, state.client.pos)); + self.consumer.notify(ClientEvent::Create(client, state.client.pos)); + } else { + self.producer.notify(ClientEvent::Destroy(client)); + self.consumer.notify(ClientEvent::Destroy(client)); + } + } + } + + pub fn remove_client(&mut self, client: ClientHandle) -> Option { + self.producer.notify(ClientEvent::Destroy(client)); + self.consumer.notify(ClientEvent::Destroy(client)); + if let Some(client) = self.client_manager.remove_client(client).map(|s| s.client.handle) { + let notify = FrontendNotify::NotifyClientDelete(client); + log::debug!("{notify:?}"); + if let Err(e) = self.frontend.notify_all(notify) { + log::error!("{e}"); + } + Some(client) + } else { + None + } + } + + pub fn update_client( + &mut self, + client: ClientHandle, + hostname: Option, + port: u16, + pos: Position, + ) { + // retrieve state + let Some(state) = self.client_manager.get_mut(client) else { + return + }; + + // update pos + state.client.pos = pos; + if state.active { + self.producer.notify(ClientEvent::Destroy(client)); + self.consumer.notify(ClientEvent::Destroy(client)); + self.producer.notify(ClientEvent::Create(client, pos)); + self.consumer.notify(ClientEvent::Create(client, pos)); + } + + // update port + if state.client.port != port { + state.client.port = port; + state.client.addrs = state.client.addrs + .iter() + .cloned() + .map(|mut a| { a.set_port(port); a }) + .collect(); + state.client.active_addr.map(|a| { SocketAddr::new(a.ip(), port) }); + } + + // update hostname + if state.client.hostname != hostname { + state.client.addrs = HashSet::new(); + state.client.active_addr = None; + state.client.hostname = hostname; + if let Some(hostname) = state.client.hostname.as_ref() { + if let Ok(ips) = self.resolver.resolve(hostname.as_str()) { + let addrs = ips.iter().map(|i| SocketAddr::new(*i, port)); + state.client.addrs = HashSet::from_iter(addrs); } } } + log::debug!("client updated: {:?}", state); } fn handle_udp_rx(&mut self) { @@ -129,7 +209,6 @@ impl Server { continue } }; - log::trace!("{:20} <-<-<-<------ {addr}", event.to_string()); // get handle for addr let handle = match self.client_manager.get_client(addr) { @@ -139,10 +218,19 @@ impl Server { continue } }; + log::trace!("{:20} <-<-<-<------ {addr} ({handle})", event.to_string()); + let state = match self.client_manager.get_mut(handle) { + Some(s) => s, + None => { + log::error!("unknown handle"); + continue + } + }; - // reset ttl for client and set addr as new default for this client - self.client_manager.reset_last_seen(handle); - self.client_manager.set_default_addr(handle, addr); + // reset ttl for client and + state.last_seen = Some(Instant::now()); + // set addr as new default for this client + state.client.active_addr = Some(addr); match (event, addr) { (Event::Pong(), _) => {}, (Event::Ping(), addr) => { @@ -153,32 +241,31 @@ impl Server { // since its very likely, that we wont get a release event self.producer.release(); } - (event, addr) => { - match self.state { - State::Sending => { - // in sending state, we dont want to process - // any events to avoid feedback loops, - // therefore we tell the event producer - // to release the pointer and move on - // first event -> release pointer - if let Event::Release() = event { - log::debug!("releasing pointer ..."); - self.producer.release(); - self.state = State::Receiving; - } + (event, addr) => match self.state { + State::Sending => { + // in sending state, we dont want to process + // any events to avoid feedback loops, + // therefore we tell the event producer + // to release the pointer and move on + // first event -> release pointer + if let Event::Release() = event { + log::debug!("releasing pointer ..."); + self.producer.release(); + self.state = State::Receiving; } - State::Receiving => { - // consume event - self.consumer.consume(event, handle); + } + State::Receiving => { + // consume event + self.consumer.consume(event, handle); - // let the server know we are still alive once every second - let last_replied = self.client_manager.last_replied(handle); - if last_replied.is_none() - || last_replied.is_some() && last_replied.unwrap() > Duration::from_secs(1) { - self.client_manager.reset_last_replied(handle); - if let Err(e) = Self::send_event(&self.socket, Event::Pong(), addr) { - log::error!("udp send: {}", e); - } + // let the server know we are still alive once every second + let last_replied = state.last_replied; + if last_replied.is_none() + || last_replied.is_some() + && last_replied.unwrap().elapsed() > Duration::from_secs(1) { + state.last_replied = Some(Instant::now()); + if let Err(e) = Self::send_event(&self.socket, Event::Pong(), addr) { + log::error!("udp send: {}", e); } } } @@ -196,10 +283,18 @@ impl Server { if let Event::Release() = e { self.state = State::Sending; } + + log::trace!("producer: ({c}) {e:?}"); + let state = match self.client_manager.get_mut(c) { + Some(state) => state, + None => { + log::warn!("unknown client!"); + continue + } + }; // otherwise we should have an address to send to // transmit events to the corrensponding client - if let Some(addr) = self.client_manager.get_active_addr(c) { - log::trace!("{:20} ------>->->-> {addr}", e.to_string()); + if let Some(addr) = state.client.active_addr { if let Err(e) = Self::send_event(&self.socket, e, addr) { log::error!("udp send: {}", e); } @@ -208,41 +303,38 @@ impl Server { // if client last responded > 2 seconds ago // and we have not sent a ping since 500 milliseconds, // send a ping - let last_seen = self.client_manager.last_seen(c); - let last_ping = self.client_manager.last_ping(c); - if last_seen.is_some() && last_seen.unwrap() < Duration::from_secs(2) { + if state.last_seen.is_some() + && state.last_seen.unwrap().elapsed() < Duration::from_secs(2) { continue } // client last seen > 500ms ago - if last_ping.is_some() && last_ping.unwrap() < Duration::from_millis(500) { + if state.last_ping.is_some() + && state.last_ping.unwrap().elapsed() < Duration::from_millis(500) { continue } // release mouse if client didnt respond to the first ping - if last_ping.is_some() && last_ping.unwrap() < Duration::from_secs(1) { + if state.last_ping.is_some() + && state.last_ping.unwrap().elapsed() < Duration::from_secs(1) { should_release = true; } // last ping > 500ms ago -> ping all interfaces - self.client_manager.reset_last_ping(c); - if let Some(iter) = self.client_manager.get_addrs(c) { - for addr in iter { - log::debug!("pinging {addr}"); - if let Err(e) = Self::send_event(&self.socket, Event::Ping(), addr) { - if e.kind() != ErrorKind::WouldBlock { - log::error!("udp send: {}", e); - } - } - // send additional release event, in case client is still in sending mode - if let Err(e) = Self::send_event(&self.socket, Event::Release(), addr) { - if e.kind() != ErrorKind::WouldBlock { - log::error!("udp send: {}", e); - } + state.last_ping = Some(Instant::now()); + for addr in state.client.addrs.iter() { + log::debug!("pinging {addr}"); + if let Err(e) = Self::send_event(&self.socket, Event::Ping(), *addr) { + if e.kind() != ErrorKind::WouldBlock { + log::error!("udp send: {}", e); + } + } + // send additional release event, in case client is still in sending mode + if let Err(e) = Self::send_event(&self.socket, Event::Release(), *addr) { + if e.kind() != ErrorKind::WouldBlock { + log::error!("udp send: {}", e); } } - } else { - // TODO should repeat dns lookup } } @@ -254,32 +346,70 @@ impl Server { } - fn handle_frontend_rx(&mut self) -> bool { + fn handle_frontend_incoming(&mut self) { loop { - match self.frontend.read_event() { - Ok(event) => match event { - FrontendEvent::AddClient(host, port, pos) => { - if let Ok(ips) = dns::resolve(host.as_str()) { - let addrs = ips.iter().map(|i| SocketAddr::new(*i, port)); - self.add_client(HashSet::from_iter(addrs), pos); - } + let token = self.fresh_token(); + let poll = &mut self.poll; + match self.frontend.handle_incoming(|s, i| { + poll.registry().register(s, token, i)?; + Ok(token) + }) { + Err(e) if e.kind() == ErrorKind::WouldBlock => break, + Err(e) if e.kind() == ErrorKind::Interrupted => continue, + Err(e) => { + log::error!("{e}"); + break + } + _ => continue, + } + } + // notify new frontend connections of current clients + self.enumerate(); + } + + fn handle_frontend_event(&mut self, token: Token) -> bool { + loop { + let event = match self.frontend.read_event(token) { + Ok(event) => event, + Err(e) if e.kind() == ErrorKind::WouldBlock => return false, + Err(e) if e.kind() == ErrorKind::Interrupted => continue, + Err(e) => { + log::error!("{e}"); + return false; + } + }; + if let Some(event) = event { + log::debug!("frontend: {event:?}"); + match event { + FrontendEvent::AddClient(hostname, port, pos) => { + self.add_client(hostname, HashSet::new(), port, pos); } - FrontendEvent::DelClient(host, port) => self.remove_client(host, port), + FrontendEvent::ActivateClient(client, active) => { + self.activate_client(client, active); + } + FrontendEvent::DelClient(client) => { + self.remove_client(client); + } + FrontendEvent::UpdateClient(client, hostname, port, pos) => { + self.update_client(client, hostname, port, pos); + } + FrontendEvent::Enumerate() => self.enumerate(), FrontendEvent::Shutdown() => { log::info!("terminating gracefully..."); return true; }, - FrontendEvent::ChangePort(_) => todo!(), - FrontendEvent::AddIp(_, _) => todo!(), - } - Err(e) if e.kind() == ErrorKind::WouldBlock => return false, - Err(e) => { - log::error!("frontend: {e}"); } } } } + fn enumerate(&mut self) { + let clients = self.client_manager.enumerate(); + if let Err(e) = self.frontend.notify_all(FrontendNotify::Enumerate(clients)) { + log::error!("{e}"); + } + } + #[cfg(not(windows))] fn handle_signal(&mut self) -> bool { #[cfg(windows)] @@ -306,6 +436,7 @@ impl Server { } fn send_event(sock: &UdpSocket, e: Event, addr: SocketAddr) -> Result { + log::trace!("{:20} ------>->->-> {addr}", e.to_string()); let data: Vec = (&e).into(); // We are currently abusing a blocking send to get the lowest possible latency. // It may be better to set the socket to non-blocking and only send when ready. @@ -319,4 +450,16 @@ impl Server { Err(e) => Err(Box::new(e)), } } + + fn fresh_token(&mut self) -> Token { + let token = self.next_token as usize; + self.next_token += 1; + Token(token) + } + + pub fn register_frontend(&mut self, source: &mut dyn Source, interests: Interest) -> Result { + let token = self.fresh_token(); + self.poll.registry().register(source, token, interests)?; + Ok(token) + } } diff --git a/src/frontend.rs b/src/frontend.rs index ae681ab..323c67c 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -1,20 +1,25 @@ -use std::io::{Read, Result}; -use std::net::IpAddr; +use std::collections::HashMap; +use std::io::{Read, Result, Write}; use std::str; #[cfg(unix)] use std::{env, path::{Path, PathBuf}}; +use mio::Interest; use mio::{Registry, Token, event::Source}; +#[cfg(unix)] +use mio::net::UnixStream; #[cfg(unix)] use mio::net::UnixListener; #[cfg(windows)] +use mio::net::TcpStream; +#[cfg(windows)] use mio::net::TcpListener; use serde::{Serialize, Deserialize}; -use crate::client::{Client, Position}; +use crate::client::{Position, ClientHandle, Client}; /// cli frontend pub mod cli; @@ -25,29 +30,40 @@ pub mod gtk; #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] pub enum FrontendEvent { - ChangePort(u16), - AddClient(String, u16, Position), - DelClient(String, u16), - AddIp(String, Option), + /// add a new client + AddClient(Option, u16, Position), + /// activate/deactivate client + ActivateClient(ClientHandle, bool), + /// update a client (hostname, port, position) + UpdateClient(ClientHandle, Option, u16, Position), + /// remove a client + DelClient(ClientHandle), + /// request an enumertaion of all clients + Enumerate(), + /// service shutdown Shutdown(), } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum FrontendNotify { - NotifyClientCreate(Client), + NotifyClientCreate(ClientHandle, Option, u16, Position), + NotifyClientUpdate(ClientHandle, Option, u16, Position), + NotifyClientDelete(ClientHandle), + Enumerate(Vec<(Client, bool)>), NotifyError(String), } -pub struct FrontendAdapter { +pub struct FrontendListener { #[cfg(windows)] listener: TcpListener, #[cfg(unix)] listener: UnixListener, #[cfg(unix)] socket_path: PathBuf, + frontend_connections: HashMap, } -impl FrontendAdapter { +impl FrontendListener { pub fn new() -> std::result::Result> { #[cfg(unix)] let socket_path = Path::new(env::var("XDG_RUNTIME_DIR")?.as_str()).join("lan-mouse-socket.sock"); @@ -67,28 +83,57 @@ impl FrontendAdapter { listener, #[cfg(unix)] socket_path, + frontend_connections: HashMap::new(), }; Ok(adapter) } - pub fn read_event(&mut self) -> Result{ + #[cfg(unix)] + pub fn handle_incoming(&mut self, register_frontend: F) -> Result<()> + where F: Fn(&mut UnixStream, Interest) -> Result { let (mut stream, _) = self.listener.accept()?; - let mut buf = [0u8; 128]; // FIXME - stream.read(&mut buf)?; - let json = str::from_utf8(&buf) - .unwrap() - .trim_end_matches(char::from(0)); // remove trailing 0-bytes - log::debug!("{json}"); - let event = serde_json::from_str(json).unwrap(); - log::debug!("{:?}", event); - Ok(event) + let token = register_frontend(&mut stream, Interest::READABLE)?; + let con = FrontendConnection::new(stream); + self.frontend_connections.insert(token, con); + Ok(()) } - pub fn notify(&self, _event: FrontendNotify) { } + #[cfg(windows)] + pub fn handle_incoming(&mut self, register_frontend: F) -> Result<()> + where F: Fn(&mut TcpStream, Interest) -> Result { + let (mut stream, _) = self.listener.accept()?; + let token = register_frontend(&mut stream, Interest::READABLE)?; + let con = FrontendConnection::new(stream); + self.frontend_connections.insert(token, con); + Ok(()) + } + + pub fn read_event(&mut self, token: Token) -> Result> { + if let Some(con) = self.frontend_connections.get_mut(&token) { + con.handle_event() + } else { + panic!("unknown token"); + } + } + + pub(crate) fn notify_all(&mut self, notify: FrontendNotify) -> Result<()> { + // encode event + let json = serde_json::to_string(¬ify).unwrap(); + let payload = json.as_bytes(); + let len = payload.len().to_ne_bytes(); + log::debug!("json: {json}, len: {}", payload.len()); + + for con in self.frontend_connections.values_mut() { + // write len + payload + con.stream.write(&len)?; + con.stream.write(payload)?; + } + Ok(()) + } } -impl Source for FrontendAdapter { +impl Source for FrontendListener { fn register( &mut self, registry: &Registry, @@ -113,9 +158,79 @@ impl Source for FrontendAdapter { } #[cfg(unix)] -impl Drop for FrontendAdapter { +impl Drop for FrontendListener { fn drop(&mut self) { log::debug!("remove socket: {:?}", self.socket_path); let _ = std::fs::remove_file(&self.socket_path); } } + +enum ReceiveState { + Len, Data, +} + +pub struct FrontendConnection { + #[cfg(unix)] + stream: UnixStream, + #[cfg(windows)] + stream: TcpStream, + state: ReceiveState, + len: usize, + len_buf: [u8; std::mem::size_of::()], + recieve_buf: [u8; 256], // FIXME + pos: usize, +} + +impl FrontendConnection { + #[cfg(unix)] + pub fn new(stream: UnixStream) -> Self { + Self { + stream, + state: ReceiveState::Len, + len: 0, + len_buf: [0u8; std::mem::size_of::()], + recieve_buf: [0u8; 256], + pos: 0, + } + } + + #[cfg(windows)] + pub fn new(stream: TcpStream) -> Self { + Self { + stream, + state: ReceiveState::Len, + len: 0, + len_buf: [0u8; std::mem::size_of::()], + recieve_buf: [0u8; 256], + pos: 0, + } + } + + pub fn handle_event(&mut self) -> Result> { + match self.state { + ReceiveState::Len => { + // we receive sizeof(usize) Bytes + let n = self.stream.read(&mut self.len_buf)?; + self.pos += n; + if self.pos == self.len_buf.len() { + self.state = ReceiveState::Data; + self.len = usize::from_ne_bytes(self.len_buf); + self.pos = 0; + } + Ok(None) + }, + ReceiveState::Data => { + // read at most as many bytes as the length of the next event + let n = self.stream.read(&mut self.recieve_buf[..self.len])?; + self.pos += n; + if n == self.len { + self.state = ReceiveState::Len; + self.pos = 0; + Ok(Some(serde_json::from_slice(&self.recieve_buf[..self.len])?)) + } else { + Ok(None) + } + } + } + } +} diff --git a/src/frontend/cli.rs b/src/frontend/cli.rs index d94f128..383ad0a 100644 --- a/src/frontend/cli.rs +++ b/src/frontend/cli.rs @@ -1,5 +1,5 @@ -use anyhow::Result; -use std::{thread::{self, JoinHandle}, io::Write}; +use anyhow::{anyhow, Result, Context}; +use std::{thread::{self, JoinHandle}, io::{Write, Read, ErrorKind}, str::SplitWhitespace}; #[cfg(windows)] use std::net::SocketAddrV4; @@ -10,90 +10,188 @@ use std::net::TcpStream; use crate::{client::Position, config::DEFAULT_PORT}; -use super::FrontendEvent; +use super::{FrontendEvent, FrontendNotify}; -pub fn start() -> Result> { +pub fn start() -> Result<(JoinHandle<()>, JoinHandle<()>)> { #[cfg(unix)] let socket_path = Path::new(env::var("XDG_RUNTIME_DIR")?.as_str()).join("lan-mouse-socket.sock"); - Ok(thread::Builder::new() + + #[cfg(unix)] + let Ok(mut tx) = UnixStream::connect(&socket_path) else { + return Err(anyhow!("Could not connect to lan-mouse-socket")); + }; + + #[cfg(windows)] + let Ok(mut tx) = TcpStream::connect("127.0.0.1:5252".parse::().unwrap()) else { + return Err(anyhow!("Could not connect to lan-mouse-socket")); + }; + + let mut rx = tx.try_clone()?; + + let reader = thread::Builder::new() .name("cli-frontend".to_string()) .spawn(move || { + // all further prompts + prompt(); loop { - eprint!("lan-mouse > "); - std::io::stderr().flush().unwrap(); let mut buf = String::new(); match std::io::stdin().read_line(&mut buf) { + Ok(0) => break, Ok(len) => { - if let Some(event) = parse_cmd(buf, len) { - #[cfg(unix)] - let Ok(mut stream) = UnixStream::connect(&socket_path) else { - log::error!("Could not connect to lan-mouse-socket"); - continue; - }; - #[cfg(windows)] - let Ok(mut stream) = TcpStream::connect("127.0.0.1:5252".parse::().unwrap()) else { - log::error!("Could not connect to lan-mouse-server"); - continue; - }; - let json = serde_json::to_string(&event).unwrap(); - if let Err(e) = stream.write(json.as_bytes()) { - log::error!("error sending message: {e}"); - }; - if event == FrontendEvent::Shutdown() { - break; + if let Some(events) = parse_cmd(buf, len) { + for event in events.iter() { + let json = serde_json::to_string(&event).unwrap(); + let bytes = json.as_bytes(); + let len = bytes.len().to_ne_bytes(); + if let Err(e) = tx.write(&len) { + log::error!("error sending message: {e}"); + }; + if let Err(e) = tx.write(bytes) { + log::error!("error sending message: {e}"); + }; + if *event == FrontendEvent::Shutdown() { + break; + } } + // prompt is printed after the server response is received + } else { + prompt(); } } Err(e) => { - log::error!("{e:?}"); + log::error!("error reading from stdin: {e}"); break } } } - })?) + })?; + + let writer = thread::Builder::new() + .name("cli-frontend-notify".to_string()) + .spawn(move || { + loop { + // read len + let mut len = [0u8; 8]; + match rx.read_exact(&mut len) { + Ok(()) => (), + Err(e) if e.kind() == ErrorKind::UnexpectedEof => break, + Err(e) => break log::error!("{e}"), + }; + let len = usize::from_ne_bytes(len); + + // read payload + let mut buf: Vec = vec![0u8; len]; + match rx.read_exact(&mut buf[..len]) { + Ok(()) => (), + Err(e) if e.kind() == ErrorKind::UnexpectedEof => break, + Err(e) => break log::error!("{e}"), + }; + + let notify: FrontendNotify = match serde_json::from_slice(&buf) { + Ok(n) => n, + Err(e) => break log::error!("{e}"), + }; + match notify { + FrontendNotify::NotifyClientCreate(client, host, port, pos) => { + log::info!("new client ({client}): {}:{port} - {pos}", host.as_deref().unwrap_or("")); + }, + FrontendNotify::NotifyClientUpdate(client, host, port, pos) => { + log::info!("client ({client}) updated: {}:{port} - {pos}", host.as_deref().unwrap_or("")); + }, + FrontendNotify::NotifyClientDelete(client) => { + log::info!("client ({client}) deleted."); + }, + FrontendNotify::NotifyError(e) => { + log::warn!("{e}"); + }, + FrontendNotify::Enumerate(clients) => { + for (client, active) in clients.into_iter() { + log::info!("client ({}) [{}]: active: {}, associated addresses: [{}]", + client.handle, + client.hostname.as_deref().unwrap_or(""), + if active { "yes" } else { "no" }, + client.addrs.into_iter().map(|a| a.to_string()) + .collect::>() + .join(", ") + ); + } + } + } + prompt(); + } + })?; + Ok((reader, writer)) } -fn parse_cmd(s: String, len: usize) -> Option { +fn prompt() { + eprint!("lan-mouse > "); + std::io::stderr().flush().unwrap(); +} + +fn parse_cmd(s: String, len: usize) -> Option> { if len == 0 { - return Some(FrontendEvent::Shutdown()) + return Some(vec![FrontendEvent::Shutdown()]) } let mut l = s.split_whitespace(); let cmd = l.next()?; - match cmd { - "connect" => { - let host = l.next()?.to_owned(); - let pos = match l.next()? { - "right" => Position::Right, - "top" => Position::Top, - "bottom" => Position::Bottom, - _ => Position::Left, - }; - let port = match l.next() { - Some(p) => match p.parse() { - Ok(p) => p, - Err(e) => { - log::error!("{e}"); - return None; - } - } - None => DEFAULT_PORT, - }; - Some(FrontendEvent::AddClient(host, port, pos)) - } - "disconnect" => { - let host = l.next()?.to_owned(); - let port = match l.next()?.parse() { - Ok(p) => p, - Err(e) => { - log::error!("{e}"); - return None; - } - }; - Some(FrontendEvent::DelClient(host, port)) + let res = match cmd { + "help" => { + log::info!("list list clients"); + log::info!("connect left|right|top|bottom [port] add a new client"); + log::info!("disconnect remove a client"); + log::info!("activate activate a client"); + log::info!("deactivate deactivate a client"); + log::info!("exit exit lan-mouse"); + None } + "exit" => return Some(vec![FrontendEvent::Shutdown()]), + "list" => return Some(vec![FrontendEvent::Enumerate()]), + "connect" => Some(parse_connect(l)), + "disconnect" => Some(parse_disconnect(l)), + "activate" => Some(parse_activate(l)), + "deactivate" => Some(parse_deactivate(l)), _ => { log::error!("unknown command: {s}"); None } + }; + match res { + Some(Ok(e)) => Some(vec![e, FrontendEvent::Enumerate()]), + Some(Err(e)) => { + log::warn!("{e}"); + None + } + _ => None } } + +fn parse_connect(mut l: SplitWhitespace) -> Result { + let usage = "usage: connect left|right|top|bottom [port]"; + let host = l.next().context(usage)?.to_owned(); + let pos = match l.next().context(usage)? { + "right" => Position::Right, + "top" => Position::Top, + "bottom" => Position::Bottom, + _ => Position::Left, + }; + let port = if let Some(p) = l.next() { + p.parse()? + } else { + DEFAULT_PORT + }; + Ok(FrontendEvent::AddClient(Some(host), port, pos)) +} + +fn parse_disconnect(mut l: SplitWhitespace) -> Result { + let client = l.next().context("usage: disconnect ")?.parse()?; + Ok(FrontendEvent::DelClient(client)) +} + +fn parse_activate(mut l: SplitWhitespace) -> Result { + let client = l.next().context("usage: activate ")?.parse()?; + Ok(FrontendEvent::ActivateClient(client, true)) +} +fn parse_deactivate(mut l: SplitWhitespace) -> Result { + let client = l.next().context("usage: deactivate ")?.parse()?; + Ok(FrontendEvent::ActivateClient(client, false)) +} diff --git a/src/frontend/gtk.rs b/src/frontend/gtk.rs index 5f8eb5d..9154bc8 100644 --- a/src/frontend/gtk.rs +++ b/src/frontend/gtk.rs @@ -2,16 +2,18 @@ mod window; mod client_object; mod client_row; -use std::{io::Result, thread::{self, JoinHandle}}; +use std::{io::{Result, Read, ErrorKind}, thread::{self, JoinHandle}, env, process, path::Path, os::unix::net::UnixStream, str}; -use crate::frontend::gtk::window::Window; +use crate::{frontend::gtk::window::Window, config::DEFAULT_PORT}; -use gtk::{prelude::*, IconTheme, gdk::Display, gio::{SimpleAction, SimpleActionGroup}, glib::clone, CssProvider}; +use gtk::{prelude::*, IconTheme, gdk::Display, gio::{SimpleAction, SimpleActionGroup}, glib::{clone, MainContext, Priority}, CssProvider, subclass::prelude::ObjectSubclassIsExt}; use adw::Application; use gtk::{gio, glib, prelude::ApplicationExt}; use self::client_object::ClientObject; +use super::FrontendNotify; + pub fn start() -> Result> { thread::Builder::new() .name("gtk-thread".into()) @@ -49,45 +51,137 @@ fn load_icons() { } fn build_ui(app: &Application) { + let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") { + Ok(v) => v, + Err(e) => { + log::error!("{e}"); + process::exit(1); + } + }; + let socket_path = Path::new(xdg_runtime_dir.as_str()) + .join("lan-mouse-socket.sock"); + let Ok(mut rx) = UnixStream::connect(&socket_path) else { + log::error!("Could not connect to lan-mouse-socket @ {socket_path:?}"); + process::exit(1); + }; + let tx = match rx.try_clone() { + Ok(sock) => sock, + Err(e) => { + log::error!("{e}"); + process::exit(1); + } + }; + + let (sender, receiver) = MainContext::channel::(Priority::default()); + + gio::spawn_blocking(move || { + match loop { + // read length + let mut len = [0u8; 8]; + match rx.read_exact(&mut len) { + Ok(_) => (), + Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()), + Err(e) => break Err(e), + }; + let len = usize::from_ne_bytes(len); + + // read payload + let mut buf = vec![0u8; len]; + match rx.read_exact(&mut buf) { + Ok(_) => (), + Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()), + Err(e) => break Err(e), + }; + + // parse json + let json = str::from_utf8(&buf) + .unwrap(); + match serde_json::from_str(json) { + Ok(notify) => sender.send(notify).unwrap(), + Err(e) => log::error!("{e}"), + } + } { + Ok(()) => {}, + Err(e) => log::error!("{e}"), + } + }); + let window = Window::new(app); - let action_client_activate = SimpleAction::new( - "activate-client", - Some(&i32::static_variant_type()), + window.imp().stream.borrow_mut().replace(tx); + receiver.attach(None, clone!(@weak window => @default-return glib::ControlFlow::Break, + move |notify| { + match notify { + FrontendNotify::NotifyClientCreate(client, hostname, port, position) => { + window.new_client(client, hostname, port, position, false); + }, + FrontendNotify::NotifyClientUpdate(client, hostname, port, position) => { + log::info!("client updated: {client}, {}:{port}, {position}", hostname.unwrap_or("".to_string())); + } + FrontendNotify::NotifyError(e) => { + // TODO + log::error!("{e}"); + }, + FrontendNotify::NotifyClientDelete(client) => { + window.delete_client(client); + } + FrontendNotify::Enumerate(clients) => { + for (client, active) in clients { + if window.client_idx(client.handle).is_some() { + continue + } + window.new_client( + client.handle, + client.hostname, + client.addrs + .iter() + .next() + .map(|s| s.port()) + .unwrap_or(DEFAULT_PORT), + client.pos, + active, + ); + } + }, + } + glib::ControlFlow::Continue + } + )); + + let action_request_client_update = SimpleAction::new( + "request-client-update", + Some(&u32::static_variant_type()), ); + + // remove client let action_client_delete = SimpleAction::new( - "delete-client", - Some(&i32::static_variant_type()), + "request-client-delete", + Some(&u32::static_variant_type()), ); - action_client_activate.connect_activate(clone!(@weak window => move |_action, param| { - log::debug!("activate-client"); + + // update client state + action_request_client_update.connect_activate(clone!(@weak window => move |_action, param| { + log::debug!("request-client-update"); let index = param.unwrap() - .get::() + .get::() .unwrap(); let Some(client) = window.clients().item(index as u32) else { return; }; let client = client.downcast_ref::().unwrap(); - window.update_client(client); + window.request_client_update(client); })); + action_client_delete.connect_activate(clone!(@weak window => move |_action, param| { log::debug!("delete-client"); - let index = param.unwrap() - .get::() + let idx = param.unwrap() + .get::() .unwrap(); - let Some(client) = window.clients().item(index as u32) else { - return; - }; - let client = client.downcast_ref::().unwrap(); - window.update_client(client); - window.clients().remove(index as u32); - if window.clients().n_items() == 0 { - window.set_placeholder_visible(true); - } + window.request_client_delete(idx); })); let actions = SimpleActionGroup::new(); window.insert_action_group("win", Some(&actions)); - actions.add_action(&action_client_activate); + actions.add_action(&action_request_client_update); actions.add_action(&action_client_delete); window.present(); } diff --git a/src/frontend/gtk/client_object.rs b/src/frontend/gtk/client_object.rs index d2f6b24..1037b33 100644 --- a/src/frontend/gtk/client_object.rs +++ b/src/frontend/gtk/client_object.rs @@ -3,13 +3,16 @@ mod imp; use gtk::glib::{self, Object}; use adw::subclass::prelude::*; +use crate::client::ClientHandle; + glib::wrapper! { pub struct ClientObject(ObjectSubclass); } impl ClientObject { - pub fn new(hostname: String, port: u32, active: bool, position: String) -> Self { + pub fn new(handle: ClientHandle, hostname: Option, port: u32, position: String, active: bool) -> Self { Object::builder() + .property("handle", handle) .property("hostname", hostname) .property("port", port) .property("active", active) @@ -24,7 +27,8 @@ impl ClientObject { #[derive(Default, Clone)] pub struct ClientData { - pub hostname: String, + pub handle: ClientHandle, + pub hostname: Option, pub port: u32, pub active: bool, pub position: String, diff --git a/src/frontend/gtk/client_object/imp.rs b/src/frontend/gtk/client_object/imp.rs index 4e9dc49..8ca456e 100644 --- a/src/frontend/gtk/client_object/imp.rs +++ b/src/frontend/gtk/client_object/imp.rs @@ -5,11 +5,14 @@ use gtk::glib; use gtk::prelude::*; use gtk::subclass::prelude::*; +use crate::client::ClientHandle; + use super::ClientData; #[derive(Properties, Default)] #[properties(wrapper_type = super::ClientObject)] pub struct ClientObject { + #[property(name = "handle", get, set, type = ClientHandle, member = handle)] #[property(name = "hostname", get, set, type = String, member = hostname)] #[property(name = "port", get, set, type = u32, member = port, maximum = u16::MAX as u32)] #[property(name = "active", get, set, type = bool, member = active)] diff --git a/src/frontend/gtk/client_row.rs b/src/frontend/gtk/client_row.rs index c087c63..1cb2645 100644 --- a/src/frontend/gtk/client_row.rs +++ b/src/frontend/gtk/client_row.rs @@ -31,8 +31,19 @@ impl ClientRow { let hostname_binding = client_object .bind_property("hostname", &self.imp().hostname.get(), "text") + .transform_to(|_, v: Option| { + if let Some(hostname) = v { + Some(hostname) + } else { + Some("".to_string()) + } + }) .transform_from(|_, v: String| { - if v == "" { Some("hostname".into()) } else { Some(v) } + if v.as_str().trim() == "" { + Some(None) + } else { + Some(Some(v)) + } }) .bidirectional() .sync_create() @@ -40,18 +51,34 @@ impl ClientRow { let title_binding = client_object .bind_property("hostname", self, "title") + .transform_to(|_, v: Option| { + if let Some(hostname) = v { + Some(hostname) + } else { + Some("no hostname!".to_string()) + } + }) + .sync_create() .build(); let port_binding = client_object .bind_property("port", &self.imp().port.get(), "text") .transform_from(|_, v: String| { if v == "" { - Some(4242) + Some(DEFAULT_PORT as u32) } else { Some(v.parse::().unwrap_or(DEFAULT_PORT) as u32) } }) + .transform_to(|_, v: u32| { + if v == 4242 { + Some("".to_string()) + } else { + Some(v.to_string()) + } + }) .bidirectional() + .sync_create() .build(); let subtitle_binding = client_object diff --git a/src/frontend/gtk/client_row/imp.rs b/src/frontend/gtk/client_row/imp.rs index 4c72f85..cd8fa1a 100644 --- a/src/frontend/gtk/client_row/imp.rs +++ b/src/frontend/gtk/client_row/imp.rs @@ -54,8 +54,8 @@ impl ObjectImpl for ClientRow { impl ClientRow { #[template_callback] fn handle_client_set_state(&self, state: bool, switch: &Switch) -> bool { - let idx = self.obj().index(); - switch.activate_action("win.activate-client", Some(&idx.to_variant())).unwrap(); + let idx = self.obj().index() as u32; + switch.activate_action("win.request-client-update", Some(&idx.to_variant())).unwrap(); switch.set_state(state); true // dont run default handler @@ -64,8 +64,10 @@ impl ClientRow { #[template_callback] fn handle_client_delete(&self, button: &Button) { log::debug!("delete button pressed"); - let idx = self.obj().index(); - button.activate_action("win.delete-client", Some(&idx.to_variant())).unwrap(); + let idx = self.obj().index() as u32; + button + .activate_action("win.request-client-delete", Some(&idx.to_variant())) + .unwrap(); } } diff --git a/src/frontend/gtk/window.rs b/src/frontend/gtk/window.rs index 2e45aa5..fb80acc 100644 --- a/src/frontend/gtk/window.rs +++ b/src/frontend/gtk/window.rs @@ -1,13 +1,13 @@ mod imp; -use std::{path::{Path, PathBuf}, env, process, os::unix::net::UnixStream, io::Write}; +use std::io::Write; use adw::prelude::*; use adw::subclass::prelude::*; use gtk::{glib, gio, NoSelection}; use glib::{clone, Object}; -use crate::{frontend::{gtk::client_object::ClientObject, FrontendEvent}, config::DEFAULT_PORT, client::Position}; +use crate::{frontend::{gtk::client_object::ClientObject, FrontendEvent}, client::{Position, ClientHandle}, config::DEFAULT_PORT}; use super::client_row::ClientRow; @@ -67,16 +67,44 @@ impl Window { row } - fn new_client(&self) { - let client = ClientObject::new(String::from(""), DEFAULT_PORT as u32, false, "left".into()); + pub fn new_client(&self, handle: ClientHandle, hostname: Option, port: u16, position: Position, active: bool) { + let client = ClientObject::new(handle, hostname, port as u32, position.to_string(), active); self.clients().append(&client); + self.set_placeholder_visible(false); } - pub fn update_client(&self, client: &ClientObject) { + pub fn client_idx(&self, handle: ClientHandle) -> Option { + self.clients() + .iter::() + .position(|c| { + if let Ok(c) = c { + c.handle() == handle + } else { + false + } + }) + .map(|p| p as usize) + } + + pub fn delete_client(&self, handle: ClientHandle) { + let Some(idx) = self.client_idx(handle) else { + log::warn!("could not find client with handle {handle}"); + return; + }; + + self.clients().remove(idx as u32); + if self.clients().n_items() == 0 { + self.set_placeholder_visible(true); + } + } + + pub fn request_client_create(&self) { + let event = FrontendEvent::AddClient(None, DEFAULT_PORT, Position::default()); + self.request(event); + } + + pub fn request_client_update(&self, client: &ClientObject) { let data = client.get_data(); - let socket_path = self.imp().socket_path.borrow(); - let socket_path = socket_path.as_ref().unwrap().as_path(); - let host_name = data.hostname; let position = match data.position.as_str() { "left" => Position::Left, "right" => Position::Right, @@ -87,18 +115,37 @@ impl Window { return } }; - let port = data.port; - let event = if client.active() { - FrontendEvent::DelClient(host_name, port as u16) - } else { - FrontendEvent::AddClient(host_name, port as u16, position) - }; + let hostname = data.hostname; + let port = data.port as u16; + let event = FrontendEvent::UpdateClient(client.handle(), hostname, port, position); + self.request(event); + + let event = FrontendEvent::ActivateClient(client.handle(), !client.active()); + self.request(event); + } + + pub fn request_client_delete(&self, idx: u32) { + if let Some(obj) = self.clients().item(idx) { + let client_object: &ClientObject = obj + .downcast_ref() + .expect("Expected object of type `ClientObject`."); + let handle = client_object.handle(); + let event = FrontendEvent::DelClient(handle); + self.request(event); + } + } + + fn request(&self, event: FrontendEvent) { let json = serde_json::to_string(&event).unwrap(); - let Ok(mut stream) = UnixStream::connect(socket_path) else { - log::error!("Could not connect to lan-mouse-socket @ {socket_path:?}"); - return; + log::debug!("requesting {json}"); + let mut stream = self.imp().stream.borrow_mut(); + let stream = stream.as_mut().unwrap(); + let bytes = json.as_bytes(); + let len = bytes.len().to_ne_bytes(); + if let Err(e) = stream.write(&len) { + log::error!("error sending message: {e}"); }; - if let Err(e) = stream.write(json.as_bytes()) { + if let Err(e) = stream.write(bytes) { log::error!("error sending message: {e}"); }; } @@ -107,21 +154,7 @@ impl Window { self.imp() .add_client_button .connect_clicked(clone!(@weak self as window => move |_| { - window.new_client(); - window.set_placeholder_visible(false); + window.request_client_create(); })); } - - fn connect_stream(&self) { - let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") { - Ok(v) => v, - Err(e) => { - log::error!("{e}"); - process::exit(1); - } - }; - let socket_path = Path::new(xdg_runtime_dir.as_str()) - .join("lan-mouse-socket.sock"); - self.imp().socket_path.borrow_mut().replace(PathBuf::from(socket_path)); - } } diff --git a/src/frontend/gtk/window/imp.rs b/src/frontend/gtk/window/imp.rs index fb937bd..8c65eca 100644 --- a/src/frontend/gtk/window/imp.rs +++ b/src/frontend/gtk/window/imp.rs @@ -1,4 +1,4 @@ -use std::{cell::{Cell, RefCell}, path::PathBuf}; +use std::{cell::{Cell, RefCell}, os::unix::net::UnixStream}; use glib::subclass::InitializingObject; use adw::{prelude::*, ActionRow}; @@ -16,7 +16,7 @@ pub struct Window { #[template_child] pub client_placeholder: TemplateChild, pub clients: RefCell>, - pub socket_path: RefCell>, + pub stream: RefCell>, } #[glib::object_subclass] @@ -54,7 +54,6 @@ impl ObjectImpl for Window { obj.setup_icon(); obj.setup_clients(); obj.setup_callbacks(); - obj.connect_stream(); } } diff --git a/src/main.rs b/src/main.rs index 92a700f..23b317e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use env_logger::Env; use lan_mouse::{ consumer, producer, config::{Config, Frontend::{Cli, Gtk}}, event::server::Server, - frontend::{FrontendAdapter, cli}, + frontend::{FrontendListener, cli}, }; #[cfg(all(unix, feature = "gtk"))] @@ -31,7 +31,7 @@ pub fn run() -> Result<(), Box> { let consumer = consumer::create()?; // create frontend communication adapter - let frontend_adapter = FrontendAdapter::new()?; + let frontend_adapter = FrontendListener::new()?; // start sending and receiving events let mut event_server = Server::new(config.port, producer, consumer, frontend_adapter)?; @@ -45,24 +45,10 @@ pub fn run() -> Result<(), Box> { Cli => { cli::start()?; } }; - // this currently causes issues, because the clients from - // the config arent communicated to gtk yet. - if config.frontend == Gtk { - log::warn!("clients defined in config currently have no effect with the gtk frontend"); - } else { - // add clients from config - config.get_clients().into_iter().for_each(|(c, h, p)| { - let host_name = match h { - Some(h) => format!(" '{}'", h), - None => "".to_owned(), - }; - if c.len() == 0 { - log::warn!("ignoring client{} with 0 assigned ips!", host_name); - } - log::info!("adding client [{}]{} @ {:?}", p, host_name, c); - event_server.add_client(c, p); - }); - } + // add clients from config + config.get_clients().into_iter().for_each(|(c, h, port, p)| { + event_server.add_client(h, c, port, p); + }); log::info!("Press Ctrl+Alt+Shift+Super to release the mouse"); // run event loop