mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-03-07 11:59:59 +03:00
extract frontend crate (#186)
This commit is contained in:
committed by
GitHub
parent
12bc0d86ca
commit
be677d4c81
56
Cargo.lock
generated
56
Cargo.lock
generated
@@ -1313,21 +1313,17 @@ dependencies = [
|
||||
name = "lan-mouse"
|
||||
version = "0.9.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
"clap",
|
||||
"endi",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"glib-build-tools",
|
||||
"gtk4",
|
||||
"hickory-resolver",
|
||||
"hostname 0.4.0",
|
||||
"input-capture",
|
||||
"input-emulation",
|
||||
"input-event",
|
||||
"lan-mouse-cli",
|
||||
"lan-mouse-gtk",
|
||||
"lan-mouse-ipc",
|
||||
"lan-mouse-proto",
|
||||
"libadwaita",
|
||||
"libc",
|
||||
"local-channel",
|
||||
"log",
|
||||
@@ -1340,6 +1336,41 @@ dependencies = [
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lan-mouse-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"lan-mouse-ipc",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lan-mouse-gtk"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"glib-build-tools",
|
||||
"gtk4",
|
||||
"hostname 0.4.0",
|
||||
"lan-mouse-ipc",
|
||||
"libadwaita",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lan-mouse-ipc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lan-mouse-proto"
|
||||
version = "0.1.0"
|
||||
@@ -2090,6 +2121,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.11"
|
||||
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -1,5 +1,13 @@
|
||||
[workspace]
|
||||
members = ["input-capture", "input-emulation", "input-event", "lan-mouse-proto"]
|
||||
members = [
|
||||
"input-capture",
|
||||
"input-emulation",
|
||||
"input-event",
|
||||
"lan-mouse-ipc",
|
||||
"lan-mouse-cli",
|
||||
"lan-mouse-gtk",
|
||||
"lan-mouse-proto",
|
||||
]
|
||||
|
||||
[package]
|
||||
name = "lan-mouse"
|
||||
@@ -17,12 +25,14 @@ lto = "fat"
|
||||
input-event = { path = "input-event", version = "0.2.1" }
|
||||
input-emulation = { path = "input-emulation", version = "0.2.1", default-features = false }
|
||||
input-capture = { path = "input-capture", version = "0.2.0", default-features = false }
|
||||
lan-mouse-cli = { path = "lan-mouse-cli", version = "0.1.0" }
|
||||
lan-mouse-gtk = { path = "lan-mouse-gtk", version = "0.1.0", optional = true }
|
||||
lan-mouse-ipc = { path = "lan-mouse-ipc", version = "0.1.0" }
|
||||
lan-mouse-proto = { path = "lan-mouse-proto", version = "0.1.0" }
|
||||
|
||||
hickory-resolver = "0.24.1"
|
||||
toml = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
anyhow = "1.0.71"
|
||||
log = "0.4.20"
|
||||
env_logger = "0.11.3"
|
||||
serde_json = "1.0.107"
|
||||
@@ -38,16 +48,7 @@ tokio = { version = "1.32.0", features = [
|
||||
] }
|
||||
futures = "0.3.28"
|
||||
clap = { version = "4.4.11", features = ["derive"] }
|
||||
gtk = { package = "gtk4", version = "0.9.0", features = [
|
||||
"v4_2",
|
||||
], optional = true }
|
||||
adw = { package = "libadwaita", version = "0.7.0", features = [
|
||||
"v1_1",
|
||||
], optional = true }
|
||||
async-channel = { version = "2.1.1", optional = true }
|
||||
hostname = "0.4.0"
|
||||
slab = "0.4.9"
|
||||
endi = "1.1.0"
|
||||
thiserror = "1.0.61"
|
||||
tokio-util = "0.7.11"
|
||||
local-channel = "0.1.5"
|
||||
@@ -55,13 +56,10 @@ local-channel = "0.1.5"
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.148"
|
||||
|
||||
[build-dependencies]
|
||||
glib-build-tools = { version = "0.20.0", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["wayland", "x11", "xdg_desktop_portal", "libei", "gtk"]
|
||||
wayland = ["input-capture/wayland", "input-emulation/wayland"]
|
||||
x11 = ["input-capture/x11", "input-emulation/x11"]
|
||||
xdg_desktop_portal = ["input-emulation/xdg_desktop_portal"]
|
||||
libei = ["input-event/libei", "input-capture/libei", "input-emulation/libei"]
|
||||
gtk = ["dep:gtk", "dep:adw", "dep:async-channel", "dep:glib-build-tools"]
|
||||
gtk = ["dep:lan-mouse-gtk"]
|
||||
|
||||
@@ -116,7 +116,7 @@ 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
|
||||
sudo cp lan-mouse-gtk/resources/de.feschber.LanMouse.svg /usr/local/share/icons/hicolor/scalable/apps
|
||||
|
||||
# update icon cache
|
||||
gtk-update-icon-cache /usr/local/share/icons/hicolor/
|
||||
|
||||
8
build.rs
8
build.rs
@@ -12,12 +12,4 @@ fn main() {
|
||||
|
||||
let git_describe = String::from_utf8(git_describe.stdout).unwrap();
|
||||
println!("cargo::rustc-env=GIT_DESCRIBE={git_describe}");
|
||||
|
||||
// composite_templates
|
||||
#[cfg(feature = "gtk")]
|
||||
glib_build_tools::compile_resources(
|
||||
&["resources"],
|
||||
"resources/resources.gresource.xml",
|
||||
"lan-mouse.gresource",
|
||||
);
|
||||
}
|
||||
|
||||
18
lan-mouse-cli/Cargo.toml
Normal file
18
lan-mouse-cli/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "lan-mouse-cli"
|
||||
description = "CLI Frontend for lan-mouse"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/feschber/lan-mouse"
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3.30"
|
||||
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.1.0" }
|
||||
tokio = { version = "1.32.0", features = [
|
||||
"io-util",
|
||||
"io-std",
|
||||
"macros",
|
||||
"net",
|
||||
"rt",
|
||||
] }
|
||||
@@ -3,7 +3,7 @@ use std::{
|
||||
str::{FromStr, SplitWhitespace},
|
||||
};
|
||||
|
||||
use crate::client::{ClientHandle, Position};
|
||||
use lan_mouse_ipc::{ClientHandle, Position};
|
||||
|
||||
pub(super) enum CommandType {
|
||||
NoCommand,
|
||||
@@ -1,58 +1,41 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::StreamExt;
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
|
||||
io::{AsyncBufReadExt, BufReader},
|
||||
task::LocalSet,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
use tokio::net::tcp::{ReadHalf, WriteHalf};
|
||||
#[cfg(unix)]
|
||||
use tokio::net::unix::{ReadHalf, WriteHalf};
|
||||
|
||||
use std::io::{self, Write};
|
||||
|
||||
use crate::{
|
||||
client::{ClientConfig, ClientHandle, ClientState},
|
||||
config::DEFAULT_PORT,
|
||||
};
|
||||
|
||||
use self::command::{Command, CommandType};
|
||||
|
||||
use super::{FrontendEvent, FrontendRequest};
|
||||
use lan_mouse_ipc::{
|
||||
AsyncFrontendEventReader, AsyncFrontendRequestWriter, ClientConfig, ClientHandle, ClientState,
|
||||
FrontendEvent, FrontendRequest, IpcError, DEFAULT_PORT,
|
||||
};
|
||||
|
||||
mod command;
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
let Ok(stream) = super::wait_for_service() else {
|
||||
return Err(anyhow!("Could not connect to lan-mouse-socket"));
|
||||
};
|
||||
|
||||
pub fn run() -> Result<(), IpcError> {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()?;
|
||||
runtime.block_on(LocalSet::new().run_until(async move {
|
||||
stream.set_nonblocking(true)?;
|
||||
#[cfg(unix)]
|
||||
let mut stream = tokio::net::UnixStream::from_std(stream)?;
|
||||
#[cfg(windows)]
|
||||
let mut stream = tokio::net::TcpStream::from_std(stream)?;
|
||||
let (rx, tx) = stream.split();
|
||||
|
||||
let (rx, tx) = lan_mouse_ipc::connect_async().await?;
|
||||
let mut cli = Cli::new(rx, tx);
|
||||
cli.run().await
|
||||
}))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Cli<'a> {
|
||||
struct Cli {
|
||||
clients: Vec<(ClientHandle, ClientConfig, ClientState)>,
|
||||
rx: ReadHalf<'a>,
|
||||
tx: WriteHalf<'a>,
|
||||
rx: AsyncFrontendEventReader,
|
||||
tx: AsyncFrontendRequestWriter,
|
||||
}
|
||||
|
||||
impl<'a> Cli<'a> {
|
||||
fn new(rx: ReadHalf<'a>, tx: WriteHalf<'a>) -> Cli<'a> {
|
||||
impl Cli {
|
||||
fn new(rx: AsyncFrontendEventReader, tx: AsyncFrontendRequestWriter) -> Cli {
|
||||
Self {
|
||||
clients: vec![],
|
||||
rx,
|
||||
@@ -60,19 +43,21 @@ impl<'a> Cli<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(&mut self) -> Result<()> {
|
||||
async fn run(&mut self) -> Result<(), IpcError> {
|
||||
let stdin = tokio::io::stdin();
|
||||
let stdin = BufReader::new(stdin);
|
||||
let mut stdin = stdin.lines();
|
||||
|
||||
/* initial state sync */
|
||||
let request = FrontendRequest::Enumerate();
|
||||
self.send_request(request).await?;
|
||||
|
||||
self.clients = loop {
|
||||
let event = self.await_event().await?;
|
||||
if let FrontendEvent::Enumerate(clients) = event {
|
||||
break clients;
|
||||
match self.rx.next().await {
|
||||
Some(Ok(e)) => {
|
||||
if let FrontendEvent::Enumerate(clients) = e {
|
||||
break clients;
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => return Err(e),
|
||||
None => return Ok(()),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -92,18 +77,18 @@ impl<'a> Cli<'a> {
|
||||
};
|
||||
self.execute(cmd).await?;
|
||||
}
|
||||
event = self.await_event() => {
|
||||
let event = event?;
|
||||
self.handle_event(event);
|
||||
event = self.rx.next() => {
|
||||
if let Some(event) = event {
|
||||
self.handle_event(event?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_client(&mut self, handle: ClientHandle) -> Result<()> {
|
||||
self.send_request(FrontendRequest::GetState(handle)).await?;
|
||||
loop {
|
||||
let event = self.await_event().await?;
|
||||
async fn update_client(&mut self, handle: ClientHandle) -> Result<(), IpcError> {
|
||||
self.tx.request(FrontendRequest::GetState(handle)).await?;
|
||||
while let Some(Ok(event)) = self.rx.next().await {
|
||||
self.handle_event(event.clone());
|
||||
if let FrontendEvent::State(_, _, _) | FrontendEvent::NoSuchClient(_) = event {
|
||||
break;
|
||||
@@ -112,22 +97,23 @@ impl<'a> Cli<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute(&mut self, cmd: Command) -> Result<()> {
|
||||
async fn execute(&mut self, cmd: Command) -> Result<(), IpcError> {
|
||||
match cmd {
|
||||
Command::None => {}
|
||||
Command::Connect(pos, host, port) => {
|
||||
let request = FrontendRequest::Create;
|
||||
self.send_request(request).await?;
|
||||
self.tx.request(request).await?;
|
||||
let handle = loop {
|
||||
let event = self.await_event().await?;
|
||||
match event {
|
||||
FrontendEvent::Created(h, c, s) => {
|
||||
self.clients.push((h, c, s));
|
||||
break h;
|
||||
}
|
||||
_ => {
|
||||
self.handle_event(event);
|
||||
continue;
|
||||
if let Some(Ok(event)) = self.rx.next().await {
|
||||
match event {
|
||||
FrontendEvent::Created(h, c, s) => {
|
||||
self.clients.push((h, c, s));
|
||||
break h;
|
||||
}
|
||||
_ => {
|
||||
self.handle_event(event);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -136,35 +122,36 @@ impl<'a> Cli<'a> {
|
||||
FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT)),
|
||||
FrontendRequest::UpdatePosition(handle, pos),
|
||||
] {
|
||||
self.send_request(request).await?;
|
||||
self.tx.request(request).await?;
|
||||
}
|
||||
self.update_client(handle).await?;
|
||||
}
|
||||
Command::Disconnect(id) => {
|
||||
self.send_request(FrontendRequest::Delete(id)).await?;
|
||||
self.tx.request(FrontendRequest::Delete(id)).await?;
|
||||
loop {
|
||||
let event = self.await_event().await?;
|
||||
self.handle_event(event.clone());
|
||||
if let FrontendEvent::Deleted(_) = event {
|
||||
self.handle_event(event);
|
||||
break;
|
||||
if let Some(Ok(event)) = self.rx.next().await {
|
||||
self.handle_event(event.clone());
|
||||
if let FrontendEvent::Deleted(_) = event {
|
||||
self.handle_event(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Command::Activate(id) => {
|
||||
self.send_request(FrontendRequest::Activate(id, true))
|
||||
.await?;
|
||||
self.tx.request(FrontendRequest::Activate(id, true)).await?;
|
||||
self.update_client(id).await?;
|
||||
}
|
||||
Command::Deactivate(id) => {
|
||||
self.send_request(FrontendRequest::Activate(id, false))
|
||||
self.tx
|
||||
.request(FrontendRequest::Activate(id, false))
|
||||
.await?;
|
||||
self.update_client(id).await?;
|
||||
}
|
||||
Command::List => {
|
||||
self.send_request(FrontendRequest::Enumerate()).await?;
|
||||
loop {
|
||||
let event = self.await_event().await?;
|
||||
self.tx.request(FrontendRequest::Enumerate()).await?;
|
||||
while let Some(e) = self.rx.next().await {
|
||||
let event = e?;
|
||||
self.handle_event(event.clone());
|
||||
if let FrontendEvent::Enumerate(_) = event {
|
||||
break;
|
||||
@@ -173,12 +160,12 @@ impl<'a> Cli<'a> {
|
||||
}
|
||||
Command::SetHost(handle, host) => {
|
||||
let request = FrontendRequest::UpdateHostname(handle, Some(host.clone()));
|
||||
self.send_request(request).await?;
|
||||
self.tx.request(request).await?;
|
||||
self.update_client(handle).await?;
|
||||
}
|
||||
Command::SetPort(handle, port) => {
|
||||
let request = FrontendRequest::UpdatePort(handle, port.unwrap_or(DEFAULT_PORT));
|
||||
self.send_request(request).await?;
|
||||
self.tx.request(request).await?;
|
||||
self.update_client(handle).await?;
|
||||
}
|
||||
Command::Help => {
|
||||
@@ -291,23 +278,6 @@ impl<'a> Cli<'a> {
|
||||
eprintln!();
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_request(&mut self, request: FrontendRequest) -> io::Result<()> {
|
||||
let json = serde_json::to_string(&request).unwrap();
|
||||
let bytes = json.as_bytes();
|
||||
let len = bytes.len();
|
||||
self.tx.write_u64(len as u64).await?;
|
||||
self.tx.write_all(bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn await_event(&mut self) -> Result<FrontendEvent> {
|
||||
let len = self.rx.read_u64().await?;
|
||||
let mut buf = vec![0u8; len as usize];
|
||||
self.rx.read_exact(&mut buf).await?;
|
||||
let event: FrontendEvent = serde_json::from_slice(&buf)?;
|
||||
Ok(event)
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt() -> io::Result<()> {
|
||||
18
lan-mouse-gtk/Cargo.toml
Normal file
18
lan-mouse-gtk/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "lan-mouse-gtk"
|
||||
description = "GTK4 / Libadwaita Frontend for lan-mouse"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/feschber/lan-mouse"
|
||||
|
||||
[dependencies]
|
||||
gtk = { package = "gtk4", version = "0.9.0", features = ["v4_2"] }
|
||||
adw = { package = "libadwaita", version = "0.7.0", features = ["v1_1"] }
|
||||
async-channel = { version = "2.1.1" }
|
||||
hostname = "0.4.0"
|
||||
log = "0.4.20"
|
||||
lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.1.0" }
|
||||
|
||||
[build-dependencies]
|
||||
glib-build-tools = { version = "0.20.0" }
|
||||
8
lan-mouse-gtk/build.rs
Normal file
8
lan-mouse-gtk/build.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
fn main() {
|
||||
// composite_templates
|
||||
glib_build_tools::compile_resources(
|
||||
&["resources"],
|
||||
"resources/resources.gresource.xml",
|
||||
"lan-mouse.gresource",
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
@@ -3,7 +3,7 @@ mod imp;
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::glib::{self, Object};
|
||||
|
||||
use crate::client::{ClientConfig, ClientHandle, ClientState};
|
||||
use lan_mouse_ipc::{ClientConfig, ClientHandle, ClientState};
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ClientObject(ObjectSubclass<imp::ClientObject>);
|
||||
@@ -5,7 +5,7 @@ use gtk::glib;
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::prelude::*;
|
||||
|
||||
use crate::client::ClientHandle;
|
||||
use lan_mouse_ipc::ClientHandle;
|
||||
|
||||
use super::ClientData;
|
||||
|
||||
@@ -4,7 +4,7 @@ use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::glib::{self, Object};
|
||||
|
||||
use crate::config::DEFAULT_PORT;
|
||||
use lan_mouse_ipc::DEFAULT_PORT;
|
||||
|
||||
use super::ClientObject;
|
||||
|
||||
@@ -2,16 +2,13 @@ mod client_object;
|
||||
mod client_row;
|
||||
mod window;
|
||||
|
||||
use std::{
|
||||
env,
|
||||
io::{ErrorKind, Read},
|
||||
process, str,
|
||||
};
|
||||
use std::{env, process, str};
|
||||
|
||||
use crate::frontend::gtk::window::Window;
|
||||
use window::Window;
|
||||
|
||||
use lan_mouse_ipc::FrontendEvent;
|
||||
|
||||
use adw::Application;
|
||||
use endi::{Endian, ReadBytes};
|
||||
use gtk::{
|
||||
gdk::Display, glib::clone, prelude::*, subclass::prelude::ObjectSubclassIsExt, IconTheme,
|
||||
};
|
||||
@@ -19,8 +16,6 @@ use gtk::{gio, glib, prelude::ApplicationExt};
|
||||
|
||||
use self::client_object::ClientObject;
|
||||
|
||||
use super::FrontendEvent;
|
||||
|
||||
pub fn run() -> glib::ExitCode {
|
||||
log::debug!("running gtk frontend");
|
||||
#[cfg(windows)]
|
||||
@@ -65,15 +60,8 @@ fn load_icons() {
|
||||
|
||||
fn build_ui(app: &Application) {
|
||||
log::debug!("connecting to lan-mouse-socket");
|
||||
let mut rx = match super::wait_for_service() {
|
||||
Ok(stream) => stream,
|
||||
Err(e) => {
|
||||
log::error!("could not connect to lan-mouse-socket: {e}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
let tx = match rx.try_clone() {
|
||||
Ok(sock) => sock,
|
||||
let (mut frontend_rx, frontend_tx) = match lan_mouse_ipc::connect() {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => {
|
||||
log::error!("{e}");
|
||||
process::exit(1);
|
||||
@@ -84,35 +72,18 @@ fn build_ui(app: &Application) {
|
||||
let (sender, receiver) = async_channel::bounded(10);
|
||||
|
||||
gio::spawn_blocking(move || {
|
||||
match loop {
|
||||
// read length
|
||||
let len = match rx.read_u64(Endian::Big) {
|
||||
Ok(l) => l,
|
||||
Err(e) if e.kind() == ErrorKind::UnexpectedEof => break Ok(()),
|
||||
Err(e) => break Err(e),
|
||||
};
|
||||
|
||||
// read payload
|
||||
let mut buf = vec![0u8; len as usize];
|
||||
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_blocking(notify).unwrap(),
|
||||
Err(e) => log::error!("{e}"),
|
||||
while let Some(e) = frontend_rx.next_event() {
|
||||
match e {
|
||||
Ok(e) => sender.send_blocking(e).unwrap(),
|
||||
Err(e) => {
|
||||
log::error!("{e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
} {
|
||||
Ok(()) => {}
|
||||
Err(e) => log::error!("{e}"),
|
||||
}
|
||||
});
|
||||
|
||||
let window = Window::new(app, tx);
|
||||
let window = Window::new(app, frontend_tx);
|
||||
|
||||
glib::spawn_future_local(clone!(
|
||||
#[weak]
|
||||
@@ -1,16 +1,7 @@
|
||||
mod imp;
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::net::TcpStream;
|
||||
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use endi::{Endian, WriteBytes};
|
||||
use glib::{clone, Object};
|
||||
use gtk::{
|
||||
gio,
|
||||
@@ -18,13 +9,12 @@ use gtk::{
|
||||
ListBox, NoSelection,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
client::{ClientConfig, ClientHandle, ClientState, Position},
|
||||
config::DEFAULT_PORT,
|
||||
frontend::{gtk::client_object::ClientObject, FrontendRequest},
|
||||
use lan_mouse_ipc::{
|
||||
ClientConfig, ClientHandle, ClientState, FrontendRequest, FrontendRequestWriter, Position,
|
||||
DEFAULT_PORT,
|
||||
};
|
||||
|
||||
use super::client_row::ClientRow;
|
||||
use super::{client_object::ClientObject, client_row::ClientRow};
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct Window(ObjectSubclass<imp::Window>)
|
||||
@@ -34,13 +24,13 @@ glib::wrapper! {
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub(crate) fn new(
|
||||
app: &adw::Application,
|
||||
#[cfg(unix)] tx: UnixStream,
|
||||
#[cfg(windows)] tx: TcpStream,
|
||||
) -> Self {
|
||||
pub(crate) fn new(app: &adw::Application, conn: FrontendRequestWriter) -> Self {
|
||||
let window: Self = Object::builder().property("application", app).build();
|
||||
window.imp().stream.borrow_mut().replace(tx);
|
||||
window
|
||||
.imp()
|
||||
.frontend_request_writer
|
||||
.borrow_mut()
|
||||
.replace(conn);
|
||||
window
|
||||
}
|
||||
|
||||
@@ -292,16 +282,10 @@ impl Window {
|
||||
self.request(FrontendRequest::Delete(client.handle()));
|
||||
}
|
||||
|
||||
pub fn request(&self, event: FrontendRequest) {
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
log::debug!("requesting: {json}");
|
||||
let mut stream = self.imp().stream.borrow_mut();
|
||||
let stream = stream.as_mut().unwrap();
|
||||
let bytes = json.as_bytes();
|
||||
if let Err(e) = stream.write_u64(Endian::Big, bytes.len() as u64) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
if let Err(e) = stream.write(bytes) {
|
||||
pub fn request(&self, request: FrontendRequest) {
|
||||
let mut requester = self.imp().frontend_request_writer.borrow_mut();
|
||||
let requester = requester.as_mut().unwrap();
|
||||
if let Err(e) = requester.request(request) {
|
||||
log::error!("error sending message: {e}");
|
||||
};
|
||||
}
|
||||
@@ -1,17 +1,12 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::net::TcpStream;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
use adw::subclass::prelude::*;
|
||||
use adw::{prelude::*, ActionRow, PreferencesGroup, ToastOverlay};
|
||||
use glib::subclass::InitializingObject;
|
||||
use gtk::glib::clone;
|
||||
use gtk::{gdk, gio, glib, Button, CompositeTemplate, Entry, Label, ListBox};
|
||||
|
||||
use crate::config::DEFAULT_PORT;
|
||||
use lan_mouse_ipc::{FrontendRequestWriter, DEFAULT_PORT};
|
||||
|
||||
#[derive(CompositeTemplate, Default)]
|
||||
#[template(resource = "/de/feschber/LanMouse/window.ui")]
|
||||
@@ -41,10 +36,7 @@ pub struct Window {
|
||||
#[template_child]
|
||||
pub input_capture_button: TemplateChild<Button>,
|
||||
pub clients: RefCell<Option<gio::ListStore>>,
|
||||
#[cfg(unix)]
|
||||
pub stream: RefCell<Option<UnixStream>>,
|
||||
#[cfg(windows)]
|
||||
pub stream: RefCell<Option<TcpStream>>,
|
||||
pub frontend_request_writer: RefCell<Option<FrontendRequestWriter>>,
|
||||
pub port: Cell<u16>,
|
||||
pub capture_active: Cell<bool>,
|
||||
pub emulation_active: Cell<bool>,
|
||||
16
lan-mouse-ipc/Cargo.toml
Normal file
16
lan-mouse-ipc/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "lan-mouse-ipc"
|
||||
description = "library for communication between lan-mouse service and frontends"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/feschber/lan-mouse"
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3.30"
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.107"
|
||||
thiserror = "1.0.63"
|
||||
tokio = { version = "1.32.0", features = ["net", "io-util", "time"] }
|
||||
tokio-stream = { version = "0.1.15", features = ["io-util"] }
|
||||
89
lan-mouse-ipc/src/connect.rs
Normal file
89
lan-mouse-ipc/src/connect.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
|
||||
use std::{
|
||||
cmp::min,
|
||||
io::{self, prelude::*, BufReader, LineWriter, Lines},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::net::TcpStream;
|
||||
|
||||
pub struct FrontendEventReader {
|
||||
#[cfg(unix)]
|
||||
lines: Lines<BufReader<UnixStream>>,
|
||||
#[cfg(windows)]
|
||||
lines: Lines<BufReader<TcpStream>>,
|
||||
}
|
||||
|
||||
pub struct FrontendRequestWriter {
|
||||
#[cfg(unix)]
|
||||
line_writer: LineWriter<UnixStream>,
|
||||
#[cfg(windows)]
|
||||
line_writer: LineWriter<TcpStream>,
|
||||
}
|
||||
|
||||
impl FrontendEventReader {
|
||||
pub fn next_event(&mut self) -> Option<Result<FrontendEvent, IpcError>> {
|
||||
match self.lines.next()? {
|
||||
Err(e) => Some(Err(e.into())),
|
||||
Ok(l) => Some(serde_json::from_str(l.as_str()).map_err(|e| e.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FrontendRequestWriter {
|
||||
pub fn request(&mut self, request: FrontendRequest) -> Result<(), io::Error> {
|
||||
let mut json = serde_json::to_string(&request).unwrap();
|
||||
log::debug!("requesting: {json}");
|
||||
json.push('\n');
|
||||
self.line_writer.write_all(json.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect() -> Result<(FrontendEventReader, FrontendRequestWriter), ConnectionError> {
|
||||
let rx = wait_for_service()?;
|
||||
let tx = rx.try_clone()?;
|
||||
let buf_reader = BufReader::new(rx);
|
||||
let lines = buf_reader.lines();
|
||||
let line_writer = LineWriter::new(tx);
|
||||
let reader = FrontendEventReader { lines };
|
||||
let writer = FrontendRequestWriter { line_writer };
|
||||
Ok((reader, writer))
|
||||
}
|
||||
|
||||
/// wait for the lan-mouse socket to come online
|
||||
#[cfg(unix)]
|
||||
fn wait_for_service() -> Result<UnixStream, ConnectionError> {
|
||||
let socket_path = crate::default_socket_path()?;
|
||||
let mut duration = Duration::from_millis(10);
|
||||
loop {
|
||||
if let Ok(stream) = UnixStream::connect(&socket_path) {
|
||||
break Ok(stream);
|
||||
}
|
||||
// a signaling mechanism or inotify could be used to
|
||||
// improve this
|
||||
thread::sleep(exponential_back_off(&mut duration));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn wait_for_service() -> Result<TcpStream, ConnectionError> {
|
||||
let mut duration = Duration::from_millis(10);
|
||||
loop {
|
||||
if let Ok(stream) = TcpStream::connect("127.0.0.1:5252") {
|
||||
break Ok(stream);
|
||||
}
|
||||
thread::sleep(exponential_back_off(&mut duration));
|
||||
}
|
||||
}
|
||||
|
||||
fn exponential_back_off(duration: &mut Duration) -> Duration {
|
||||
let new = duration.saturating_mul(2);
|
||||
*duration = min(new, Duration::from_secs(1));
|
||||
*duration
|
||||
}
|
||||
104
lan-mouse-ipc/src/connect_async.rs
Normal file
104
lan-mouse-ipc/src/connect_async.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use crate::{ConnectionError, FrontendEvent, FrontendRequest, IpcError};
|
||||
use std::{
|
||||
cmp::min,
|
||||
io,
|
||||
task::{ready, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use futures::{Stream, StreamExt};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, ReadHalf, WriteHalf};
|
||||
use tokio_stream::wrappers::LinesStream;
|
||||
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
#[cfg(windows)]
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
pub struct AsyncFrontendEventReader {
|
||||
#[cfg(unix)]
|
||||
lines_stream: LinesStream<BufReader<ReadHalf<UnixStream>>>,
|
||||
#[cfg(windows)]
|
||||
lines_stream: LinesStream<BufReader<ReadHalf<TcpStream>>>,
|
||||
}
|
||||
|
||||
pub struct AsyncFrontendRequestWriter {
|
||||
#[cfg(unix)]
|
||||
tx: WriteHalf<UnixStream>,
|
||||
#[cfg(windows)]
|
||||
tx: WriteHalf<TcpStream>,
|
||||
}
|
||||
|
||||
impl Stream for AsyncFrontendEventReader {
|
||||
type Item = Result<FrontendEvent, IpcError>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
let line = ready!(self.lines_stream.poll_next_unpin(cx));
|
||||
let event = line.map(|l| {
|
||||
l.map_err(Into::<IpcError>::into)
|
||||
.and_then(|l| serde_json::from_str(l.as_str()).map_err(|e| e.into()))
|
||||
});
|
||||
Poll::Ready(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncFrontendRequestWriter {
|
||||
pub async fn request(&mut self, request: FrontendRequest) -> Result<(), io::Error> {
|
||||
let mut json = serde_json::to_string(&request).unwrap();
|
||||
log::debug!("requesting: {json}");
|
||||
json.push('\n');
|
||||
self.tx.write_all(json.as_bytes()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect_async(
|
||||
) -> Result<(AsyncFrontendEventReader, AsyncFrontendRequestWriter), ConnectionError> {
|
||||
let stream = wait_for_service().await?;
|
||||
#[cfg(unix)]
|
||||
let (rx, tx): (ReadHalf<UnixStream>, WriteHalf<UnixStream>) = tokio::io::split(stream);
|
||||
#[cfg(windows)]
|
||||
let (rx, tx): (ReadHalf<TcpStream>, WriteHalf<TcpStream>) = tokio::io::split(stream);
|
||||
let buf_reader = BufReader::new(rx);
|
||||
let lines = buf_reader.lines();
|
||||
let lines_stream = LinesStream::new(lines);
|
||||
let reader = AsyncFrontendEventReader { lines_stream };
|
||||
let writer = AsyncFrontendRequestWriter { tx };
|
||||
Ok((reader, writer))
|
||||
}
|
||||
|
||||
/// wait for the lan-mouse socket to come online
|
||||
#[cfg(unix)]
|
||||
async fn wait_for_service() -> Result<UnixStream, ConnectionError> {
|
||||
let socket_path = crate::default_socket_path()?;
|
||||
let mut duration = Duration::from_millis(10);
|
||||
loop {
|
||||
if let Ok(stream) = UnixStream::connect(&socket_path).await {
|
||||
break Ok(stream);
|
||||
}
|
||||
// a signaling mechanism or inotify could be used to
|
||||
// improve this
|
||||
tokio::time::sleep(exponential_back_off(&mut duration)).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn wait_for_service() -> Result<TcpStream, ConnectionError> {
|
||||
let mut duration = Duration::from_millis(10);
|
||||
loop {
|
||||
if let Ok(stream) = TcpStream::connect("127.0.0.1:5252").await {
|
||||
break Ok(stream);
|
||||
}
|
||||
tokio::time::sleep(exponential_back_off(&mut duration)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn exponential_back_off(duration: &mut Duration) -> Duration {
|
||||
let new = duration.saturating_mul(2);
|
||||
*duration = min(new, Duration::from_secs(1));
|
||||
*duration
|
||||
}
|
||||
262
lan-mouse-ipc/src/lib.rs
Normal file
262
lan-mouse-ipc/src/lib.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
env::VarError,
|
||||
fmt::Display,
|
||||
io,
|
||||
net::{IpAddr, SocketAddr},
|
||||
str::FromStr,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod connect;
|
||||
mod connect_async;
|
||||
mod listen;
|
||||
|
||||
pub use connect::{connect, FrontendEventReader, FrontendRequestWriter};
|
||||
pub use connect_async::{connect_async, AsyncFrontendEventReader, AsyncFrontendRequestWriter};
|
||||
pub use listen::AsyncFrontendListener;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConnectionError {
|
||||
#[error(transparent)]
|
||||
SocketPath(#[from] SocketPathError),
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ListenerCreationError {
|
||||
#[error("could not determine socket-path: `{0}`")]
|
||||
SocketPath(#[from] SocketPathError),
|
||||
#[error("service already running!")]
|
||||
AlreadyRunning,
|
||||
#[error("failed to bind lan-mouse socket: `{0}`")]
|
||||
Bind(io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum IpcError {
|
||||
#[error("io error occured: `{0}`")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("invalid json: `{0}`")]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
Connection(#[from] ConnectionError),
|
||||
#[error(transparent)]
|
||||
Listen(#[from] ListenerCreationError),
|
||||
}
|
||||
|
||||
pub const DEFAULT_PORT: u16 = 4242;
|
||||
|
||||
#[derive(Debug, Default, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum Position {
|
||||
#[default]
|
||||
Left,
|
||||
Right,
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("not a valid position: {pos}")]
|
||||
pub struct PositionParseError {
|
||||
pos: String,
|
||||
}
|
||||
|
||||
impl FromStr for Position {
|
||||
type Err = PositionParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"left" => Ok(Self::Left),
|
||||
"right" => Ok(Self::Right),
|
||||
"top" => Ok(Self::Top),
|
||||
"bottom" => Ok(Self::Bottom),
|
||||
_ => Err(PositionParseError { pos: s.into() }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Position {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Position::Left => "left",
|
||||
Position::Right => "right",
|
||||
Position::Top => "top",
|
||||
Position::Bottom => "bottom",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 ClientConfig {
|
||||
/// hostname of this client
|
||||
pub hostname: Option<String>,
|
||||
/// fix ips, determined by the user
|
||||
pub fix_ips: Vec<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
|
||||
pub pos: Position,
|
||||
/// enter hook
|
||||
pub cmd: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ClientConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
port: DEFAULT_PORT,
|
||||
hostname: Default::default(),
|
||||
fix_ips: Default::default(),
|
||||
pos: Default::default(),
|
||||
cmd: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type ClientHandle = u64;
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct ClientState {
|
||||
/// events should be sent to and received from the client
|
||||
pub active: bool,
|
||||
/// `active` address of the client, used to send data to.
|
||||
/// This should generally be the socket address where data
|
||||
/// was last received from.
|
||||
pub active_addr: Option<SocketAddr>,
|
||||
/// tracks whether or not the client is responding to pings
|
||||
pub alive: bool,
|
||||
/// ips from dns
|
||||
pub dns_ips: Vec<IpAddr>,
|
||||
/// 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 ips: HashSet<IpAddr>,
|
||||
/// client has pressed keys
|
||||
pub has_pressed_keys: bool,
|
||||
/// dns resolving in progress
|
||||
pub resolving: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum FrontendEvent {
|
||||
/// a client was created
|
||||
Created(ClientHandle, ClientConfig, ClientState),
|
||||
/// no such client
|
||||
NoSuchClient(ClientHandle),
|
||||
/// state changed
|
||||
State(ClientHandle, ClientConfig, ClientState),
|
||||
/// the client was deleted
|
||||
Deleted(ClientHandle),
|
||||
/// new port, reason of failure (if failed)
|
||||
PortChanged(u16, Option<String>),
|
||||
/// list of all clients, used for initial state synchronization
|
||||
Enumerate(Vec<(ClientHandle, ClientConfig, ClientState)>),
|
||||
/// an error occured
|
||||
Error(String),
|
||||
/// capture status
|
||||
CaptureStatus(Status),
|
||||
/// emulation status
|
||||
EmulationStatus(Status),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum FrontendRequest {
|
||||
/// activate/deactivate client
|
||||
Activate(ClientHandle, bool),
|
||||
/// add a new client
|
||||
Create,
|
||||
/// change the listen port (recreate udp listener)
|
||||
ChangePort(u16),
|
||||
/// remove a client
|
||||
Delete(ClientHandle),
|
||||
/// request an enumeration of all clients
|
||||
Enumerate(),
|
||||
/// resolve dns
|
||||
ResolveDns(ClientHandle),
|
||||
/// update hostname
|
||||
UpdateHostname(ClientHandle, Option<String>),
|
||||
/// update port
|
||||
UpdatePort(ClientHandle, u16),
|
||||
/// update position
|
||||
UpdatePosition(ClientHandle, Position),
|
||||
/// update fix-ips
|
||||
UpdateFixIps(ClientHandle, Vec<IpAddr>),
|
||||
/// request the state of the given client
|
||||
GetState(ClientHandle),
|
||||
/// request reenabling input capture
|
||||
EnableCapture,
|
||||
/// request reenabling input emulation
|
||||
EnableEmulation,
|
||||
/// synchronize all state
|
||||
Sync,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
|
||||
pub enum Status {
|
||||
#[default]
|
||||
Disabled,
|
||||
Enabled,
|
||||
}
|
||||
|
||||
impl From<Status> for bool {
|
||||
fn from(status: Status) -> Self {
|
||||
match status {
|
||||
Status::Enabled => true,
|
||||
Status::Disabled => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
const LAN_MOUSE_SOCKET_NAME: &str = "lan-mouse-socket.sock";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SocketPathError {
|
||||
#[error("could not determine $XDG_RUNTIME_DIR: `{0}`")]
|
||||
XdgRuntimeDirNotFound(VarError),
|
||||
#[error("could not determine $HOME: `{0}`")]
|
||||
HomeDirNotFound(VarError),
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
pub fn default_socket_path() -> Result<PathBuf, SocketPathError> {
|
||||
let xdg_runtime_dir =
|
||||
env::var("XDG_RUNTIME_DIR").map_err(SocketPathError::XdgRuntimeDirNotFound)?;
|
||||
Ok(Path::new(xdg_runtime_dir.as_str()).join(LAN_MOUSE_SOCKET_NAME))
|
||||
}
|
||||
|
||||
#[cfg(all(unix, target_os = "macos"))]
|
||||
pub fn default_socket_path() -> Result<PathBuf, SocketPathError> {
|
||||
let home = env::var("HOME").map_err(SocketPathError::HomeDirNotFound)?;
|
||||
Ok(Path::new(home.as_str())
|
||||
.join("Library")
|
||||
.join("Caches")
|
||||
.join(LAN_MOUSE_SOCKET_NAME))
|
||||
}
|
||||
147
lan-mouse-ipc/src/listen.rs
Normal file
147
lan-mouse-ipc/src/listen.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use futures::{stream::SelectAll, Stream, StreamExt};
|
||||
#[cfg(unix)]
|
||||
use std::path::PathBuf;
|
||||
use std::{
|
||||
io::ErrorKind,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, ReadHalf, WriteHalf};
|
||||
use tokio_stream::wrappers::LinesStream;
|
||||
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixListener;
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
#[cfg(windows)]
|
||||
use tokio::net::TcpListener;
|
||||
#[cfg(windows)]
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{FrontendEvent, FrontendRequest, IpcError, ListenerCreationError};
|
||||
|
||||
pub struct AsyncFrontendListener {
|
||||
#[cfg(windows)]
|
||||
listener: TcpListener,
|
||||
#[cfg(unix)]
|
||||
listener: UnixListener,
|
||||
#[cfg(unix)]
|
||||
socket_path: PathBuf,
|
||||
#[cfg(unix)]
|
||||
line_streams: SelectAll<LinesStream<BufReader<ReadHalf<UnixStream>>>>,
|
||||
#[cfg(windows)]
|
||||
line_streams: SelectAll<LinesStream<BufReader<ReadHalf<TcpStream>>>>,
|
||||
#[cfg(unix)]
|
||||
tx_streams: Vec<WriteHalf<UnixStream>>,
|
||||
#[cfg(windows)]
|
||||
tx_streams: Vec<WriteHalf<TcpStream>>,
|
||||
}
|
||||
|
||||
impl AsyncFrontendListener {
|
||||
pub async fn new() -> Result<Self, ListenerCreationError> {
|
||||
#[cfg(unix)]
|
||||
let (socket_path, listener) = {
|
||||
let socket_path = crate::default_socket_path()?;
|
||||
|
||||
log::debug!("remove socket: {:?}", socket_path);
|
||||
if socket_path.exists() {
|
||||
// try to connect to see if some other instance
|
||||
// of lan-mouse is already running
|
||||
match UnixStream::connect(&socket_path).await {
|
||||
// connected -> lan-mouse is already running
|
||||
Ok(_) => return Err(ListenerCreationError::AlreadyRunning),
|
||||
// lan-mouse is not running but a socket was left behind
|
||||
Err(e) => {
|
||||
log::debug!("{socket_path:?}: {e} - removing left behind socket");
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
let listener = match UnixListener::bind(&socket_path) {
|
||||
Ok(ls) => ls,
|
||||
// some other lan-mouse instance has bound the socket in the meantime
|
||||
Err(e) if e.kind() == ErrorKind::AddrInUse => {
|
||||
return Err(ListenerCreationError::AlreadyRunning)
|
||||
}
|
||||
Err(e) => return Err(ListenerCreationError::Bind(e)),
|
||||
};
|
||||
(socket_path, listener)
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
let listener = match TcpListener::bind("127.0.0.1:5252").await {
|
||||
Ok(ls) => ls,
|
||||
// some other lan-mouse instance has bound the socket in the meantime
|
||||
Err(e) if e.kind() == ErrorKind::AddrInUse => {
|
||||
return Err(ListenerCreationError::AlreadyRunning)
|
||||
}
|
||||
Err(e) => return Err(ListenerCreationError::Bind(e)),
|
||||
};
|
||||
|
||||
let adapter = Self {
|
||||
listener,
|
||||
#[cfg(unix)]
|
||||
socket_path,
|
||||
line_streams: SelectAll::new(),
|
||||
tx_streams: vec![],
|
||||
};
|
||||
|
||||
Ok(adapter)
|
||||
}
|
||||
|
||||
pub async fn broadcast(&mut self, notify: FrontendEvent) {
|
||||
// encode event
|
||||
let mut json = serde_json::to_string(¬ify).unwrap();
|
||||
json.push('\n');
|
||||
|
||||
let mut keep = vec![];
|
||||
// TODO do simultaneously
|
||||
for tx in self.tx_streams.iter_mut() {
|
||||
// write len + payload
|
||||
if tx.write(json.as_bytes()).await.is_err() {
|
||||
keep.push(false);
|
||||
continue;
|
||||
}
|
||||
keep.push(true);
|
||||
}
|
||||
|
||||
// could not find a better solution because async
|
||||
let mut keep = keep.into_iter();
|
||||
self.tx_streams.retain(|_| keep.next().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Drop for AsyncFrontendListener {
|
||||
fn drop(&mut self) {
|
||||
log::debug!("remove socket: {:?}", self.socket_path);
|
||||
let _ = std::fs::remove_file(&self.socket_path);
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for AsyncFrontendListener {
|
||||
type Item = Result<FrontendRequest, IpcError>;
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
if let Poll::Ready(Some(Ok(l))) = self.line_streams.poll_next_unpin(cx) {
|
||||
let request = serde_json::from_str(l.as_str()).map_err(|e| e.into());
|
||||
return Poll::Ready(Some(request));
|
||||
}
|
||||
let mut sync = false;
|
||||
while let Poll::Ready(Ok((stream, _))) = self.listener.poll_accept(cx) {
|
||||
let (rx, tx) = tokio::io::split(stream);
|
||||
let buf_reader = BufReader::new(rx);
|
||||
let lines = buf_reader.lines();
|
||||
let lines = LinesStream::new(lines);
|
||||
self.line_streams.push(lines);
|
||||
self.tx_streams.push(tx);
|
||||
sync = true;
|
||||
}
|
||||
if sync {
|
||||
Poll::Ready(Some(Ok(FrontendRequest::Sync)))
|
||||
} else {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,17 @@ use input_capture::{self, CaptureError, CaptureEvent, InputCapture, InputCapture
|
||||
use input_event::{Event, KeyboardEvent};
|
||||
use tokio::task::LocalSet;
|
||||
|
||||
pub fn run() -> anyhow::Result<()> {
|
||||
pub fn run() -> Result<(), InputCaptureError> {
|
||||
log::info!("running input capture test");
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()?;
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let config = Config::new()?;
|
||||
let config = Config::new().unwrap();
|
||||
|
||||
Ok(runtime.block_on(LocalSet::new().run_until(input_capture_test(config)))?)
|
||||
runtime.block_on(LocalSet::new().run_until(input_capture_test(config)))
|
||||
}
|
||||
|
||||
async fn input_capture_test(config: Config) -> Result<(), InputCaptureError> {
|
||||
|
||||
131
src/client.rs
131
src/client.rs
@@ -1,135 +1,8 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::Display,
|
||||
net::{IpAddr, SocketAddr},
|
||||
str::FromStr,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use slab::Slab;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::config::DEFAULT_PORT;
|
||||
use input_capture;
|
||||
|
||||
#[derive(Debug, Default, Eq, Hash, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum Position {
|
||||
#[default]
|
||||
Left,
|
||||
Right,
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl From<Position> for input_capture::Position {
|
||||
fn from(position: Position) -> input_capture::Position {
|
||||
match position {
|
||||
Position::Left => input_capture::Position::Left,
|
||||
Position::Right => input_capture::Position::Right,
|
||||
Position::Top => input_capture::Position::Top,
|
||||
Position::Bottom => input_capture::Position::Bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("not a valid position: {pos}")]
|
||||
pub struct PositionParseError {
|
||||
pos: String,
|
||||
}
|
||||
|
||||
impl FromStr for Position {
|
||||
type Err = PositionParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"left" => Ok(Self::Left),
|
||||
"right" => Ok(Self::Right),
|
||||
"top" => Ok(Self::Top),
|
||||
"bottom" => Ok(Self::Bottom),
|
||||
_ => Err(PositionParseError { pos: s.into() }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Position {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Position::Left => "left",
|
||||
Position::Right => "right",
|
||||
Position::Top => "top",
|
||||
Position::Bottom => "bottom",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 ClientConfig {
|
||||
/// hostname of this client
|
||||
pub hostname: Option<String>,
|
||||
/// fix ips, determined by the user
|
||||
pub fix_ips: Vec<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
|
||||
pub pos: Position,
|
||||
/// enter hook
|
||||
pub cmd: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ClientConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
port: DEFAULT_PORT,
|
||||
hostname: Default::default(),
|
||||
fix_ips: Default::default(),
|
||||
pos: Default::default(),
|
||||
cmd: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type ClientHandle = u64;
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct ClientState {
|
||||
/// events should be sent to and received from the client
|
||||
pub active: bool,
|
||||
/// `active` address of the client, used to send data to.
|
||||
/// This should generally be the socket address where data
|
||||
/// was last received from.
|
||||
pub active_addr: Option<SocketAddr>,
|
||||
/// tracks whether or not the client is responding to pings
|
||||
pub alive: bool,
|
||||
/// ips from dns
|
||||
pub dns_ips: Vec<IpAddr>,
|
||||
/// 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 ips: HashSet<IpAddr>,
|
||||
/// client has pressed keys
|
||||
pub has_pressed_keys: bool,
|
||||
/// dns resolving in progress
|
||||
pub resolving: bool,
|
||||
}
|
||||
use lan_mouse_ipc::{ClientConfig, ClientHandle, ClientState, Position};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ClientManager {
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::env::{self, VarError};
|
||||
use std::fmt::Display;
|
||||
use std::fs;
|
||||
use std::net::IpAddr;
|
||||
use std::{error::Error, fs};
|
||||
use std::{collections::HashSet, io};
|
||||
use thiserror::Error;
|
||||
use toml;
|
||||
|
||||
use crate::client::Position;
|
||||
use lan_mouse_ipc::{Position, DEFAULT_PORT};
|
||||
|
||||
use input_event::scancode::{
|
||||
self,
|
||||
Linux::{KeyLeftAlt, KeyLeftCtrl, KeyLeftMeta, KeyLeftShift},
|
||||
};
|
||||
|
||||
pub const DEFAULT_PORT: u16 = 4242;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ConfigToml {
|
||||
pub capture_backend: Option<CaptureBackend>,
|
||||
@@ -42,7 +40,7 @@ pub struct TomlClient {
|
||||
}
|
||||
|
||||
impl ConfigToml {
|
||||
pub fn new(path: &str) -> Result<ConfigToml, Box<dyn Error>> {
|
||||
pub fn new(path: &str) -> Result<ConfigToml, ConfigError> {
|
||||
let config = fs::read_to_string(path)?;
|
||||
log::info!("using config: \"{path}\"");
|
||||
Ok(toml::from_str::<_>(&config)?)
|
||||
@@ -231,11 +229,21 @@ pub struct ConfigClient {
|
||||
pub enter_hook: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigError {
|
||||
#[error(transparent)]
|
||||
Toml(#[from] toml::de::Error),
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
#[error(transparent)]
|
||||
Var(#[from] VarError),
|
||||
}
|
||||
|
||||
const DEFAULT_RELEASE_KEYS: [scancode::Linux; 4] =
|
||||
[KeyLeftCtrl, KeyLeftShift, KeyLeftMeta, KeyLeftAlt];
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Result<Self> {
|
||||
pub fn new() -> Result<Self, ConfigError> {
|
||||
let args = CliArgs::parse();
|
||||
let config_file = "config.toml";
|
||||
#[cfg(unix)]
|
||||
|
||||
@@ -3,7 +3,8 @@ use std::net::IpAddr;
|
||||
|
||||
use hickory_resolver::{error::ResolveError, TokioAsyncResolver};
|
||||
|
||||
use crate::{client::ClientHandle, server::Server};
|
||||
use crate::server::Server;
|
||||
use lan_mouse_ipc::ClientHandle;
|
||||
|
||||
pub(crate) struct DnsResolver {
|
||||
resolver: TokioAsyncResolver,
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use crate::config::Config;
|
||||
use anyhow::Result;
|
||||
use input_emulation::InputEmulation;
|
||||
use input_emulation::{InputEmulation, InputEmulationError};
|
||||
use input_event::{Event, PointerEvent};
|
||||
use std::f64::consts::PI;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::task::LocalSet;
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
pub fn run() -> Result<(), InputEmulationError> {
|
||||
log::info!("running input emulation test");
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()?;
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let config = Config::new()?;
|
||||
let config = Config::new().unwrap();
|
||||
|
||||
runtime.block_on(LocalSet::new().run_until(input_emulation_test(config)))
|
||||
}
|
||||
@@ -21,7 +21,7 @@ pub fn run() -> Result<()> {
|
||||
const FREQUENCY_HZ: f64 = 1.0;
|
||||
const RADIUS: f64 = 100.0;
|
||||
|
||||
async fn input_emulation_test(config: Config) -> Result<()> {
|
||||
async fn input_emulation_test(config: Config) -> Result<(), InputEmulationError> {
|
||||
let backend = config.emulation_backend.map(|b| b.into());
|
||||
let mut emulation = InputEmulation::new(backend).await?;
|
||||
emulation.create(0).await;
|
||||
|
||||
307
src/frontend.rs
307
src/frontend.rs
@@ -1,307 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::{cmp::min, io::ErrorKind, net::IpAddr, str, time::Duration};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use tokio::io::ReadHalf;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, WriteHalf};
|
||||
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixListener;
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
#[cfg(windows)]
|
||||
use tokio::net::TcpListener;
|
||||
#[cfg(windows)]
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
client::{ClientConfig, ClientHandle, ClientState, Position},
|
||||
config::{Config, Frontend},
|
||||
};
|
||||
|
||||
/// cli frontend
|
||||
pub mod cli;
|
||||
|
||||
/// gtk frontend
|
||||
#[cfg(feature = "gtk")]
|
||||
pub mod gtk;
|
||||
|
||||
pub fn run_frontend(config: &Config) -> Result<()> {
|
||||
match config.frontend {
|
||||
#[cfg(feature = "gtk")]
|
||||
Frontend::Gtk => {
|
||||
gtk::run();
|
||||
}
|
||||
#[cfg(not(feature = "gtk"))]
|
||||
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
|
||||
Frontend::Cli => {
|
||||
cli::run()?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exponential_back_off(duration: &mut Duration) -> &Duration {
|
||||
let new = duration.saturating_mul(2);
|
||||
*duration = min(new, Duration::from_secs(1));
|
||||
duration
|
||||
}
|
||||
|
||||
/// wait for the lan-mouse socket to come online
|
||||
#[cfg(unix)]
|
||||
pub fn wait_for_service() -> Result<std::os::unix::net::UnixStream> {
|
||||
let socket_path = FrontendListener::socket_path()?;
|
||||
let mut duration = Duration::from_millis(1);
|
||||
loop {
|
||||
use std::os::unix::net::UnixStream;
|
||||
if let Ok(stream) = UnixStream::connect(&socket_path) {
|
||||
break Ok(stream);
|
||||
}
|
||||
// a signaling mechanism or inotify could be used to
|
||||
// improve this
|
||||
std::thread::sleep(*exponential_back_off(&mut duration));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn wait_for_service() -> Result<std::net::TcpStream> {
|
||||
let mut duration = Duration::from_millis(1);
|
||||
loop {
|
||||
use std::net::TcpStream;
|
||||
if let Ok(stream) = TcpStream::connect("127.0.0.1:5252") {
|
||||
break Ok(stream);
|
||||
}
|
||||
std::thread::sleep(*exponential_back_off(&mut duration));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum FrontendRequest {
|
||||
/// activate/deactivate client
|
||||
Activate(ClientHandle, bool),
|
||||
/// add a new client
|
||||
Create,
|
||||
/// change the listen port (recreate udp listener)
|
||||
ChangePort(u16),
|
||||
/// remove a client
|
||||
Delete(ClientHandle),
|
||||
/// request an enumeration of all clients
|
||||
Enumerate(),
|
||||
/// resolve dns
|
||||
ResolveDns(ClientHandle),
|
||||
/// update hostname
|
||||
UpdateHostname(ClientHandle, Option<String>),
|
||||
/// update port
|
||||
UpdatePort(ClientHandle, u16),
|
||||
/// update position
|
||||
UpdatePosition(ClientHandle, Position),
|
||||
/// update fix-ips
|
||||
UpdateFixIps(ClientHandle, Vec<IpAddr>),
|
||||
/// request the state of the given client
|
||||
GetState(ClientHandle),
|
||||
/// request reenabling input capture
|
||||
EnableCapture,
|
||||
/// request reenabling input emulation
|
||||
EnableEmulation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
|
||||
pub enum Status {
|
||||
#[default]
|
||||
Disabled,
|
||||
Enabled,
|
||||
}
|
||||
|
||||
impl From<Status> for bool {
|
||||
fn from(status: Status) -> Self {
|
||||
match status {
|
||||
Status::Enabled => true,
|
||||
Status::Disabled => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum FrontendEvent {
|
||||
/// a client was created
|
||||
Created(ClientHandle, ClientConfig, ClientState),
|
||||
/// no such client
|
||||
NoSuchClient(ClientHandle),
|
||||
/// state changed
|
||||
State(ClientHandle, ClientConfig, ClientState),
|
||||
/// the client was deleted
|
||||
Deleted(ClientHandle),
|
||||
/// new port, reason of failure (if failed)
|
||||
PortChanged(u16, Option<String>),
|
||||
/// list of all clients, used for initial state synchronization
|
||||
Enumerate(Vec<(ClientHandle, ClientConfig, ClientState)>),
|
||||
/// an error occured
|
||||
Error(String),
|
||||
/// capture status
|
||||
CaptureStatus(Status),
|
||||
/// emulation status
|
||||
EmulationStatus(Status),
|
||||
}
|
||||
|
||||
pub struct FrontendListener {
|
||||
#[cfg(windows)]
|
||||
listener: TcpListener,
|
||||
#[cfg(unix)]
|
||||
listener: UnixListener,
|
||||
#[cfg(unix)]
|
||||
socket_path: PathBuf,
|
||||
#[cfg(unix)]
|
||||
tx_streams: Vec<WriteHalf<UnixStream>>,
|
||||
#[cfg(windows)]
|
||||
tx_streams: Vec<WriteHalf<TcpStream>>,
|
||||
}
|
||||
|
||||
impl FrontendListener {
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
pub fn socket_path() -> Result<PathBuf> {
|
||||
let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") {
|
||||
Ok(d) => d,
|
||||
Err(e) => return Err(anyhow!("could not find XDG_RUNTIME_DIR: {e}")),
|
||||
};
|
||||
let xdg_runtime_dir = Path::new(xdg_runtime_dir.as_str());
|
||||
Ok(xdg_runtime_dir.join("lan-mouse-socket.sock"))
|
||||
}
|
||||
|
||||
#[cfg(all(unix, target_os = "macos"))]
|
||||
pub fn socket_path() -> Result<PathBuf> {
|
||||
let home = match env::var("HOME") {
|
||||
Ok(d) => d,
|
||||
Err(e) => return Err(anyhow!("could not find HOME: {e}")),
|
||||
};
|
||||
let home = Path::new(home.as_str());
|
||||
let path = home
|
||||
.join("Library")
|
||||
.join("Caches")
|
||||
.join("lan-mouse-socket.sock");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub async fn new() -> Option<Result<Self>> {
|
||||
#[cfg(unix)]
|
||||
let (socket_path, listener) = {
|
||||
let socket_path = match Self::socket_path() {
|
||||
Ok(path) => path,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
|
||||
log::debug!("remove socket: {:?}", socket_path);
|
||||
if socket_path.exists() {
|
||||
// try to connect to see if some other instance
|
||||
// of lan-mouse is already running
|
||||
match UnixStream::connect(&socket_path).await {
|
||||
// connected -> lan-mouse is already running
|
||||
Ok(_) => return None,
|
||||
// lan-mouse is not running but a socket was left behind
|
||||
Err(e) => {
|
||||
log::debug!("{socket_path:?}: {e} - removing left behind socket");
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
let listener = match UnixListener::bind(&socket_path) {
|
||||
Ok(ls) => ls,
|
||||
// some other lan-mouse instance has bound the socket in the meantime
|
||||
Err(e) if e.kind() == ErrorKind::AddrInUse => return None,
|
||||
Err(e) => return Some(Err(anyhow!("failed to bind lan-mouse-socket: {e}"))),
|
||||
};
|
||||
(socket_path, listener)
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
let listener = match TcpListener::bind("127.0.0.1:5252").await {
|
||||
Ok(ls) => ls,
|
||||
// some other lan-mouse instance has bound the socket in the meantime
|
||||
Err(e) if e.kind() == ErrorKind::AddrInUse => return None,
|
||||
Err(e) => return Some(Err(anyhow!("failed to bind lan-mouse-socket: {e}"))),
|
||||
};
|
||||
|
||||
let adapter = Self {
|
||||
listener,
|
||||
#[cfg(unix)]
|
||||
socket_path,
|
||||
tx_streams: vec![],
|
||||
};
|
||||
|
||||
Some(Ok(adapter))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub async fn accept(&mut self) -> Result<ReadHalf<UnixStream>> {
|
||||
let stream = self.listener.accept().await?.0;
|
||||
let (rx, tx) = tokio::io::split(stream);
|
||||
self.tx_streams.push(tx);
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub async fn accept(&mut self) -> Result<ReadHalf<TcpStream>> {
|
||||
let stream = self.listener.accept().await?.0;
|
||||
let (rx, tx) = tokio::io::split(stream);
|
||||
self.tx_streams.push(tx);
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
pub(crate) async fn broadcast(&mut self, notify: FrontendEvent) {
|
||||
// encode event
|
||||
let json = serde_json::to_string(¬ify).unwrap();
|
||||
let payload = json.as_bytes();
|
||||
let len = payload.len().to_be_bytes();
|
||||
log::debug!("broadcasting event to streams: {json}");
|
||||
let mut keep = vec![];
|
||||
// TODO do simultaneously
|
||||
for tx in self.tx_streams.iter_mut() {
|
||||
// write len + payload
|
||||
if tx.write(&len).await.is_err() {
|
||||
keep.push(false);
|
||||
continue;
|
||||
}
|
||||
if tx.write(payload).await.is_err() {
|
||||
keep.push(false);
|
||||
continue;
|
||||
}
|
||||
keep.push(true);
|
||||
}
|
||||
|
||||
// could not find a better solution because async
|
||||
let mut keep = keep.into_iter();
|
||||
self.tx_streams.retain(|_| keep.next().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Drop for FrontendListener {
|
||||
fn drop(&mut self) {
|
||||
log::debug!("remove socket: {:?}", self.socket_path);
|
||||
let _ = std::fs::remove_file(&self.socket_path);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub async fn wait_for_request(stream: &mut ReadHalf<UnixStream>) -> Result<FrontendRequest> {
|
||||
let len = stream.read_u64().await?;
|
||||
assert!(len <= 256);
|
||||
let mut buf = [0u8; 256];
|
||||
stream.read_exact(&mut buf[..len as usize]).await?;
|
||||
Ok(serde_json::from_slice(&buf[..len as usize])?)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub async fn wait_for_request(stream: &mut ReadHalf<TcpStream>) -> Result<FrontendRequest> {
|
||||
let len = stream.read_u64().await?;
|
||||
let mut buf = [0u8; 256];
|
||||
stream.read_exact(&mut buf[..len as usize]).await?;
|
||||
Ok(serde_json::from_slice(&buf[..len as usize])?)
|
||||
}
|
||||
@@ -5,4 +5,3 @@ pub mod server;
|
||||
|
||||
pub mod capture_test;
|
||||
pub mod emulation_test;
|
||||
pub mod frontend;
|
||||
|
||||
60
src/main.rs
60
src/main.rs
@@ -1,9 +1,16 @@
|
||||
use anyhow::Result;
|
||||
use std::process::{self, Child, Command};
|
||||
|
||||
use env_logger::Env;
|
||||
use lan_mouse::{capture_test, config::Config, emulation_test, frontend, server::Server};
|
||||
|
||||
use lan_mouse::{
|
||||
capture_test,
|
||||
config::{Config, ConfigError, Frontend},
|
||||
emulation_test,
|
||||
server::{Server, ServiceError},
|
||||
};
|
||||
use lan_mouse_ipc::IpcError;
|
||||
use std::{
|
||||
io,
|
||||
process::{self, Child, Command},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tokio::task::LocalSet;
|
||||
|
||||
pub fn main() {
|
||||
@@ -17,7 +24,7 @@ pub fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_service() -> Result<Child> {
|
||||
fn start_service() -> Result<Child, io::Error> {
|
||||
let child = Command::new(std::env::current_exe()?)
|
||||
.args(std::env::args().skip(1))
|
||||
.arg("--daemon")
|
||||
@@ -25,16 +32,28 @@ pub fn start_service() -> Result<Child> {
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
#[derive(Debug, Error)]
|
||||
enum LanMouseError {
|
||||
#[error(transparent)]
|
||||
Service(#[from] ServiceError),
|
||||
#[error(transparent)]
|
||||
IpcError(#[from] IpcError),
|
||||
#[error(transparent)]
|
||||
Config(#[from] ConfigError),
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
fn run() -> Result<(), LanMouseError> {
|
||||
// parse config file + cli args
|
||||
let config = Config::new()?;
|
||||
log::debug!("{config:?}");
|
||||
log::info!("release bind: {:?}", config.release_bind);
|
||||
|
||||
if config.test_capture {
|
||||
capture_test::run()?;
|
||||
capture_test::run().unwrap();
|
||||
} else if config.test_emulation {
|
||||
emulation_test::run()?;
|
||||
emulation_test::run().unwrap();
|
||||
} else if config.daemon {
|
||||
// if daemon is specified we run the service
|
||||
run_service(config)?;
|
||||
@@ -42,7 +61,7 @@ pub fn run() -> Result<()> {
|
||||
// otherwise start the service as a child process and
|
||||
// run a frontend
|
||||
let mut service = start_service()?;
|
||||
frontend::run_frontend(&config)?;
|
||||
run_frontend(&config)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// on unix we give the service a chance to terminate gracefully
|
||||
@@ -55,10 +74,10 @@ pub fn run() -> Result<()> {
|
||||
service.kill()?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_service(config: Config) -> Result<()> {
|
||||
fn run_service(config: Config) -> Result<(), ServiceError> {
|
||||
// create single threaded tokio runtime
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
@@ -74,7 +93,22 @@ fn run_service(config: Config) -> Result<()> {
|
||||
server.run().await?;
|
||||
|
||||
log::debug!("service exiting");
|
||||
anyhow::Ok(())
|
||||
Result::<(), ServiceError>::Ok(())
|
||||
}))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_frontend(config: &Config) -> Result<(), IpcError> {
|
||||
match config.frontend {
|
||||
#[cfg(feature = "gtk")]
|
||||
Frontend::Gtk => {
|
||||
lan_mouse_gtk::run();
|
||||
}
|
||||
#[cfg(not(feature = "gtk"))]
|
||||
Frontend::Gtk => panic!("gtk frontend requested but feature not enabled!"),
|
||||
Frontend::Cli => {
|
||||
lan_mouse_cli::run()?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
117
src/server.rs
117
src/server.rs
@@ -1,30 +1,27 @@
|
||||
use capture_task::CaptureRequest;
|
||||
use emulation_task::EmulationRequest;
|
||||
use futures::StreamExt;
|
||||
use hickory_resolver::error::ResolveError;
|
||||
use local_channel::mpsc::{channel, Sender};
|
||||
use log;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::{HashSet, VecDeque},
|
||||
io::ErrorKind,
|
||||
io,
|
||||
net::{IpAddr, SocketAddr},
|
||||
rc::Rc,
|
||||
};
|
||||
use tokio::{io::ReadHalf, join, signal, sync::Notify, task::JoinHandle};
|
||||
use thiserror::Error;
|
||||
use tokio::{join, signal, sync::Notify};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
client::{ClientConfig, ClientHandle, ClientManager, ClientState, Position},
|
||||
config::Config,
|
||||
dns::DnsResolver,
|
||||
frontend::{self, FrontendEvent, FrontendListener, FrontendRequest, Status},
|
||||
use crate::{client::ClientManager, config::Config, dns::DnsResolver};
|
||||
|
||||
use lan_mouse_ipc::{
|
||||
AsyncFrontendListener, ClientConfig, ClientHandle, ClientState, FrontendEvent, FrontendRequest,
|
||||
ListenerCreationError, Position, Status,
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
#[cfg(windows)]
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
mod capture_task;
|
||||
mod emulation_task;
|
||||
mod network_task;
|
||||
@@ -41,6 +38,16 @@ enum State {
|
||||
AwaitAck,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ServiceError {
|
||||
#[error(transparent)]
|
||||
Dns(#[from] ResolveError),
|
||||
#[error(transparent)]
|
||||
Listen(#[from] ListenerCreationError),
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Server {
|
||||
active_client: Rc<Cell<Option<ClientHandle>>>,
|
||||
@@ -113,21 +120,21 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> anyhow::Result<()> {
|
||||
pub async fn run(&mut self) -> Result<(), ServiceError> {
|
||||
// create frontend communication adapter, exit if already running
|
||||
let mut frontend = match FrontendListener::new().await {
|
||||
Some(f) => f?,
|
||||
None => {
|
||||
let mut frontend = match AsyncFrontendListener::new().await {
|
||||
Ok(f) => f,
|
||||
Err(ListenerCreationError::AlreadyRunning) => {
|
||||
log::info!("service already running, exiting");
|
||||
return Ok(());
|
||||
}
|
||||
e => e?,
|
||||
};
|
||||
|
||||
let (capture_tx, capture_rx) = channel(); /* requests for input capture */
|
||||
let (emulation_tx, emulation_rx) = channel(); /* emulation requests */
|
||||
let (udp_recv_tx, udp_recv_rx) = channel(); /* udp receiver */
|
||||
let (udp_send_tx, udp_send_rx) = channel(); /* udp sender */
|
||||
let (request_tx, mut request_rx) = channel(); /* frontend requests */
|
||||
let (dns_tx, dns_rx) = channel(); /* dns requests */
|
||||
|
||||
// udp task
|
||||
@@ -158,22 +165,17 @@ impl Server {
|
||||
|
||||
log::info!("running service");
|
||||
|
||||
let mut join_handles = vec![];
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
stream = frontend.accept() => {
|
||||
match stream {
|
||||
Ok(s) => join_handles.push(handle_frontend_stream(self.notifies.cancel.clone(), s, request_tx.clone())),
|
||||
Err(e) => log::warn!("error accepting frontend connection: {e}"),
|
||||
request = frontend.next() => {
|
||||
let request = match request {
|
||||
Some(Ok(r)) => r,
|
||||
Some(Err(e)) => {
|
||||
log::error!("error receiving request: {e}");
|
||||
continue;
|
||||
}
|
||||
None => break,
|
||||
};
|
||||
self.enumerate();
|
||||
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status.get()));
|
||||
self.notify_frontend(FrontendEvent::CaptureStatus(self.capture_status.get()));
|
||||
self.notify_frontend(FrontendEvent::PortChanged(self.port.get(), None));
|
||||
}
|
||||
request = request_rx.recv() => {
|
||||
let request = request.expect("channel closed");
|
||||
log::debug!("received frontend request: {request:?}");
|
||||
self.handle_request(&capture_tx.clone(), &emulation_tx.clone(), request).await;
|
||||
log::debug!("handled frontend request");
|
||||
@@ -207,7 +209,6 @@ impl Server {
|
||||
log::info!("terminating service");
|
||||
|
||||
self.cancel();
|
||||
futures::future::join_all(join_handles).await;
|
||||
let _ = join!(capture, dns_task, emulation, network, ping);
|
||||
|
||||
Ok(())
|
||||
@@ -329,6 +330,12 @@ impl Server {
|
||||
self.update_pos(handle, capture, emulate, pos).await;
|
||||
}
|
||||
FrontendRequest::ResolveDns(handle) => self.request_dns(handle),
|
||||
FrontendRequest::Sync => {
|
||||
self.enumerate();
|
||||
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status.get()));
|
||||
self.notify_frontend(FrontendEvent::CaptureStatus(self.capture_status.get()));
|
||||
self.notify_frontend(FrontendEvent::PortChanged(self.port.get(), None));
|
||||
}
|
||||
};
|
||||
false
|
||||
}
|
||||
@@ -396,7 +403,7 @@ impl Server {
|
||||
};
|
||||
|
||||
/* notify emulation, capture and frontends */
|
||||
let _ = capture.send(CaptureRequest::Create(handle, pos.into()));
|
||||
let _ = capture.send(CaptureRequest::Create(handle, to_capture_pos(pos)));
|
||||
let _ = emulate.send(EmulationRequest::Create(handle));
|
||||
log::debug!("activating client {handle} done");
|
||||
}
|
||||
@@ -504,7 +511,7 @@ impl Server {
|
||||
let _ = capture.send(CaptureRequest::Destroy(handle));
|
||||
let _ = emulate.send(EmulationRequest::Destroy(handle));
|
||||
}
|
||||
let _ = capture.send(CaptureRequest::Create(handle, pos.into()));
|
||||
let _ = capture.send(CaptureRequest::Create(handle, to_capture_pos(pos)));
|
||||
let _ = emulate.send(EmulationRequest::Create(handle));
|
||||
}
|
||||
}
|
||||
@@ -567,41 +574,11 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen_frontend(
|
||||
request_tx: Sender<FrontendRequest>,
|
||||
#[cfg(unix)] mut stream: ReadHalf<UnixStream>,
|
||||
#[cfg(windows)] mut stream: ReadHalf<TcpStream>,
|
||||
) {
|
||||
use std::io;
|
||||
loop {
|
||||
let request = frontend::wait_for_request(&mut stream).await;
|
||||
match request {
|
||||
Ok(request) => {
|
||||
let _ = request_tx.send(request);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
fn to_capture_pos(pos: Position) -> input_capture::Position {
|
||||
match pos {
|
||||
Position::Left => input_capture::Position::Left,
|
||||
Position::Right => input_capture::Position::Right,
|
||||
Position::Top => input_capture::Position::Top,
|
||||
Position::Bottom => input_capture::Position::Bottom,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_frontend_stream(
|
||||
cancel: CancellationToken,
|
||||
#[cfg(unix)] stream: ReadHalf<UnixStream>,
|
||||
#[cfg(windows)] stream: ReadHalf<TcpStream>,
|
||||
request_tx: Sender<FrontendRequest>,
|
||||
) -> JoinHandle<()> {
|
||||
tokio::task::spawn_local(async move {
|
||||
tokio::select! {
|
||||
_ = listen_frontend(request_tx, stream) => {},
|
||||
_ = cancel.cancelled() => {},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ use input_capture::{
|
||||
self, CaptureError, CaptureEvent, CaptureHandle, InputCapture, InputCaptureError, Position,
|
||||
};
|
||||
|
||||
use crate::{client::ClientHandle, frontend::Status, server::State};
|
||||
use crate::server::State;
|
||||
use lan_mouse_ipc::{ClientHandle, Status};
|
||||
|
||||
use super::Server;
|
||||
|
||||
@@ -85,7 +86,7 @@ async fn do_capture(
|
||||
)
|
||||
});
|
||||
for (handle, pos) in clients {
|
||||
capture.create(handle, pos.into()).await?;
|
||||
capture.create(handle, to_capture_pos(pos)).await?;
|
||||
}
|
||||
|
||||
loop {
|
||||
@@ -194,3 +195,12 @@ fn spawn_hook_command(server: &Server, handle: ClientHandle) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn to_capture_pos(pos: lan_mouse_ipc::Position) -> input_capture::Position {
|
||||
match pos {
|
||||
lan_mouse_ipc::Position::Left => input_capture::Position::Left,
|
||||
lan_mouse_ipc::Position::Right => input_capture::Position::Right,
|
||||
lan_mouse_ipc::Position::Top => input_capture::Position::Top,
|
||||
lan_mouse_ipc::Position::Bottom => input_capture::Position::Bottom,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,11 @@ use std::net::SocketAddr;
|
||||
use lan_mouse_proto::ProtoEvent;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::{
|
||||
client::{ClientHandle, ClientManager},
|
||||
frontend::Status,
|
||||
server::State,
|
||||
};
|
||||
use lan_mouse_ipc::ClientHandle;
|
||||
|
||||
use crate::{client::ClientManager, server::State};
|
||||
use input_emulation::{self, EmulationError, EmulationHandle, InputEmulation, InputEmulationError};
|
||||
use lan_mouse_ipc::Status;
|
||||
|
||||
use super::{network_task::NetworkError, Server};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use lan_mouse_proto::ProtoEvent;
|
||||
use local_channel::mpsc::Sender;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::client::ClientHandle;
|
||||
use lan_mouse_ipc::ClientHandle;
|
||||
|
||||
use super::{capture_task::CaptureRequest, emulation_task::EmulationRequest, Server, State};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user