diff --git a/Cargo.lock b/Cargo.lock index 99060e8..9352572 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -260,6 +260,7 @@ dependencies = [ "anyhow", "crossbeam", "log", + "named_pipe", "serde", "serde_json", "thiserror", @@ -393,6 +394,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "named_pipe" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9c443cce91fc3e12f017290db75dde490d685cdaaf508d7159d7cf41f0eb2b" +dependencies = [ + "winapi", +] + [[package]] name = "notify-rust" version = "4.2.2" diff --git a/espanso-detect/src/lib.rs b/espanso-detect/src/lib.rs index 3915ae9..11e0927 100644 --- a/espanso-detect/src/lib.rs +++ b/espanso-detect/src/lib.rs @@ -43,7 +43,7 @@ pub type SourceCallback = Box; pub trait Source { fn initialize(&mut self) -> Result<()>; - fn eventloop(&self, event_callback: SourceCallback); + fn eventloop(&self, event_callback: SourceCallback) -> Result<()>; } #[allow(dead_code)] diff --git a/espanso-detect/src/win32/mod.rs b/espanso-detect/src/win32/mod.rs index bb0c01a..48ddd18 100644 --- a/espanso-detect/src/win32/mod.rs +++ b/espanso-detect/src/win32/mod.rs @@ -26,7 +26,7 @@ use widestring::U16CStr; use anyhow::Result; use thiserror::Error; -use crate::event::Status::*; +use crate::{Source, SourceCallback, event::Status::*}; use crate::event::Variant::*; use crate::event::{InputEvent, Key, KeyboardEvent, Variant}; use crate::event::{Key::*, MouseButton, MouseEvent}; @@ -109,13 +109,14 @@ impl Source for Win32Source { Ok(()) } - fn eventloop(&self, event_callback: SourceCallback) { + fn eventloop(&self, event_callback: SourceCallback) -> Result<()> { if self.handle.is_null() { panic!("Attempt to start Win32Source eventloop without initialization"); } if self.callback.fill(event_callback).is_err() { - panic!("Unable to set Win32Source event callback"); + error!("Unable to set Win32Source event callback"); + return Err(Win32SourceError::Unknown().into()) } extern "C" fn callback(_self: *mut Win32Source, event: RawInputEvent) { @@ -132,8 +133,11 @@ impl Source for Win32Source { let error_code = unsafe { detect_eventloop(self.handle, callback) }; if error_code <= 0 { - panic!("Win32Source eventloop returned a negative error code"); + error!("Win32Source eventloop returned a negative error code"); + return Err(Win32SourceError::Unknown().into()) } + + Ok(()) } } diff --git a/espanso-inject/src/lib.rs b/espanso-inject/src/lib.rs index a11b22e..508e12e 100644 --- a/espanso-inject/src/lib.rs +++ b/espanso-inject/src/lib.rs @@ -122,13 +122,13 @@ impl Default for InjectorCreationOptions { } #[cfg(target_os = "windows")] -pub fn get_injector(_options: InjectorOptions) -> Result> { +pub fn get_injector(_options: InjectorCreationOptions) -> Result> { info!("using Win32Injector"); Ok(Box::new(win32::Win32Injector::new())) } #[cfg(target_os = "macos")] -pub fn get_injector(_options: InjectorOptions) -> Result> { +pub fn get_injector(_options: InjectorCreationOptions) -> Result> { info!("using MacInjector"); Ok(Box::new(mac::MacInjector::new())) } diff --git a/espanso-inject/src/win32/mod.rs b/espanso-inject/src/win32/mod.rs index 6e04565..d6b0772 100644 --- a/espanso-inject/src/win32/mod.rs +++ b/espanso-inject/src/win32/mod.rs @@ -25,7 +25,7 @@ use raw_keys::convert_key_to_vkey; use anyhow::Result; use thiserror::Error; -use crate::{keys, Injector}; +use crate::{InjectionOptions, Injector, keys}; #[allow(improper_ctypes)] #[link(name = "espansoinject", kind = "static")] @@ -60,7 +60,7 @@ impl Win32Injector { } impl Injector for Win32Injector { - fn send_string(&self, string: &str) -> Result<()> { + fn send_string(&self, string: &str, _: InjectionOptions) -> Result<()> { let wide_string = widestring::WideCString::from_str(string)?; unsafe { inject_string(wide_string.as_ptr()); @@ -68,32 +68,32 @@ impl Injector for Win32Injector { Ok(()) } - fn send_keys(&self, keys: &[keys::Key], delay: i32) -> Result<()> { + fn send_keys(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> { let virtual_keys = Self::convert_to_vk_array(keys)?; - if delay == 0 { + if options.delay == 0 { unsafe { inject_separate_vkeys(virtual_keys.as_ptr(), virtual_keys.len() as i32); } } else { unsafe { - inject_separate_vkeys_with_delay(virtual_keys.as_ptr(), virtual_keys.len() as i32, delay); + inject_separate_vkeys_with_delay(virtual_keys.as_ptr(), virtual_keys.len() as i32, options.delay); } } Ok(()) } - fn send_key_combination(&self, keys: &[keys::Key], delay: i32) -> Result<()> { + fn send_key_combination(&self, keys: &[keys::Key], options: InjectionOptions) -> Result<()> { let virtual_keys = Self::convert_to_vk_array(keys)?; - if delay == 0 { + if options.delay == 0 { unsafe { inject_vkeys_combination(virtual_keys.as_ptr(), virtual_keys.len() as i32); } } else { unsafe { - inject_vkeys_combination_with_delay(virtual_keys.as_ptr(), virtual_keys.len() as i32, delay); + inject_vkeys_combination_with_delay(virtual_keys.as_ptr(), virtual_keys.len() as i32, options.delay); } } diff --git a/espanso-ipc/Cargo.toml b/espanso-ipc/Cargo.toml index 13d8be3..9103412 100644 --- a/espanso-ipc/Cargo.toml +++ b/espanso-ipc/Cargo.toml @@ -13,5 +13,4 @@ serde_json = "1.0.62" crossbeam = "0.8.0" [target.'cfg(windows)'.dependencies] - -[target.'cfg(unix)'.dependencies] \ No newline at end of file +named_pipe = "0.4.1" \ No newline at end of file diff --git a/espanso-ipc/src/lib.rs b/espanso-ipc/src/lib.rs index caafddf..2d8099a 100644 --- a/espanso-ipc/src/lib.rs +++ b/espanso-ipc/src/lib.rs @@ -51,6 +51,19 @@ pub fn client(id: &str, parent_dir: &Path) -> Result(id: &str, _: &Path) -> Result<(impl IPCServer, Receiver)> { + let (sender, receiver) = unbounded(); + let server = windows::WinIPCServer::new(id, sender)?; + Ok((server, receiver)) +} + +#[cfg(target_os = "windows")] +pub fn client(id: &str, _: &Path) -> Result> { + let client = windows::WinIPCClient::new(id)?; + Ok(client) +} + #[derive(Error, Debug)] pub enum IPCServerError { #[error("stream ended")] @@ -78,6 +91,7 @@ mod tests { server.accept_one().unwrap(); }); + std::thread::sleep(std::time::Duration::from_secs(1)); let client = client::("testespansoipc", &std::env::temp_dir()).unwrap(); client.send(Event::Foo("hello".to_string())).unwrap(); diff --git a/espanso-ipc/src/windows.rs b/espanso-ipc/src/windows.rs index 9d9eec3..78bb699 100644 --- a/espanso-ipc/src/windows.rs +++ b/espanso-ipc/src/windows.rs @@ -16,3 +16,104 @@ * 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 named_pipe::{PipeClient, PipeOptions}; +use serde::{de::DeserializeOwned, Serialize}; +use std::{ + io::{BufReader, Read, Write}, +}; + +use crate::{IPCClient, IPCServer, IPCServerError}; + +const CLIENT_TIMEOUT: u32 = 2000; + +pub struct WinIPCServer { + options: PipeOptions, + sender: Sender, +} + +impl WinIPCServer { + pub fn new(id: &str, sender: Sender) -> Result { + let pipe_name = format!("\\\\.\\pipe\\{}", id); + + let options = PipeOptions::new(&pipe_name); + + info!( + "binded to named pipe: {}", + pipe_name + ); + + Ok(Self { options, sender }) + } +} + +impl IPCServer for WinIPCServer { + fn run(&self) -> anyhow::Result<()> { + loop { + self.accept_one()?; + } + } + + fn accept_one(&self) -> Result<()> { + let server = self.options.single()?; + let connection = server.wait(); + + 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 WinIPCClient { + pipe_name: String, +} + +impl WinIPCClient { + pub fn new(id: &str) -> Result { + let pipe_name = format!("\\\\.\\pipe\\{}", id); + Ok(Self { pipe_name }) + } +} + +impl IPCClient for WinIPCClient { + fn send(&self, event: Event) -> Result<()> { + let mut stream = PipeClient::connect_ms(&self.pipe_name, CLIENT_TIMEOUT)?; + + let json_event = serde_json::to_string(&event)?; + stream.write_all(json_event.as_bytes())?; + + Ok(()) + } +} + diff --git a/espanso-ui/src/lib.rs b/espanso-ui/src/lib.rs index 3b7901f..8f5b1d0 100644 --- a/espanso-ui/src/lib.rs +++ b/espanso-ui/src/lib.rs @@ -1,5 +1,6 @@ use icons::TrayIcon; use anyhow::Result; +use thiserror::Error; pub mod event; pub mod icons; @@ -22,8 +23,8 @@ pub trait UIRemote { pub type UIEventCallback = Box; pub trait UIEventLoop { - fn initialize(&mut self); - fn run(&self, event_callback: UIEventCallback); + fn initialize(&mut self) -> Result<()>; + fn run(&self, event_callback: UIEventCallback) -> Result<()>; } pub struct UIOptions { @@ -43,22 +44,33 @@ impl Default for UIOptions { } #[cfg(target_os = "windows")] -pub fn create_ui(_options: UIOptions) -> Result> { - // TODO: refactor - Ok(Box::new(win32::Win32Injector::new())) +pub fn create_ui(options: UIOptions) -> Result<(Box, Box)> { + let (remote, eventloop) = win32::create(win32::Win32UIOptions { + show_icon: options.show_icon, + icon_paths: &options.icon_paths, + notification_icon_path: options.notification_icon_path.ok_or_else(|| UIError::MissingOption("notification icon".to_string()))?, + })?; + Ok((Box::new(remote), Box::new(eventloop))) } #[cfg(target_os = "macos")] -pub fn create_ui(_options: UIOptions) -> Result> { - // TODO: refactor - Ok(Box::new(mac::MacInjector::new())) -} - -#[cfg(target_os = "linux")] pub fn create_ui(options: UIOptions) -> Result<(Box, Box)> { - // TODO: here we could avoid panicking and instead return a good result let (remote, eventloop) = linux::create(linux::LinuxUIOptions { notification_icon_path: options.notification_icon_path.expect("missing notification icon path") }); Ok((Box::new(remote), Box::new(eventloop))) +} + +#[cfg(target_os = "linux")] +pub fn create_ui(options: UIOptions) -> Result<(Box, Box)> { + let (remote, eventloop) = linux::create(linux::LinuxUIOptions { + notification_icon_path: options.notification_icon_path.ok_or(UIError::MissingOption("notification icon".to_string()))?, + }); + Ok((Box::new(remote), Box::new(eventloop))) +} + +#[derive(Error, Debug)] +pub enum UIError { + #[error("missing required option for ui: `{0}`")] + MissingOption(String), } \ No newline at end of file diff --git a/espanso-ui/src/win32/mod.rs b/espanso-ui/src/win32/mod.rs index 8f9e0d9..d802427 100644 --- a/espanso-ui/src/win32/mod.rs +++ b/espanso-ui/src/win32/mod.rs @@ -32,8 +32,10 @@ use std::{ use lazycell::LazyCell; use log::{error, trace}; use widestring::WideCString; +use anyhow::Result; +use thiserror::Error; -use crate::{event::UIEvent, icons::TrayIcon, menu::Menu}; +use crate::{UIEventCallback, UIEventLoop, UIRemote, event::UIEvent, icons::TrayIcon, menu::Menu}; // IMPORTANT: if you change these, also edit the native.h file. const MAX_FILE_PATH: usize = 260; @@ -83,7 +85,7 @@ pub struct Win32UIOptions<'a> { pub notification_icon_path: String, } -pub fn create(options: Win32UIOptions) -> (Win32Remote, Win32EventLoop) { +pub fn create(options: Win32UIOptions) -> Result<(Win32Remote, Win32EventLoop)> { let handle: Arc> = Arc::new(AtomicPtr::new(std::ptr::null_mut())); // Validate icons @@ -107,7 +109,7 @@ pub fn create(options: Win32UIOptions) -> (Win32Remote, Win32EventLoop) { ); let remote = Win32Remote::new(handle, icon_indexes); - (remote, eventloop) + Ok((remote, eventloop)) } pub struct Win32EventLoop { @@ -118,7 +120,7 @@ pub struct Win32EventLoop { notification_icon_path: String, // Internal - _event_callback: LazyCell, + _event_callback: LazyCell, _init_thread_id: LazyCell, } @@ -138,11 +140,14 @@ impl Win32EventLoop { _init_thread_id: LazyCell::new(), } } +} - pub fn initialize(&mut self) { +impl UIEventLoop for Win32EventLoop { + fn initialize(&mut self) -> Result<()> { let window_handle = self.handle.load(Ordering::Acquire); if !window_handle.is_null() { - panic!("Attempt to initialize Win32EventLoop on non-null window handle"); + error!("Attempt to initialize Win32EventLoop on non-null window handle"); + return Err(Win32UIError::InvalidHandle().into()); } // Convert the icon paths to the raw representation @@ -150,15 +155,13 @@ impl Win32EventLoop { [[0; MAX_FILE_PATH]; MAX_ICON_COUNT]; for (i, icon_path) in icon_paths.iter_mut().enumerate().take(self.icons.len()) { let wide_path = - WideCString::from_str(&self.icons[i]).expect("Error while converting icon to wide string"); + WideCString::from_str(&self.icons[i])?; let len = min(wide_path.len(), MAX_FILE_PATH - 1); icon_path[0..len].clone_from_slice(&wide_path.as_slice()[..len]); - // TODO: test overflow, correct case } let wide_notification_icon_path = - widestring::WideCString::from_str(&self.notification_icon_path) - .expect("Error while converting notification icon to wide string"); + widestring::WideCString::from_str(&self.notification_icon_path)?; let mut wide_notification_icon_path_buffer: [u16; MAX_FILE_PATH] = [0; MAX_FILE_PATH]; wide_notification_icon_path_buffer[..wide_notification_icon_path.as_slice().len()] .clone_from_slice(wide_notification_icon_path.as_slice()); @@ -174,11 +177,11 @@ impl Win32EventLoop { let handle = unsafe { ui_initialize(self as *const Win32EventLoop, options, &mut error_code) }; if handle.is_null() { - match error_code { - -1 => panic!("Unable to initialize Win32EventLoop, error registering window class"), - -2 => panic!("Unable to initialize Win32EventLoop, error creating window"), - -3 => panic!("Unable to initialize Win32EventLoop, initializing notifications"), - _ => panic!("Unable to initialize Win32EventLoop, unknown error"), + return match error_code { + -1 => Err(Win32UIError::EventLoopInitError("Unable to initialize Win32EventLoop, error registering window class".to_string()).into()), + -2 => Err(Win32UIError::EventLoopInitError("Unable to initialize Win32EventLoop, error creating window".to_string()).into()), + -3 => Err(Win32UIError::EventLoopInitError("Unable to initialize Win32EventLoop, initializing notifications".to_string()).into()), + _ => Err(Win32UIError::EventLoopInitError("Unable to initialize Win32EventLoop, unknown error".to_string()).into()), } } @@ -189,25 +192,27 @@ impl Win32EventLoop { ._init_thread_id .fill(std::thread::current().id()) .expect("Unable to set initialization thread id"); + + Ok(()) } - pub fn run(&self, event_callback: Win32UIEventCallback) { + fn run(&self, event_callback: UIEventCallback) -> Result<()> { // Make sure the run() method is called in the same thread as initialize() if let Some(init_id) = self._init_thread_id.borrow() { if init_id != &std::thread::current().id() { panic!("Win32EventLoop run() and initialize() methods should be called in the same thread"); - // TODO: test } } let window_handle = self.handle.load(Ordering::Acquire); if window_handle.is_null() { - panic!("Attempt to run Win32EventLoop on a null window handle"); - // TODO: test + error!("Attempt to run Win32EventLoop on a null window handle"); + return Err(Win32UIError::InvalidHandle().into()) } if self._event_callback.fill(event_callback).is_err() { - panic!("Unable to set Win32EventLoop callback"); + error!("Unable to set Win32EventLoop callback"); + return Err(Win32UIError::InternalError().into()) } extern "C" fn callback(_self: *mut Win32EventLoop, event: RawUIEvent) { @@ -224,8 +229,11 @@ impl Win32EventLoop { let error_code = unsafe { ui_eventloop(window_handle, callback) }; if error_code <= 0 { - panic!("Win32EventLoop exited with <= 0 code") + error!("Win32EventLoop exited with <= 0 code"); + return Err(Win32UIError::InternalError().into()) } + + Ok(()) } } @@ -262,8 +270,10 @@ impl Win32Remote { icon_indexes, } } +} - pub fn update_tray_icon(&self, icon: TrayIcon) { +impl UIRemote for Win32Remote { + fn update_tray_icon(&self, icon: TrayIcon) { let handle = self.handle.load(Ordering::Acquire); if handle.is_null() { error!("Unable to update tray icon, pointer is null"); @@ -277,7 +287,7 @@ impl Win32Remote { } } - pub fn show_notification(&self, message: &str) { + fn show_notification(&self, message: &str) { let handle = self.handle.load(Ordering::Acquire); if handle.is_null() { error!("Unable to show notification, pointer is null"); @@ -296,7 +306,7 @@ impl Win32Remote { } } - pub fn show_context_menu(&self, menu: &Menu) { + fn show_context_menu(&self, menu: &Menu) { let handle = self.handle.load(Ordering::Acquire); if handle.is_null() { error!("Unable to show context menu, pointer is null"); @@ -338,6 +348,18 @@ impl From for Option { } } +#[derive(Error, Debug)] +pub enum Win32UIError { + #[error("invalid handle")] + InvalidHandle(), + + #[error("event loop initialization failed: `{0}`")] + EventLoopInitError(String), + + #[error("internal error")] + InternalError(), +} + #[cfg(test)] mod tests { use super::*; diff --git a/espanso/src/main.rs b/espanso/src/main.rs index dd9efa7..37157bf 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -53,7 +53,7 @@ fn main() { ..Default::default() }).unwrap(); - eventloop.initialize(); + eventloop.initialize().unwrap(); let handle = std::thread::spawn(move || { let injector = get_injector(Default::default()).unwrap(); @@ -73,7 +73,7 @@ fn main() { } } } - })); + })).unwrap(); }); eventloop.run(Box::new(move |event| {