From ee611c3a038da4d05f395e711a67e78ee5e05f6a Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Mon, 15 Feb 2021 21:25:38 +0100 Subject: [PATCH] First unix ipc implementation --- Cargo.lock | 90 ++++++++++++++++++++++++++- Cargo.toml | 1 + espanso-ipc/Cargo.toml | 17 ++++++ espanso-ipc/src/lib.rs | 95 +++++++++++++++++++++++++++++ espanso-ipc/src/unix.rs | 122 +++++++++++++++++++++++++++++++++++++ espanso-ipc/src/windows.rs | 18 ++++++ 6 files changed, 341 insertions(+), 2 deletions(-) create mode 100644 espanso-ipc/Cargo.toml create mode 100644 espanso-ipc/src/lib.rs create mode 100644 espanso-ipc/src/unix.rs create mode 100644 espanso-ipc/src/windows.rs diff --git a/Cargo.lock b/Cargo.lock index 89bf3e0..99060e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,12 +87,77 @@ dependencies = [ "winapi", ] +[[package]] +name = "const_fn" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6" + [[package]] name = "constant_time_eq" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "crossbeam" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd01a6eb3daaafa260f6fc94c3a6c36390abc2080e38e3e34ced87393fb77d80" +dependencies = [ + "cfg-if", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1aaa739f95311c2c7887a76863f500026092fb1dce0161dab577e559ef3569d" +dependencies = [ + "cfg-if", + "const_fn", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.1" @@ -188,6 +253,18 @@ dependencies = [ "widestring", ] +[[package]] +name = "espanso-ipc" +version = "0.1.0" +dependencies = [ + "anyhow", + "crossbeam", + "log", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "espanso-ui" version = "0.1.0" @@ -307,6 +384,15 @@ version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +[[package]] +name = "memoffset" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87" +dependencies = [ + "autocfg", +] + [[package]] name = "notify-rust" version = "4.2.2" @@ -483,9 +569,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" +checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486" dependencies = [ "itoa", "ryu", diff --git a/Cargo.toml b/Cargo.toml index 7eeca9f..8484773 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,5 @@ members = [ "espanso-detect", "espanso-ui", "espanso-inject", + "espanso-ipc", ] \ No newline at end of file diff --git a/espanso-ipc/Cargo.toml b/espanso-ipc/Cargo.toml new file mode 100644 index 0000000..13d8be3 --- /dev/null +++ b/espanso-ipc/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "espanso-ipc" +version = "0.1.0" +authors = ["Federico Terzi "] +edition = "2018" + +[dependencies] +log = "0.4.14" +anyhow = "1.0.38" +thiserror = "1.0.23" +serde = { version = "1.0.123", features = ["derive"] } +serde_json = "1.0.62" +crossbeam = "0.8.0" + +[target.'cfg(windows)'.dependencies] + +[target.'cfg(unix)'.dependencies] \ No newline at end of file diff --git a/espanso-ipc/src/lib.rs b/espanso-ipc/src/lib.rs new file mode 100644 index 0000000..caafddf --- /dev/null +++ b/espanso-ipc/src/lib.rs @@ -0,0 +1,95 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use std::path::Path; +use anyhow::Result; +use serde::{Serialize, de::DeserializeOwned}; +use thiserror::Error; +use crossbeam::channel::{Receiver, unbounded}; + +#[cfg(target_os = "windows")] +pub mod windows; + +#[cfg(not(target_os = "windows"))] +pub mod unix; + +pub trait IPCServer { + fn run(&self) -> Result<()>; + fn accept_one(&self) -> Result<()>; +} + +pub trait IPCClient { + fn send(&self, event: Event) -> Result<()>; +} + +#[cfg(not(target_os = "windows"))] +pub fn server(id: &str, parent_dir: &Path) -> Result<(impl IPCServer, Receiver)> { + let (sender, receiver) = unbounded(); + let server = unix::UnixIPCServer::new(id, parent_dir, sender)?; + Ok((server, receiver)) +} + +#[cfg(not(target_os = "windows"))] +pub fn client(id: &str, parent_dir: &Path) -> Result> { + let client = unix::UnixIPCClient::new(id, parent_dir)?; + Ok(client) +} + +#[derive(Error, Debug)] +pub enum IPCServerError { + #[error("stream ended")] + StreamEnded(#[from] std::io::Error), + + #[error("send failed")] + SendFailed(), +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::{Serialize, Deserialize}; + + #[derive(Serialize, Deserialize)] + enum Event { + Bar, + Foo(String), + } + + #[test] + fn ipc_works_correctly() { + let (server, receiver) = server::("testespansoipc", &std::env::temp_dir()).unwrap(); + let server_handle = std::thread::spawn(move || { + server.accept_one().unwrap(); + }); + + let client = client::("testespansoipc", &std::env::temp_dir()).unwrap(); + client.send(Event::Foo("hello".to_string())).unwrap(); + + let event = receiver.recv().unwrap(); + assert!(matches!(event, Event::Foo(x) if x == "hello")); + + server_handle.join().unwrap(); + } + + #[test] + fn ipc_client_fails_to_send() { + let client = client::("testespansoipc", &std::env::temp_dir()).unwrap(); + assert!(client.send(Event::Foo("hello".to_string())).is_err()); + } +} \ No newline at end of file diff --git a/espanso-ipc/src/unix.rs b/espanso-ipc/src/unix.rs new file mode 100644 index 0000000..caa7455 --- /dev/null +++ b/espanso-ipc/src/unix.rs @@ -0,0 +1,122 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use anyhow::Result; +use crossbeam::channel::Sender; +use log::{error, info}; +use serde::{de::DeserializeOwned, Serialize}; +use std::{ + io::{BufReader, Read, Write}, + os::unix::net::{UnixListener, UnixStream}, + path::{Path, PathBuf}, +}; + +use crate::{IPCClient, IPCServer, IPCServerError}; + +pub struct UnixIPCServer { + listener: UnixListener, + sender: Sender, +} + +impl UnixIPCServer { + pub fn new(id: &str, parent_dir: &Path, sender: Sender) -> Result { + let socket_path = parent_dir.join(format!("{}.sock", id)); + + // Remove previous Unix socket + if socket_path.exists() { + std::fs::remove_file(&socket_path)?; + } + + let listener = UnixListener::bind(&socket_path)?; + + info!( + "binded to IPC unix socket: {}", + socket_path.to_string_lossy() + ); + + Ok(Self { listener, sender }) + } +} + +impl IPCServer for UnixIPCServer { + fn run(&self) -> anyhow::Result<()> { + loop { + self.accept_one()?; + } + } + + fn accept_one(&self) -> Result<()> { + let connection = self.listener.accept(); + + match connection { + Ok((stream, _)) => { + let mut json_str = String::new(); + let mut buf_reader = BufReader::new(stream); + let result = buf_reader.read_to_string(&mut json_str); + + match result { + Ok(_) => { + let event: Result = serde_json::from_str(&json_str); + match event { + Ok(event) => { + if self.sender.send(event).is_err() { + return Err(IPCServerError::SendFailed().into()); + } + } + Err(error) => { + error!("received malformed event from ipc stream: {}", error); + } + } + } + Err(error) => { + error!("error reading ipc stream: {}", error); + } + } + } + Err(err) => { + return Err(IPCServerError::StreamEnded(err).into()); + } + }; + + Ok(()) + } +} + +pub struct UnixIPCClient { + socket_path: PathBuf, +} + +impl UnixIPCClient { + pub fn new(id: &str, parent_dir: &Path) -> Result { + let socket_path = parent_dir.join(format!("{}.sock", id)); + + Ok(Self { socket_path }) + } +} + +impl IPCClient for UnixIPCClient { + fn send(&self, event: Event) -> Result<()> { + let mut stream = UnixStream::connect(&self.socket_path)?; + + let json_event = serde_json::to_string(&event)?; + stream.write_all(json_event.as_bytes())?; + + Ok(()) + } +} diff --git a/espanso-ipc/src/windows.rs b/espanso-ipc/src/windows.rs new file mode 100644 index 0000000..9d9eec3 --- /dev/null +++ b/espanso-ipc/src/windows.rs @@ -0,0 +1,18 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */