Compare commits

..

30 Commits

Author SHA1 Message Date
Ferdinand Schober
f4db2366b7 chore: Release lan-mouse version 0.6.0 2024-01-28 17:35:52 +01:00
Ferdinand Schober
c9deb6eba4 add firewalld config file 2024-01-28 17:32:42 +01:00
Kai
5cc8cda19d macos: add keyboard support (#81)
* macos: add keyboard support

* macos: handle key repeat

* update README
2024-01-26 11:05:54 +01:00
Ferdinand Schober
8084b52cfc Revert "gtk: handle exit of service properly"
This reverts commit 1f4821a16d.
breaks ubuntu lts
2024-01-23 21:51:40 +01:00
Ferdinand Schober
1f4821a16d gtk: handle exit of service properly 2024-01-23 21:36:42 +01:00
Ferdinand Schober
82926d8272 fix error handling in consumer task 2024-01-23 20:47:29 +01:00
Ferdinand Schober
006831b9f1 add systemd user service definition
ref #76
ref #49
2024-01-21 20:44:09 +01:00
Ferdinand Schober
e5b770a799 Update README.md
Add installation instructions
2024-01-19 14:14:47 +01:00
Ferdinand Schober
017bc43176 refactor timer task 2024-01-19 02:07:03 +01:00
Ferdinand Schober
36001c6fb2 refactor udp task 2024-01-19 02:03:30 +01:00
Ferdinand Schober
2803db7073 refactor dns task 2024-01-19 02:01:45 +01:00
Ferdinand Schober
622b04b36c refactor frontend task 2024-01-19 01:58:49 +01:00
Ferdinand Schober
61ff05c95a refactor consumer task 2024-01-19 01:51:09 +01:00
Ferdinand Schober
ecab3a360d refactor producer task 2024-01-18 23:46:06 +01:00
Ferdinand Schober
6674af8e63 allow incoming requests from arbitrary ports (#78)
closes #77
2024-01-18 22:36:33 +01:00
Ferdinand Schober
b3caba99ab fix misleading warning 2024-01-18 22:22:27 +01:00
Ferdinand Schober
fad48c2504 no config is not an error 2024-01-17 08:37:18 +01:00
Ferdinand Schober
f28f75418c add a warning when mouse is released by compositor
This can not be influenced and is helpful for debugging
2024-01-17 08:36:41 +01:00
Ferdinand Schober
e2c47d3096 fix: initial dns resolve was not working 2024-01-17 00:22:24 +01:00
Ferdinand Schober
f19944515a Revert "temporary fix for AUR pkg"
This reverts commit 8c276f88b7.
2024-01-16 23:11:13 +01:00
Ferdinand Schober
535cd055b9 fix initial activation 2024-01-16 19:49:34 +01:00
Ferdinand Schober
118c0dfc73 cleanup 2024-01-16 16:58:47 +01:00
Ferdinand Schober
7897db6047 remove unneccessary enumerate request 2024-01-16 16:15:23 +01:00
Ferdinand Schober
347256e966 fix frontend channel buffer size 2024-01-16 16:03:33 +01:00
Ferdinand Schober
0017dbc634 Update README.md 2024-01-16 13:01:25 +01:00
Ferdinand Schober
d90eb0cd0f Activate on startup (#70)
Frontends are now properly synced among each other and on startup the correct state is reflected.

Closes #75 
Closes #68
2024-01-16 12:59:39 +01:00
Ferdinand Schober
2e52660714 fix name of desktop entry 2024-01-15 11:06:00 +01:00
Ferdinand Schober
8c276f88b7 temporary fix for AUR pkg 2024-01-15 09:00:21 +01:00
Ferdinand Schober
13597b3587 fix app_id + app icon 2024-01-15 08:42:23 +01:00
CupricReki
b59808742a Modified .desktop file to conform with standard (#72) 2024-01-15 07:28:38 +01:00
30 changed files with 1489 additions and 1093 deletions

71
Cargo.lock generated
View File

@@ -80,6 +80,21 @@ version = "1.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355"
[[package]]
name = "arraydeque"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0ffd3d69bd89910509a5d31d1f1353f38ccffdd116dd0099bbd6627f7bd8ad8"
[[package]]
name = "arrayvec"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9"
dependencies = [
"nodrop",
]
[[package]]
name = "ashpd"
version = "0.6.7"
@@ -397,7 +412,7 @@ version = "4.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
dependencies = [
"heck",
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.42",
@@ -540,7 +555,7 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a"
dependencies = [
"heck",
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.42",
@@ -961,7 +976,7 @@ version = "0.18.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72793962ceece3863c2965d7f10c8786323b17c7adea75a515809fa20ab799a5"
dependencies = [
"heck",
"heck 0.4.1",
"proc-macro-crate 2.0.1",
"proc-macro-error",
"proc-macro2",
@@ -1104,6 +1119,15 @@ version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "heck"
version = "0.4.1"
@@ -1224,9 +1248,33 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "keycode"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b07873c3182aec8a0eb1a5a4e7b197d42e9d167ba78497a6ee932a82d94673ed"
dependencies = [
"arraydeque",
"arrayvec",
"bitflags 1.3.2",
"keycode_macro",
]
[[package]]
name = "keycode_macro"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e521ea802f5b3c7194e169d75cab431b0ff08d022f2b6047b08754b4988b89df"
dependencies = [
"anyhow",
"heck 0.3.3",
"proc-macro2",
"quote",
]
[[package]]
name = "lan-mouse"
version = "0.5.1"
version = "0.6.0"
dependencies = [
"anyhow",
"ashpd",
@@ -1239,6 +1287,7 @@ dependencies = [
"futures-core",
"glib-build-tools",
"gtk4",
"keycode",
"libadwaita",
"libc",
"log",
@@ -1421,6 +1470,12 @@ dependencies = [
"memoffset 0.7.1",
]
[[package]]
name = "nodrop"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "num_cpus"
version = "1.16.0"
@@ -1959,7 +2014,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331"
dependencies = [
"cfg-expr",
"heck",
"heck 0.4.1",
"pkg-config",
"toml",
"version-compare",
@@ -2218,6 +2273,12 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "url"
version = "2.5.0"

View File

@@ -1,7 +1,7 @@
[package]
name = "lan-mouse"
description = "Software KVM Switch / mouse & keyboard sharing software for Local Area Networks"
version = "0.5.1"
version = "0.6.0"
edition = "2021"
license = "GPL-3.0-or-later"
repository = "https://github.com/ferdinandschober/lan-mouse"
@@ -30,6 +30,7 @@ clap = { version="4.4.11", features = ["derive"] }
gtk = { package = "gtk4", version = "0.7.2", features = ["v4_2"], optional = true }
adw = { package = "libadwaita", version = "0.5.2", features = ["v1_1"], optional = true }
async-channel = { version = "2.1.1", optional = true }
keycode = "0.4.0"
[target.'cfg(unix)'.dependencies]
libc = "0.2.148"

130
README.md
View File

@@ -46,17 +46,86 @@ input capture (to send events *to* other clients) on different operating systems
| Wayland (Gnome) | :heavy_check_mark: | WIP |
| X11 | :heavy_check_mark: | WIP |
| Windows | :heavy_check_mark: | WIP |
| MacOS | ( :heavy_check_mark: ) | WIP |
Keycode translation is not yet implemented so on MacOS only mouse emulation works as of right now.
| MacOS | :heavy_check_mark: | WIP |
> [!Important]
> If you are using [Wayfire](https://github.com/WayfireWM/wayfire), make sure to use a recent version (must be newer than October 23rd) and **add `shortcuts-inhibit` to the list of plugins in your wayfire config!**
> Otherwise input capture will not work.
## Build and Run
## Installation
### Download from Releases
The easiest way to install Lan Mouse is to download precompiled release binaries from the [releases section](https://github.com/feschber/lan-mouse/releases).
For Windows, the depenedencies are included in the .zip file, for other operating systems see [Installing Dependencies](#installing-dependencies).
### Arch Linux
Lan Mouse is available on the AUR:
```sh
# git version (includes latest changes)
paru -S lan-mouse-git
# alternatively
paru -S lan-mouse-bin
```
### Building from Source
Build in release mode:
```sh
cargo build --release
```
Run directly:
```sh
cargo run --release
```
Install the files:
```sh
# install lan-mouse
sudo cp target/release/lan-mouse /usr/local/bin/
# install app icon
sudo mkdir -p /usr/local/share/icons/hicolor/scalable/apps
sudo cp resources/de.feschber.LanMouse.svg /usr/local/share/icons/hicolor/scalable/apps
# update icon cache
gtk-update-icon-cache /usr/local/share/icons/hicolor/
# install desktop entry
sudo mkdir -p /usr/local/share/applications
sudo cp de.feschber.LanMouse.dekstop /usr/local/share/applications
# when using firewalld: install firewall rule
sudo cp firewall/lan-mouse.xml /etc/firewalld/services
# -> enable the service in firewalld settings
```
### Conditional Compilation
Currently only x11, wayland, windows and MacOS are supported backends.
Depending on the toolchain used, support for other platforms is omitted
automatically (it does not make sense to build a Windows `.exe` with
support for x11 and wayland backends).
However one might still want to omit support for e.g. wayland, x11 or libei on
a Linux system.
This is possible through
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html).
E.g. if only wayland support is needed, the following command produces
an executable with just support for wayland:
```sh
cargo build --no-default-features --features wayland
```
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
## Installing Dependencies
### Install Dependencies
#### Macos
```sh
brew install libadwaita
@@ -78,6 +147,9 @@ sudo dnf install libadwaita-devel libXtst-devel libX11-devel
```
#### Windows
> [!NOTE]
> This is only necessary when building lan-mouse from source. The windows release comes with precompiled gtk dlls.
Follow the instructions at [gtk-rs.org](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation_windows.html)
*TLDR:*
@@ -120,37 +192,6 @@ Make sure to add the directory `C:\gtk-build\gtk\x64\release\bin`
To avoid building GTK from source, it is possible to disable
the gtk frontend (see conditional compilation below).
### Build and run
Build in release mode:
```sh
cargo build --release
```
Run directly:
```sh
cargo run --release
```
### Conditional Compilation
Currently only x11, wayland, windows and MacOS are supported backends.
Depending on the toolchain used, support for other platforms is omitted
automatically (it does not make sense to build a Windows `.exe` with
support for x11 and wayland backends).
However one might still want to omit support for e.g. wayland, x11 or libei on
a Linux system.
This is possible through
[cargo features](https://doc.rust-lang.org/cargo/reference/features.html).
E.g. if only wayland support is needed, the following command produces
an executable with just support for wayland:
```sh
cargo build --no-default-features --features wayland
```
For a detailed list of available features, checkout the [Cargo.toml](./Cargo.toml)
## Usage
### Gtk Frontend
By default the gtk frontend will open when running `lan-mouse`.
@@ -184,6 +225,17 @@ To do so, add `--daemon` to the commandline args:
$ cargo run --release -- --daemon
```
In order to start lan-mouse with a graphical session automatically,
the [systemd-service](service/lan-mouse.service) can be used:
Copy the file to `~/.config/systemd/user/` and enable the service:
```sh
cp service/lan-mouse.service ~/.config/systemd/user
systemctl --user daemon-reload
systemctl --user enable --now lan-mouse.service
```
## Configuration
To automatically load clients on startup, the file `$XDG_CONFIG_HOME/lan-mouse/config.toml` is parsed.
`$XDG_CONFIG_HOME` defaults to `~/.config/`.
@@ -204,7 +256,9 @@ port = 4242
# define a client on the right side with host name "iridium"
[right]
# hostname
host_name = "iridium"
hostname = "iridium"
# activate this client immediately when lan-mouse is started
activate_on_startup = true
# optional list of (known) ip addresses
ips = ["192.168.178.156"]
@@ -212,7 +266,7 @@ ips = ["192.168.178.156"]
[left]
# The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified.
host_name = "thorium"
hostname = "thorium"
# ips for ethernet and wifi
ips = ["192.168.178.189", "192.168.178.172"]
# optional port

View File

@@ -8,7 +8,7 @@ port = 4242
# define a client on the right side with host name "iridium"
[right]
# hostname
host_name = "iridium"
hostname = "iridium"
# optional list of (known) ip addresses
ips = ["192.168.178.156"]
@@ -16,7 +16,7 @@ ips = ["192.168.178.156"]
[left]
# The hostname is optional: When no hostname is specified,
# at least one ip address needs to be specified.
host_name = "thorium"
hostname = "thorium"
# ips for ethernet and wifi
ips = ["192.168.178.189", "192.168.178.172"]
# optional port

View File

@@ -1,13 +1,12 @@
[Desktop Entry]
Categories=Utility;
Comment[en_US]=mouse & keyboard sharing via LAN
Comment[en_US]=Mouse & Keyboard sharing via LAN
Comment=Mouse & Keyboard sharing via LAN
Comment[de_DE]=Maus- und Tastaturfreigabe über LAN
Exec=lan-mouse
Icon=mouse-icon.svg
Icon=de.feschber.LanMouse
Name[en_US]=Lan Mouse
Name[de_DE]=Lan Maus
Name=Lan Mouse
StartupNotify=true
Terminal=false
Type=Application
Version=0.5.1

8
firewall/lan-mouse.xml Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- for packaging: /usr/lib/firewalld/services/lan-mouse.xml -->
<!-- configure manually: /etc/firewalld/services/lan-mouse.xml -->
<service>
<short>LAN Mouse</short>
<description>mouse and keyboard sharing via LAN</description>
<port port="4242" protocol="udp"/>
</service>

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -7,6 +7,6 @@
<file compressed="true">style-dark.css</file>
</gresource>
<gresource prefix="/de/feschber/LanMouse/icons">
<file compressed="true" preprocess="xml-stripblanks">mouse-icon.svg</file>
<file compressed="true" preprocess="xml-stripblanks">de.feschber.LanMouse.svg</file>
</gresource>
</gresources>

View File

@@ -35,7 +35,7 @@
<object class="AdwStatusPage">
<property name="title" translatable="yes">Lan Mouse</property>
<property name="description" translatable="yes">easily use your mouse and keyboard on multiple computers</property>
<property name="icon-name">mouse-icon</property>
<property name="icon-name">de.feschber.LanMouse</property>
<property name="child">
<object class="AdwClamp">
<property name="maximum-size">600</property>

13
service/lan-mouse.service Normal file
View File

@@ -0,0 +1,13 @@
[Unit]
Description=Lan Mouse
# lan mouse needs an active graphical session
After=graphical-session.target
# make sure the service terminates with the graphical session
BindsTo=graphical-session.target
[Service]
ExecStart=/usr/bin/lan-mouse --daemon
Restart=on-failure
[Install]
WantedBy=graphical-session.target

View File

@@ -5,13 +5,20 @@ use anyhow::{anyhow, Result};
use async_trait::async_trait;
use core_graphics::display::{CGDisplayBounds, CGMainDisplayID, CGPoint};
use core_graphics::event::{
CGEvent, CGEventTapLocation, CGEventType, CGMouseButton, EventField, ScrollEventUnit,
CGEvent, CGEventTapLocation, CGEventType, CGKeyCode, CGMouseButton, EventField, ScrollEventUnit,
};
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use keycode::{KeyMap, KeyMapping};
use std::ops::{Index, IndexMut};
use std::time::Duration;
use tokio::task::AbortHandle;
const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500);
const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32);
pub struct MacOSConsumer {
pub event_source: CGEventSource,
repeat_task: Option<AbortHandle>,
button_state: ButtonState,
}
@@ -59,6 +66,7 @@ impl MacOSConsumer {
Ok(Self {
event_source,
button_state,
repeat_task: None,
})
}
@@ -66,6 +74,37 @@ impl MacOSConsumer {
let event: CGEvent = CGEvent::new(self.event_source.clone()).ok()?;
Some(event.location())
}
async fn spawn_repeat_task(&mut self, key: u16) {
// there can only be one repeating key and it's
// always the last to be pressed
self.kill_repeat_task();
let event_source = self.event_source.clone();
let repeat_task = tokio::task::spawn_local(async move {
tokio::time::sleep(DEFAULT_REPEAT_DELAY).await;
loop {
key_event(event_source.clone(), key, 1);
tokio::time::sleep(DEFAULT_REPEAT_INTERVAL).await;
}
});
self.repeat_task = Some(repeat_task.abort_handle());
}
fn kill_repeat_task(&mut self) {
if let Some(task) = self.repeat_task.take() {
task.abort();
}
}
}
fn key_event(event_source: CGEventSource, key: u16, state: u8) {
let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) {
Ok(e) => e,
Err(_) => {
log::warn!("unable to create key event");
return;
}
};
event.post(CGEventTapLocation::HID);
}
#[async_trait]
@@ -209,22 +248,24 @@ impl EventConsumer for MacOSConsumer {
PointerEvent::Frame { .. } => {}
},
Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key { .. } => {
/*
let code = CGKeyCode::from_le(key as u16);
let event = match CGEvent::new_keyboard_event(
self.event_source.clone(),
code,
match state { 1 => true, _ => false }
) {
Ok(e) => e,
KeyboardEvent::Key {
time: _,
key,
state,
} => {
let code = match KeyMap::from_key_mapping(KeyMapping::Evdev(key as u16)) {
Ok(k) => k.mac as CGKeyCode,
Err(_) => {
log::warn!("unable to create key event");
return
log::warn!("unable to map key event");
return;
}
};
event.post(CGEventTapLocation::HID);
*/
match state {
// pressed
1 => self.spawn_repeat_task(code).await,
_ => self.kill_repeat_task(),
}
key_event(self.event_source.clone(), code, state)
}
KeyboardEvent::Modifiers { .. } => {}
},

View File

@@ -719,6 +719,9 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
* it is impossible to grab it again (since the pointer
* lock, relative pointer,... objects are still in place)
*/
if app.pointer_lock.is_some() {
log::warn!("compositor released mouse");
}
app.ungrab();
}
wl_pointer::Event::Button {

View File

@@ -46,6 +46,20 @@ impl Display for Position {
}
}
impl TryFrom<&str> for Position {
type Error = ();
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"left" => Ok(Position::Left),
"right" => Ok(Position::Right),
"top" => Ok(Position::Top),
"bottom" => Ok(Position::Bottom),
_ => Err(()),
}
}
}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct Client {
/// hostname of this client
@@ -56,10 +70,10 @@ pub struct Client {
/// This way any event consumer / producer backend does not
/// need to know anything about a client other than its handle.
pub handle: ClientHandle,
/// all socket addresses associated with a particular client
/// all ip addresses associated with a particular client
/// e.g. Laptops usually have at least an ethernet and a wifi port
/// which have different ip addresses
pub addrs: HashSet<SocketAddr>,
pub ips: HashSet<IpAddr>,
/// 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
@@ -112,6 +126,7 @@ impl ClientManager {
ips: HashSet<IpAddr>,
port: u16,
pos: Position,
active: bool,
) -> ClientHandle {
// get a new client_handle
let handle = self.free_id();
@@ -119,15 +134,12 @@ impl ClientManager {
// store fix ip addresses
let fix_ips = ips.iter().cloned().collect();
// map ip addresses to socket addresses
let addrs = HashSet::from_iter(ips.into_iter().map(|ip| SocketAddr::new(ip, port)));
// store the client
let client = Client {
hostname,
fix_ips,
handle,
addrs,
ips,
port,
pos,
};
@@ -135,7 +147,7 @@ impl ClientManager {
// client was never seen, nor pinged
let client_state = ClientState {
client,
active: false,
active,
active_addr: None,
alive: false,
pressed_keys: HashSet::new(),
@@ -158,7 +170,7 @@ impl ClientManager {
.iter()
.position(|c| {
if let Some(c) = c {
c.active && c.client.addrs.contains(&addr)
c.active && c.client.ips.contains(&addr.ip())
} else {
false
}

View File

@@ -15,17 +15,19 @@ pub const DEFAULT_PORT: u16 = 4242;
pub struct ConfigToml {
pub port: Option<u16>,
pub frontend: Option<String>,
pub left: Option<Client>,
pub right: Option<Client>,
pub top: Option<Client>,
pub bottom: Option<Client>,
pub left: Option<TomlClient>,
pub right: Option<TomlClient>,
pub top: Option<TomlClient>,
pub bottom: Option<TomlClient>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Client {
pub struct TomlClient {
pub hostname: Option<String>,
pub host_name: Option<String>,
pub ips: Option<Vec<IpAddr>>,
pub port: Option<u16>,
pub activate_on_startup: Option<bool>,
}
impl ConfigToml {
@@ -66,10 +68,18 @@ pub enum Frontend {
pub struct Config {
pub frontend: Frontend,
pub port: u16,
pub clients: Vec<(Client, Position)>,
pub clients: Vec<(TomlClient, Position)>,
pub daemon: bool,
}
pub struct ConfigClient {
pub ips: HashSet<IpAddr>,
pub hostname: Option<String>,
pub port: u16,
pub pos: Position,
pub active: bool,
}
impl Config {
pub fn new() -> Result<Self> {
let args = CliArgs::parse();
@@ -93,7 +103,7 @@ impl Config {
let config_toml = match ConfigToml::new(config_path.as_str()) {
Err(e) => {
log::error!("{config_path}: {e}");
log::warn!("{config_path}: {e}");
log::warn!("Continuing without config file ...");
None
}
@@ -128,7 +138,7 @@ impl Config {
},
};
let mut clients: Vec<(Client, Position)> = vec![];
let mut clients: Vec<(TomlClient, Position)> = vec![];
if let Some(config_toml) = config_toml {
if let Some(c) = config_toml.right {
@@ -155,18 +165,28 @@ impl Config {
})
}
pub fn get_clients(&self) -> Vec<(HashSet<IpAddr>, Option<String>, u16, Position)> {
pub fn get_clients(&self) -> Vec<ConfigClient> {
self.clients
.iter()
.map(|(c, p)| {
.map(|(c, pos)| {
let port = c.port.unwrap_or(DEFAULT_PORT);
let ips: HashSet<IpAddr> = if let Some(ips) = c.ips.as_ref() {
HashSet::from_iter(ips.iter().cloned())
} else {
HashSet::new()
};
let host_name = c.host_name.clone();
(ips, host_name, port, *p)
let hostname = match &c.hostname {
Some(h) => Some(h.clone()),
None => c.host_name.clone(),
};
let active = c.activate_on_startup.unwrap_or(false);
ConfigClient {
ips,
hostname,
port,
pos: *pos,
active,
}
})
.collect()
}

View File

@@ -103,8 +103,9 @@ pub enum FrontendEvent {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FrontendNotify {
NotifyClientCreate(ClientHandle, Option<String>, u16, Position),
NotifyClientUpdate(ClientHandle, Option<String>, u16, Position),
NotifyClientActivate(ClientHandle, bool),
NotifyClientCreate(Client),
NotifyClientUpdate(Client),
NotifyClientDelete(ClientHandle),
/// new port, reason of failure (if failed)
NotifyPortChange(u16, Option<String>),
@@ -224,7 +225,6 @@ impl FrontendListener {
log::debug!("json: {json}, len: {}", payload.len());
let mut keep = vec![];
// TODO do simultaneously
for tx in self.tx_streams.iter_mut() {
// write len + payload

View File

@@ -83,17 +83,26 @@ pub fn run() -> Result<()> {
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::NotifyClientActivate(handle, active) => {
if active {
log::info!("client {handle} activated");
} else {
log::info!("client {handle} deactivated");
}
}
FrontendNotify::NotifyClientUpdate(client, host, port, pos) => {
log::info!(
"client ({client}) updated: {}:{port} - {pos}",
host.as_deref().unwrap_or("")
);
FrontendNotify::NotifyClientCreate(client) => {
let handle = client.handle;
let port = client.port;
let pos = client.pos;
let hostname = client.hostname.as_deref().unwrap_or("");
log::info!("new client ({handle}): {hostname}:{port} - {pos}");
}
FrontendNotify::NotifyClientUpdate(client) => {
let handle = client.handle;
let port = client.port;
let pos = client.pos;
let hostname = client.hostname.as_deref().unwrap_or("");
log::info!("client ({handle}) updated: {hostname}:{port} - {pos}");
}
FrontendNotify::NotifyClientDelete(client) => {
log::info!("client ({client}) deleted.");
@@ -109,7 +118,7 @@ pub fn run() -> Result<()> {
client.hostname.as_deref().unwrap_or(""),
if active { "yes" } else { "no" },
client
.addrs
.ips
.into_iter()
.map(|a| a.to_string())
.collect::<Vec<String>>()

View File

@@ -8,16 +8,12 @@ use std::{
process, str,
};
use crate::{config::DEFAULT_PORT, frontend::gtk::window::Window};
use crate::frontend::gtk::window::Window;
use adw::Application;
use gtk::{
gdk::Display,
gio::{SimpleAction, SimpleActionGroup},
glib::clone,
prelude::*,
subclass::prelude::ObjectSubclassIsExt,
CssProvider, IconTheme,
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, CssProvider,
IconTheme,
};
use gtk::{gio, glib, prelude::ApplicationExt};
@@ -38,7 +34,12 @@ pub fn run() -> glib::ExitCode {
#[cfg(not(windows))]
let ret = gtk_main();
log::debug!("frontend exited");
if ret == glib::ExitCode::FAILURE {
log::error!("frontend exited with failure");
} else {
log::info!("frontend exited successfully");
}
ret
}
@@ -46,7 +47,7 @@ fn gtk_main() -> glib::ExitCode {
gio::resources_register_include!("lan-mouse.gresource").expect("Failed to register resources.");
let app = Application::builder()
.application_id("de.feschber.lan-mouse")
.application_id("de.feschber.LanMouse")
.build();
app.connect_startup(|_| load_icons());
@@ -68,8 +69,8 @@ fn load_css() {
}
fn load_icons() {
let icon_theme =
IconTheme::for_display(&Display::default().expect("Could not connect to a display."));
let display = &Display::default().expect("Could not connect to a display.");
let icon_theme = IconTheme::for_display(display);
icon_theme.add_resource_path("/de/feschber/LanMouse/icons");
}
@@ -130,15 +131,17 @@ fn build_ui(app: &Application) {
loop {
let notify = receiver.recv().await.unwrap();
match notify {
FrontendNotify::NotifyClientCreate(client, hostname, port, position) => {
window.new_client(client, hostname, port, position, false);
FrontendNotify::NotifyClientActivate(handle, active) => {
window.activate_client(handle, active);
}
FrontendNotify::NotifyClientCreate(client) => {
window.new_client(client, false);
},
FrontendNotify::NotifyClientUpdate(client, hostname, port, position) => {
log::info!("client updated: {client}, {}:{port}, {position}", hostname.unwrap_or("".to_string()));
FrontendNotify::NotifyClientUpdate(client) => {
window.update_client(client);
}
FrontendNotify::NotifyError(e) => {
// TODO
log::error!("{e}");
window.show_toast(e.as_str());
},
FrontendNotify::NotifyClientDelete(client) => {
window.delete_client(client);
@@ -146,19 +149,11 @@ fn build_ui(app: &Application) {
FrontendNotify::Enumerate(clients) => {
for (client, active) in clients {
if window.client_idx(client.handle).is_some() {
continue
window.activate_client(client.handle, active);
window.update_client(client);
} else {
window.new_client(client, active);
}
window.new_client(
client.handle,
client.hostname,
client.addrs
.iter()
.next()
.map(|s| s.port())
.unwrap_or(DEFAULT_PORT),
client.pos,
active,
);
}
},
FrontendNotify::NotifyPortChange(port, msg) => {
@@ -172,37 +167,5 @@ fn build_ui(app: &Application) {
}
}));
let action_request_client_update =
SimpleAction::new("request-client-update", Some(&u32::static_variant_type()));
// remove client
let action_client_delete =
SimpleAction::new("request-client-delete", Some(&u32::static_variant_type()));
// 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::<u32>()
.unwrap();
let Some(client) = window.clients().item(index) else {
return;
};
let client = client.downcast_ref::<ClientObject>().unwrap();
window.request_client_update(client);
}));
action_client_delete.connect_activate(clone!(@weak window => move |_action, param| {
log::debug!("delete-client");
let idx = param.unwrap()
.get::<u32>()
.unwrap();
window.request_client_delete(idx);
}));
let actions = SimpleActionGroup::new();
window.insert_action_group("win", Some(&actions));
actions.add_action(&action_request_client_update);
actions.add_action(&action_client_delete);
window.present();
}

View File

@@ -3,26 +3,20 @@ mod imp;
use adw::subclass::prelude::*;
use gtk::glib::{self, Object};
use crate::client::ClientHandle;
use crate::client::{Client, ClientHandle};
glib::wrapper! {
pub struct ClientObject(ObjectSubclass<imp::ClientObject>);
}
impl ClientObject {
pub fn new(
handle: ClientHandle,
hostname: Option<String>,
port: u32,
position: String,
active: bool,
) -> Self {
pub fn new(client: Client, active: bool) -> Self {
Object::builder()
.property("handle", handle)
.property("hostname", hostname)
.property("port", port)
.property("handle", client.handle)
.property("hostname", client.hostname)
.property("port", client.port as u32)
.property("position", client.pos.to_string())
.property("active", active)
.property("position", position)
.build()
}

View File

@@ -28,6 +28,12 @@ impl ClientRow {
.sync_create()
.build();
let switch_position_binding = client_object
.bind_property("active", &self.imp().enable_switch.get(), "active")
.bidirectional()
.sync_create()
.build();
let hostname_binding = client_object
.bind_property("hostname", &self.imp().hostname.get(), "text")
.transform_to(|_, v: Option<String>| {
@@ -104,6 +110,7 @@ impl ClientRow {
.build();
bindings.push(active_binding);
bindings.push(switch_position_binding);
bindings.push(hostname_binding);
bindings.push(title_binding);
bindings.push(port_binding);

View File

@@ -4,6 +4,8 @@ use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow, ComboRow};
use glib::{subclass::InitializingObject, Binding};
use gtk::glib::clone;
use gtk::glib::once_cell::sync::Lazy;
use gtk::glib::subclass::Signal;
use gtk::{glib, Button, CompositeTemplate, Switch};
#[derive(CompositeTemplate, Default)]
@@ -28,6 +30,8 @@ pub struct ClientRow {
impl ObjectSubclass for ClientRow {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "ClientRow";
const ABSTRACT: bool = false;
type Type = super::ClientRow;
type ParentType = adw::ExpanderRow;
@@ -49,28 +53,33 @@ impl ObjectImpl for ClientRow {
row.handle_client_delete(button);
}));
}
fn signals() -> &'static [glib::subclass::Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![
Signal::builder("request-update")
.param_types([bool::static_type()])
.build(),
Signal::builder("request-delete").build(),
]
});
SIGNALS.as_ref()
}
}
#[gtk::template_callbacks]
impl ClientRow {
#[template_callback]
fn handle_client_set_state(&self, state: bool, switch: &Switch) -> bool {
let idx = self.obj().index() as u32;
switch
.activate_action("win.request-client-update", Some(&idx.to_variant()))
.unwrap();
switch.set_state(state);
fn handle_client_set_state(&self, state: bool, _switch: &Switch) -> bool {
log::debug!("state change -> requesting update");
self.obj().emit_by_name::<()>("request-update", &[&state]);
true // dont run default handler
}
#[template_callback]
fn handle_client_delete(&self, button: &Button) {
log::debug!("delete button pressed");
let idx = self.obj().index() as u32;
button
.activate_action("win.request-client-delete", Some(&idx.to_variant()))
.unwrap();
fn handle_client_delete(&self, _button: &Button) {
log::debug!("delete button pressed -> requesting delete");
self.obj().emit_by_name::<()>("request-delete", &[]);
}
}

View File

@@ -5,10 +5,14 @@ use std::io::Write;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::{clone, Object};
use gtk::{gio, glib, NoSelection};
use gtk::{
gio,
glib::{self, closure_local},
NoSelection,
};
use crate::{
client::{ClientHandle, Position},
client::{Client, ClientHandle, Position},
config::DEFAULT_PORT,
frontend::{gtk::client_object::ClientObject, FrontendEvent},
};
@@ -45,6 +49,18 @@ impl Window {
clone!(@weak self as window => @default-panic, move |obj| {
let client_object = obj.downcast_ref().expect("Expected object of type `ClientObject`.");
let row = window.create_client_row(client_object);
row.connect_closure("request-update", false, closure_local!(@strong window => move |row: ClientRow, active: bool| {
let index = row.index() as u32;
let Some(client) = window.clients().item(index) else {
return;
};
let client = client.downcast_ref::<ClientObject>().unwrap();
window.request_client_update(client, active);
}));
row.connect_closure("request-delete", false, closure_local!(@strong window => move |row: ClientRow| {
let index = row.index() as u32;
window.request_client_delete(index);
}));
row.upcast()
})
);
@@ -62,7 +78,7 @@ impl Window {
}
fn setup_icon(&self) {
self.set_icon_name(Some("mouse-icon"));
self.set_icon_name(Some("de.feschber.LanMouse"));
}
fn create_client_row(&self, client_object: &ClientObject) -> ClientRow {
@@ -71,15 +87,8 @@ impl Window {
row
}
pub fn new_client(
&self,
handle: ClientHandle,
hostname: Option<String>,
port: u16,
position: Position,
active: bool,
) {
let client = ClientObject::new(handle, hostname, port as u32, position.to_string(), active);
pub fn new_client(&self, client: Client, active: bool) {
let client = ClientObject::new(client, active);
self.clients().append(&client);
self.set_placeholder_visible(false);
}
@@ -106,6 +115,42 @@ impl Window {
}
}
pub fn update_client(&self, client: Client) {
let Some(idx) = self.client_idx(client.handle) else {
log::warn!("could not find client with handle {}", client.handle);
return;
};
let client_object = self.clients().item(idx as u32).unwrap();
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
let data = client_object.get_data();
/* only change if it actually has changed, otherwise
* the update signal is triggered */
if data.hostname != client.hostname {
client_object.set_hostname(client.hostname.unwrap_or("".into()));
}
if data.port != client.port as u32 {
client_object.set_port(client.port as u32);
}
if data.position != client.pos.to_string() {
client_object.set_position(client.pos.to_string());
}
}
pub fn activate_client(&self, handle: ClientHandle, active: bool) {
let Some(idx) = self.client_idx(handle) else {
log::warn!("could not find client with handle {handle}");
return;
};
let client_object = self.clients().item(idx as u32).unwrap();
let client_object: &ClientObject = client_object.downcast_ref().unwrap();
let data = client_object.get_data();
if data.active != active {
client_object.set_active(active);
log::debug!("set active to {active}");
}
}
pub fn request_client_create(&self) {
let event = FrontendEvent::AddClient(None, DEFAULT_PORT, Position::default());
self.imp().set_port(DEFAULT_PORT);
@@ -121,13 +166,10 @@ impl Window {
}
}
pub fn request_client_update(&self, client: &ClientObject) {
pub fn request_client_update(&self, client: &ClientObject, active: bool) {
let data = client.get_data();
let position = match data.position.as_str() {
"left" => Position::Left,
"right" => Position::Right,
"top" => Position::Top,
"bottom" => Position::Bottom,
let position = match Position::try_from(data.position.as_str()) {
Ok(pos) => pos,
_ => {
log::error!("invalid position: {}", data.position);
return;
@@ -135,10 +177,13 @@ impl Window {
};
let hostname = data.hostname;
let port = data.port as u16;
let event = FrontendEvent::UpdateClient(client.handle(), hostname, port, position);
log::debug!("requesting update: {event:?}");
self.request(event);
let event = FrontendEvent::ActivateClient(client.handle(), !client.active());
let event = FrontendEvent::ActivateClient(client.handle(), active);
log::debug!("requesting activate: {event:?}");
self.request(event);
}

View File

@@ -6,10 +6,7 @@ use std::net::TcpStream;
use std::os::unix::net::UnixStream;
use adw::subclass::prelude::*;
use adw::{
prelude::{EditableExt, WidgetExt},
ActionRow, ToastOverlay,
};
use adw::{prelude::*, ActionRow, ToastOverlay};
use glib::subclass::InitializingObject;
use gtk::{gio, glib, Button, CompositeTemplate, Entry, ListBox};
@@ -42,6 +39,8 @@ pub struct Window {
impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "LanMouseWindow";
const ABSTRACT: bool = false;
type Type = super::Window;
type ParentType = adw::ApplicationWindow;

View File

@@ -38,7 +38,6 @@ pub fn run() -> Result<()> {
// run a frontend
let mut service = start_service()?;
frontend::run_frontend(&config)?;
log::info!("terminating service");
#[cfg(unix)]
{
// on unix we give the service a chance to terminate gracefully

File diff suppressed because it is too large Load Diff

240
src/server/consumer_task.rs Normal file
View File

@@ -0,0 +1,240 @@
use anyhow::{anyhow, Result};
use std::net::SocketAddr;
use tokio::{
sync::mpsc::{Receiver, Sender},
task::JoinHandle,
};
use crate::{
client::{ClientEvent, ClientHandle},
consumer::EventConsumer,
event::{Event, KeyboardEvent},
scancode,
server::State,
};
use super::{ProducerEvent, Server};
#[derive(Clone, Debug)]
pub enum ConsumerEvent {
/// consumer is notified of a change in client states
ClientEvent(ClientEvent),
/// consumer must release keys for client
ReleaseKeys(ClientHandle),
/// termination signal
Terminate,
}
pub fn new(
mut consumer: Box<dyn EventConsumer>,
server: Server,
mut udp_rx: Receiver<Result<(Event, SocketAddr)>>,
sender_tx: Sender<(Event, SocketAddr)>,
producer_tx: Sender<ProducerEvent>,
timer_tx: Sender<()>,
) -> (JoinHandle<Result<()>>, Sender<ConsumerEvent>) {
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let consumer_task = tokio::task::spawn_local(async move {
let mut last_ignored = None;
loop {
tokio::select! {
udp_event = udp_rx.recv() => {
let udp_event = udp_event.ok_or(anyhow!("receiver closed"))??;
handle_udp_rx(&server, &producer_tx, &mut consumer, &sender_tx, &mut last_ignored, udp_event, &timer_tx).await;
}
consumer_event = rx.recv() => {
match consumer_event {
Some(e) => match e {
ConsumerEvent::ClientEvent(e) => consumer.notify(e).await,
ConsumerEvent::ReleaseKeys(c) => release_keys(&server, &mut consumer, c).await,
ConsumerEvent::Terminate => break,
},
None => break,
}
}
res = consumer.dispatch() => {
res?;
}
}
}
// release potentially still pressed keys
let clients = server
.client_manager
.borrow()
.get_client_states()
.map(|s| s.client.handle)
.collect::<Vec<_>>();
for client in clients {
release_keys(&server, &mut consumer, client).await;
}
// destroy consumer
consumer.destroy().await;
anyhow::Ok(())
});
(consumer_task, tx)
}
async fn handle_udp_rx(
server: &Server,
producer_notify_tx: &Sender<ProducerEvent>,
consumer: &mut Box<dyn EventConsumer>,
sender_tx: &Sender<(Event, SocketAddr)>,
last_ignored: &mut Option<SocketAddr>,
event: (Event, SocketAddr),
timer_tx: &Sender<()>,
) {
let (event, addr) = event;
// get handle for addr
let handle = match server.client_manager.borrow().get_client(addr) {
Some(a) => a,
None => {
if last_ignored.is_none() || last_ignored.is_some() && last_ignored.unwrap() != addr {
log::warn!("ignoring events from client {addr}");
last_ignored.replace(addr);
}
return;
}
};
// next event can be logged as ignored again
last_ignored.take();
log::trace!("{:20} <-<-<-<------ {addr} ({handle})", event.to_string());
{
let mut client_manager = server.client_manager.borrow_mut();
let client_state = match client_manager.get_mut(handle) {
Some(s) => s,
None => {
log::error!("unknown handle");
return;
}
};
// reset ttl for client and
client_state.alive = true;
// set addr as new default for this client
client_state.active_addr = Some(addr);
}
match (event, addr) {
(Event::Pong(), _) => { /* ignore pong events */ }
(Event::Ping(), addr) => {
let _ = sender_tx.send((Event::Pong(), addr)).await;
}
(Event::Disconnect(), _) => {
release_keys(server, consumer, handle).await;
}
(event, addr) => {
// tell clients that we are ready to receive events
if let Event::Enter() = event {
let _ = sender_tx.send((Event::Leave(), addr)).await;
}
match server.state.get() {
State::Sending => {
if let Event::Leave() = event {
// ignore additional leave events that may
// have been sent for redundancy
} else {
// upon receiving any event, we go back to receiving mode
server.state.replace(State::Receiving);
let _ = producer_notify_tx.send(ProducerEvent::Release).await;
log::trace!("STATE ===> Receiving");
}
}
State::Receiving => {
let mut ignore_event = false;
if let Event::Keyboard(KeyboardEvent::Key {
time: _,
key,
state,
}) = event
{
let mut client_manager = server.client_manager.borrow_mut();
let client_state =
if let Some(client_state) = client_manager.get_mut(handle) {
client_state
} else {
log::error!("unknown handle");
return;
};
if state == 0 {
// ignore release event if key not pressed
ignore_event = !client_state.pressed_keys.remove(&key);
} else {
// ignore press event if key not released
ignore_event = !client_state.pressed_keys.insert(key);
let _ = timer_tx.try_send(());
}
}
// ignore double press / release events to
// workaround buggy rdp backend.
if !ignore_event {
// consume event
consumer.consume(event, handle).await;
log::trace!("{event:?} => consumer");
}
}
State::AwaitingLeave => {
// we just entered the deadzone of a client, so
// we need to ignore events that may still
// be on the way until a leave event occurs
// telling us the client registered the enter
if let Event::Leave() = event {
server.state.replace(State::Sending);
log::trace!("STATE ===> Sending");
}
// entering a client that is waiting for a leave
// event should still be possible
if let Event::Enter() = event {
server.state.replace(State::Receiving);
let _ = producer_notify_tx.send(ProducerEvent::Release).await;
log::trace!("STATE ===> Receiving");
}
}
}
}
}
}
async fn release_keys(
server: &Server,
consumer: &mut Box<dyn EventConsumer>,
client: ClientHandle,
) {
let keys = server
.client_manager
.borrow_mut()
.get_mut(client)
.iter_mut()
.flat_map(|s| s.pressed_keys.drain())
.collect::<Vec<_>>();
for key in keys {
let event = Event::Keyboard(KeyboardEvent::Key {
time: 0,
key,
state: 0,
});
consumer.consume(event, client).await;
if let Ok(key) = scancode::Linux::try_from(key) {
log::warn!("releasing stuck key: {key:?}");
}
}
let modifiers_event = KeyboardEvent::Modifiers {
mods_depressed: 0,
mods_latched: 0,
mods_locked: 0,
group: 0,
};
consumer
.consume(Event::Keyboard(modifiers_event), client)
.await;
}

326
src/server/frontend_task.rs Normal file
View File

@@ -0,0 +1,326 @@
use std::{
collections::HashSet,
io::ErrorKind,
net::{IpAddr, SocketAddr},
};
#[cfg(unix)]
use tokio::net::UnixStream;
#[cfg(windows)]
use tokio::net::TcpStream;
use anyhow::{anyhow, Result};
use tokio::{
io::ReadHalf,
sync::mpsc::{Receiver, Sender},
task::JoinHandle,
};
use crate::{
client::{ClientEvent, ClientHandle, Position},
frontend::{self, FrontendEvent, FrontendListener, FrontendNotify},
};
use super::{
consumer_task::ConsumerEvent, producer_task::ProducerEvent, resolver_task::DnsRequest, Server,
};
pub(crate) fn new(
mut frontend: FrontendListener,
mut notify_rx: Receiver<FrontendNotify>,
server: Server,
producer_notify: Sender<ProducerEvent>,
consumer_notify: Sender<ConsumerEvent>,
resolve_ch: Sender<DnsRequest>,
port_tx: Sender<u16>,
) -> (JoinHandle<Result<()>>, Sender<FrontendEvent>) {
let (event_tx, mut event_rx) = tokio::sync::mpsc::channel(32);
let event_tx_clone = event_tx.clone();
let frontend_task = tokio::task::spawn_local(async move {
loop {
tokio::select! {
stream = frontend.accept() => {
let stream = match stream {
Ok(s) => s,
Err(e) => {
log::warn!("error accepting frontend connection: {e}");
continue;
}
};
handle_frontend_stream(&event_tx_clone, stream).await;
}
event = event_rx.recv() => {
let frontend_event = event.ok_or(anyhow!("frontend channel closed"))?;
if handle_frontend_event(&server, &producer_notify, &consumer_notify, &resolve_ch, &mut frontend, &port_tx, frontend_event).await {
break;
}
}
notify = notify_rx.recv() => {
let notify = notify.ok_or(anyhow!("frontend notify closed"))?;
let _ = frontend.notify_all(notify).await;
}
}
}
anyhow::Ok(())
});
(frontend_task, event_tx)
}
async fn handle_frontend_stream(
frontend_tx: &Sender<FrontendEvent>,
#[cfg(unix)] mut stream: ReadHalf<UnixStream>,
#[cfg(windows)] mut stream: ReadHalf<TcpStream>,
) {
use std::io;
let tx = frontend_tx.clone();
tokio::task::spawn_local(async move {
let _ = tx.send(FrontendEvent::Enumerate()).await;
loop {
let event = frontend::read_event(&mut stream).await;
match event {
Ok(event) => {
let _ = tx.send(event).await;
}
Err(e) => {
if let Some(e) = e.downcast_ref::<io::Error>() {
if e.kind() == ErrorKind::UnexpectedEof {
return;
}
}
log::error!("error reading frontend event: {e}");
return;
}
}
}
});
}
async fn handle_frontend_event(
server: &Server,
producer_tx: &Sender<ProducerEvent>,
consumer_tx: &Sender<ConsumerEvent>,
resolve_tx: &Sender<DnsRequest>,
frontend: &mut FrontendListener,
port_tx: &Sender<u16>,
event: FrontendEvent,
) -> bool {
log::debug!("frontend: {event:?}");
let response = match event {
FrontendEvent::AddClient(hostname, port, pos) => {
let handle = add_client(server, resolve_tx, hostname, HashSet::new(), port, pos).await;
let client = server
.client_manager
.borrow()
.get(handle)
.unwrap()
.client
.clone();
Some(FrontendNotify::NotifyClientCreate(client))
}
FrontendEvent::ActivateClient(handle, active) => {
activate_client(server, producer_tx, consumer_tx, handle, active).await;
Some(FrontendNotify::NotifyClientActivate(handle, active))
}
FrontendEvent::ChangePort(port) => {
let _ = port_tx.send(port).await;
None
}
FrontendEvent::DelClient(handle) => {
remove_client(server, producer_tx, consumer_tx, frontend, handle).await;
Some(FrontendNotify::NotifyClientDelete(handle))
}
FrontendEvent::Enumerate() => {
let clients = server
.client_manager
.borrow()
.get_client_states()
.map(|s| (s.client.clone(), s.active))
.collect();
Some(FrontendNotify::Enumerate(clients))
}
FrontendEvent::Shutdown() => {
log::info!("terminating gracefully...");
return true;
}
FrontendEvent::UpdateClient(handle, hostname, port, pos) => {
update_client(
server,
producer_tx,
consumer_tx,
resolve_tx,
(handle, hostname, port, pos),
)
.await;
let client = server
.client_manager
.borrow()
.get(handle)
.unwrap()
.client
.clone();
Some(FrontendNotify::NotifyClientUpdate(client))
}
};
let Some(response) = response else {
return false;
};
if let Err(e) = frontend.notify_all(response).await {
log::error!("error notifying frontend: {e}");
}
false
}
pub async fn add_client(
server: &Server,
resolver_tx: &Sender<DnsRequest>,
hostname: Option<String>,
addr: HashSet<IpAddr>,
port: u16,
pos: Position,
) -> ClientHandle {
log::info!(
"adding client [{}]{} @ {:?}",
pos,
hostname.as_deref().unwrap_or(""),
&addr
);
let handle =
server
.client_manager
.borrow_mut()
.add_client(hostname.clone(), addr, port, pos, false);
log::debug!("add_client {handle}");
if let Some(hostname) = hostname {
let _ = resolver_tx.send(DnsRequest { hostname, handle }).await;
}
handle
}
pub async fn activate_client(
server: &Server,
producer_notify_tx: &Sender<ProducerEvent>,
consumer_notify_tx: &Sender<ConsumerEvent>,
client: ClientHandle,
active: bool,
) {
let (client, pos) = match server.client_manager.borrow_mut().get_mut(client) {
Some(state) => {
state.active = active;
(state.client.handle, state.client.pos)
}
None => return,
};
if active {
let _ = producer_notify_tx
.send(ProducerEvent::ClientEvent(ClientEvent::Create(client, pos)))
.await;
let _ = consumer_notify_tx
.send(ConsumerEvent::ClientEvent(ClientEvent::Create(client, pos)))
.await;
} else {
let _ = producer_notify_tx
.send(ProducerEvent::ClientEvent(ClientEvent::Destroy(client)))
.await;
let _ = consumer_notify_tx
.send(ConsumerEvent::ClientEvent(ClientEvent::Destroy(client)))
.await;
}
}
pub async fn remove_client(
server: &Server,
producer_notify_tx: &Sender<ProducerEvent>,
consumer_notify_tx: &Sender<ConsumerEvent>,
frontend: &mut FrontendListener,
client: ClientHandle,
) -> Option<ClientHandle> {
let _ = producer_notify_tx
.send(ProducerEvent::ClientEvent(ClientEvent::Destroy(client)))
.await;
let _ = consumer_notify_tx
.send(ConsumerEvent::ClientEvent(ClientEvent::Destroy(client)))
.await;
let Some(client) = server
.client_manager
.borrow_mut()
.remove_client(client)
.map(|s| s.client.handle)
else {
return None;
};
let notify = FrontendNotify::NotifyClientDelete(client);
log::debug!("{notify:?}");
if let Err(e) = frontend.notify_all(notify).await {
log::error!("error notifying frontend: {e}");
}
Some(client)
}
async fn update_client(
server: &Server,
producer_notify_tx: &Sender<ProducerEvent>,
consumer_notify_tx: &Sender<ConsumerEvent>,
resolve_tx: &Sender<DnsRequest>,
client_update: (ClientHandle, Option<String>, u16, Position),
) {
let (handle, hostname, port, pos) = client_update;
let (hostname, handle, active) = {
// retrieve state
let mut client_manager = server.client_manager.borrow_mut();
let Some(state) = client_manager.get_mut(handle) else {
return;
};
// update pos
state.client.pos = pos;
// update port
if state.client.port != port {
state.client.port = port;
state.active_addr = state.active_addr.map(|a| SocketAddr::new(a.ip(), port));
}
// update hostname
if state.client.hostname != hostname {
state.client.ips = HashSet::new();
state.active_addr = None;
state.client.hostname = hostname;
}
log::debug!("client updated: {:?}", state);
(
state.client.hostname.clone(),
state.client.handle,
state.active,
)
};
// resolve dns
if let Some(hostname) = hostname {
let _ = resolve_tx.send(DnsRequest { hostname, handle }).await;
}
// update state in event consumer & producer
if active {
let _ = producer_notify_tx
.send(ProducerEvent::ClientEvent(ClientEvent::Destroy(handle)))
.await;
let _ = consumer_notify_tx
.send(ConsumerEvent::ClientEvent(ClientEvent::Destroy(handle)))
.await;
let _ = producer_notify_tx
.send(ProducerEvent::ClientEvent(ClientEvent::Create(handle, pos)))
.await;
let _ = consumer_notify_tx
.send(ConsumerEvent::ClientEvent(ClientEvent::Create(handle, pos)))
.await;
}
}

View File

@@ -0,0 +1,90 @@
use std::net::SocketAddr;
use anyhow::Result;
use tokio::{
net::UdpSocket,
sync::mpsc::{Receiver, Sender},
task::JoinHandle,
};
use crate::{event::Event, frontend::FrontendNotify};
use super::Server;
pub async fn new(
server: Server,
frontend_notify_tx: Sender<FrontendNotify>,
) -> Result<(
JoinHandle<()>,
Sender<(Event, SocketAddr)>,
Receiver<Result<(Event, SocketAddr)>>,
Sender<u16>,
)> {
// bind the udp socket
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), server.port.get());
let mut socket = UdpSocket::bind(listen_addr).await?;
let (receiver_tx, receiver_rx) = tokio::sync::mpsc::channel(32);
let (sender_tx, mut sender_rx) = tokio::sync::mpsc::channel(32);
let (port_tx, mut port_rx) = tokio::sync::mpsc::channel(32);
let udp_task = tokio::task::spawn_local(async move {
loop {
tokio::select! {
event = receive_event(&socket) => {
let _ = receiver_tx.send(event).await;
}
event = sender_rx.recv() => {
let Some((event, addr)) = event else {
break;
};
if let Err(e) = send_event(&socket, event, addr) {
log::warn!("udp send failed: {e}");
};
}
port = port_rx.recv() => {
let Some(port) = port else {
break;
};
if socket.local_addr().unwrap().port() == port {
continue;
}
let listen_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), port);
match UdpSocket::bind(listen_addr).await {
Ok(new_socket) => {
socket = new_socket;
server.port.replace(port);
let _ = frontend_notify_tx.send(FrontendNotify::NotifyPortChange(port, None)).await;
}
Err(e) => {
log::warn!("could not change port: {e}");
let port = socket.local_addr().unwrap().port();
let _ = frontend_notify_tx.send(FrontendNotify::NotifyPortChange(
port,
Some(format!("could not change port: {e}")),
)).await;
}
}
}
}
}
});
Ok((udp_task, sender_tx, receiver_rx, port_tx))
}
async fn receive_event(socket: &UdpSocket) -> Result<(Event, SocketAddr)> {
let mut buf = vec![0u8; 22];
let (_amt, src) = socket.recv_from(&mut buf).await?;
Ok((Event::try_from(buf)?, src))
}
fn send_event(sock: &UdpSocket, e: Event, addr: SocketAddr) -> Result<usize> {
log::trace!("{:20} ------>->->-> {addr}", e.to_string());
let data: Vec<u8> = (&e).into();
// When udp blocks, we dont want to block the event loop.
// Dropping events is better than potentially crashing the event
// producer.
Ok(sock.try_send_to(&data, addr)?)
}

131
src/server/ping_task.rs Normal file
View File

@@ -0,0 +1,131 @@
use std::{net::SocketAddr, time::Duration};
use tokio::{
sync::mpsc::{Receiver, Sender},
task::JoinHandle,
};
use crate::{client::ClientHandle, event::Event};
use super::{consumer_task::ConsumerEvent, producer_task::ProducerEvent, Server, State};
const MAX_RESPONSE_TIME: Duration = Duration::from_millis(500);
pub fn new(
server: Server,
sender_ch: Sender<(Event, SocketAddr)>,
consumer_notify: Sender<ConsumerEvent>,
producer_notify: Sender<ProducerEvent>,
mut timer_rx: Receiver<()>,
) -> JoinHandle<()> {
// timer task
let ping_task = tokio::task::spawn_local(async move {
loop {
// wait for wake up signal
let Some(_): Option<()> = timer_rx.recv().await else {
break;
};
loop {
let receiving = server.state.get() == State::Receiving;
let (ping_clients, ping_addrs) = {
let mut client_manager = server.client_manager.borrow_mut();
let ping_clients: Vec<ClientHandle> = if receiving {
// if receiving we care about clients with pressed keys
client_manager
.get_client_states_mut()
.filter(|s| !s.pressed_keys.is_empty())
.map(|s| s.client.handle)
.collect()
} else {
// if sending we care about the active client
server.active_client.get().iter().cloned().collect()
};
// get relevant socket addrs for clients
let ping_addrs: Vec<SocketAddr> = {
ping_clients
.iter()
.flat_map(|&c| client_manager.get(c))
.flat_map(|state| {
if state.alive && state.active_addr.is_some() {
vec![state.active_addr.unwrap()]
} else {
state
.client
.ips
.iter()
.cloned()
.map(|ip| SocketAddr::new(ip, state.client.port))
.collect()
}
})
.collect()
};
// reset alive
for state in client_manager.get_client_states_mut() {
state.alive = false;
}
(ping_clients, ping_addrs)
};
if receiving && ping_clients.is_empty() {
// receiving and no client has pressed keys
// -> no need to keep pinging
break;
}
// ping clients
for addr in ping_addrs {
if sender_ch.send((Event::Ping(), addr)).await.is_err() {
break;
}
}
// give clients time to resond
if receiving {
log::debug!("waiting {MAX_RESPONSE_TIME:?} for response from client with pressed keys ...");
} else {
log::debug!(
"state: {:?} => waiting {MAX_RESPONSE_TIME:?} for client to respond ...",
server.state.get()
);
}
tokio::time::sleep(MAX_RESPONSE_TIME).await;
// when anything is received from a client,
// the alive flag gets set
let unresponsive_clients: Vec<_> = {
let client_manager = server.client_manager.borrow();
ping_clients
.iter()
.filter_map(|&c| match client_manager.get(c) {
Some(state) if !state.alive => Some(c),
_ => None,
})
.collect()
};
// we may not be receiving anymore but we should respond
// to the original state and not the "new" one
if receiving {
for c in unresponsive_clients {
log::warn!("device not responding, releasing keys!");
let _ = consumer_notify.send(ConsumerEvent::ReleaseKeys(c)).await;
}
} else {
// release pointer if the active client has not responded
if !unresponsive_clients.is_empty() {
log::warn!("client not responding, releasing pointer!");
server.state.replace(State::Receiving);
let _ = producer_notify.send(ProducerEvent::Release).await;
}
}
}
}
});
ping_task
}

132
src/server/producer_task.rs Normal file
View File

@@ -0,0 +1,132 @@
use anyhow::{anyhow, Result};
use futures::StreamExt;
use std::net::SocketAddr;
use tokio::{sync::mpsc::Sender, task::JoinHandle};
use crate::{
client::{ClientEvent, ClientHandle},
event::{Event, KeyboardEvent},
producer::EventProducer,
server::State,
};
use super::Server;
#[derive(Clone, Copy, Debug)]
pub enum ProducerEvent {
/// producer must release the mouse
Release,
/// producer is notified of a change in client states
ClientEvent(ClientEvent),
/// termination signal
Terminate,
}
pub fn new(
mut producer: Box<dyn EventProducer>,
server: Server,
sender_tx: Sender<(Event, SocketAddr)>,
timer_tx: Sender<()>,
) -> (JoinHandle<Result<()>>, Sender<ProducerEvent>) {
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let task = tokio::task::spawn_local(async move {
loop {
tokio::select! {
event = producer.next() => {
let event = event.ok_or(anyhow!("event producer closed"))??;
handle_producer_event(&server, &mut producer, &sender_tx, &timer_tx, event).await?;
}
e = rx.recv() => {
log::debug!("producer notify rx: {e:?}");
match e {
Some(e) => match e {
ProducerEvent::Release => {
producer.release()?;
server.state.replace(State::Receiving);
}
ProducerEvent::ClientEvent(e) => producer.notify(e)?,
ProducerEvent::Terminate => break,
},
None => break,
}
}
}
}
anyhow::Ok(())
});
(task, tx)
}
const RELEASE_MODIFIERDS: u32 = 77; // ctrl+shift+super+alt
async fn handle_producer_event(
server: &Server,
producer: &mut Box<dyn EventProducer>,
sender_tx: &Sender<(Event, SocketAddr)>,
timer_tx: &Sender<()>,
event: (ClientHandle, Event),
) -> Result<()> {
let (c, mut e) = event;
log::trace!("({c}) {e:?}");
if let Event::Keyboard(KeyboardEvent::Modifiers { mods_depressed, .. }) = e {
if mods_depressed == RELEASE_MODIFIERDS {
producer.release()?;
server.state.replace(State::Receiving);
log::trace!("STATE ===> Receiving");
// send an event to release all the modifiers
e = Event::Disconnect();
}
}
let (addr, enter, start_timer) = {
let mut enter = false;
let mut start_timer = false;
// get client state for handle
let mut client_manager = server.client_manager.borrow_mut();
let client_state = match client_manager.get_mut(c) {
Some(state) => state,
None => {
// should not happen
log::warn!("unknown client!");
producer.release()?;
server.state.replace(State::Receiving);
log::trace!("STATE ===> Receiving");
return Ok(());
}
};
// if we just entered the client we want to send additional enter events until
// we get a leave event
if let Event::Enter() = e {
server.state.replace(State::AwaitingLeave);
server
.active_client
.replace(Some(client_state.client.handle));
log::trace!("Active client => {}", client_state.client.handle);
start_timer = true;
log::trace!("STATE ===> AwaitingLeave");
enter = true;
} else {
// ignore any potential events in receiving mode
if server.state.get() == State::Receiving && e != Event::Disconnect() {
return Ok(());
}
}
(client_state.active_addr, enter, start_timer)
};
if start_timer {
let _ = timer_tx.try_send(());
}
if let Some(addr) = addr {
if enter {
let _ = sender_tx.send((Event::Enter(), addr)).await;
}
let _ = sender_tx.send((e, addr)).await;
}
Ok(())
}

View File

@@ -0,0 +1,40 @@
use std::collections::HashSet;
use tokio::{sync::mpsc::Sender, task::JoinHandle};
use crate::{client::ClientHandle, dns::DnsResolver};
use super::Server;
#[derive(Clone)]
pub struct DnsRequest {
pub hostname: String,
pub handle: ClientHandle,
}
pub fn new(resolver: DnsResolver, server: Server) -> (JoinHandle<()>, Sender<DnsRequest>) {
let (dns_tx, mut dns_rx) = tokio::sync::mpsc::channel::<DnsRequest>(32);
let resolver_task = tokio::task::spawn_local(async move {
loop {
let (host, handle) = match dns_rx.recv().await {
Some(r) => (r.hostname, r.handle),
None => break,
};
let ips = match resolver.resolve(&host).await {
Ok(ips) => ips,
Err(e) => {
log::warn!("could not resolve host '{host}': {e}");
continue;
}
};
if let Some(state) = server.client_manager.borrow_mut().get_mut(handle) {
let mut addrs = HashSet::from_iter(state.client.fix_ips.iter().cloned());
for ip in ips {
addrs.insert(ip);
}
state.client.ips = addrs;
}
}
});
(resolver_task, dns_tx)
}