diff --git a/build.rs b/build.rs index a415c03..b2b1f65 100644 --- a/build.rs +++ b/build.rs @@ -44,6 +44,7 @@ fn print_config() { println!("cargo:rustc-link-lib=dylib=c++"); println!("cargo:rustc-link-lib=static=macbridge"); println!("cargo:rustc-link-lib=framework=Cocoa"); + println!("cargo:rustc-link-lib=framework=IOKit"); } fn main() diff --git a/native/libmacbridge/bridge.h b/native/libmacbridge/bridge.h index dc74187..03bb6bf 100644 --- a/native/libmacbridge/bridge.h +++ b/native/libmacbridge/bridge.h @@ -158,6 +158,15 @@ int32_t set_clipboard(char * text); */ int32_t set_clipboard_image(char * path); +/* + * If a process is currently holding SecureInput, then return 1 and set the pid pointer to the corresponding PID. + */ +int32_t get_secure_input_process(int64_t *pid); + +/* + * Find the executable path corresponding to the given PID, return 0 if no process was found. + */ +int32_t get_path_from_pid(int64_t pid, char *buff, int buff_size); }; #endif //ESPANSO_BRIDGE_H diff --git a/native/libmacbridge/bridge.mm b/native/libmacbridge/bridge.mm index 564405b..1c59bd6 100644 --- a/native/libmacbridge/bridge.mm +++ b/native/libmacbridge/bridge.mm @@ -20,9 +20,11 @@ #include "bridge.h" #import +#include #include "AppDelegate.h" #include #include +#include extern "C" { } @@ -334,3 +336,47 @@ void open_settings_panel() { [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]]; } +// Taken (with a few modifications) from the MagicKeys project: https://github.com/zsszatmari/MagicKeys +int32_t get_secure_input_process(int64_t *pid) { + NSArray *consoleUsersArray; + io_service_t rootService; + int32_t result = 0; + + if ((rootService = IORegistryGetRootEntry(kIOMasterPortDefault)) != 0) + { + if ((consoleUsersArray = (NSArray *)IORegistryEntryCreateCFProperty((io_registry_entry_t)rootService, CFSTR("IOConsoleUsers"), kCFAllocatorDefault, 0)) != nil) + { + if ([consoleUsersArray isKindOfClass:[NSArray class]]) // Be careful - ensure this really is an array + { + for (NSDictionary *consoleUserDict in consoleUsersArray) { + NSNumber *secureInputPID; + + if ((secureInputPID = [consoleUserDict objectForKey:@"kCGSSessionSecureInputPID"]) != nil) + { + if ([secureInputPID isKindOfClass:[NSNumber class]]) + { + *pid = ((UInt64) [secureInputPID intValue]); + result = 1; + break; + } + } + } + } + + CFRelease((CFTypeRef)consoleUsersArray); + } + + IOObjectRelease((io_object_t) rootService); + } + + return result; +} + +int32_t get_path_from_pid(int64_t pid, char *buff, int buff_size) { + int res = proc_pidpath((pid_t) pid, buff, buff_size); + if ( res <= 0 ) { + return 0; + } else { + return 1; + } +} \ No newline at end of file diff --git a/src/bridge/macos.rs b/src/bridge/macos.rs index c698d35..989786b 100644 --- a/src/bridge/macos.rs +++ b/src/bridge/macos.rs @@ -39,6 +39,8 @@ extern { pub fn open_settings_panel(); pub fn get_active_app_bundle(buffer: *mut c_char, size: i32) -> i32; pub fn get_active_app_identifier(buffer: *mut c_char, size: i32) -> i32; + pub fn get_secure_input_process(pid:*mut i64) -> i32; + pub fn get_path_from_pid(pid:i64, buffer: *mut c_char, size: i32) -> i32; // Clipboard pub fn get_clipboard(buffer: *mut c_char, size: i32) -> i32; diff --git a/src/config/mod.rs b/src/config/mod.rs index 9244868..70f5645 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -64,6 +64,9 @@ fn default_enable_active() -> bool { true } fn default_backspace_limit() -> i32 { 3 } fn default_restore_clipboard_delay() -> i32 { 300 } fn default_exclude_default_entries() -> bool {false} +fn default_secure_input_watcher_enabled() -> bool {true} +fn default_secure_input_notification() -> bool {true} +fn default_secure_input_watcher_interval() -> i32 {5000} fn default_matches() -> Vec { Vec::new() } fn default_global_vars() -> Vec { Vec::new() } @@ -138,6 +141,15 @@ pub struct Configs { #[serde(default = "default_restore_clipboard_delay")] pub restore_clipboard_delay: i32, + #[serde(default = "default_secure_input_watcher_enabled")] + pub secure_input_watcher_enabled: bool, + + #[serde(default = "default_secure_input_watcher_interval")] + pub secure_input_watcher_interval: i32, + + #[serde(default = "default_secure_input_notification")] + pub secure_input_notification: bool, + #[serde(default)] pub backend: BackendType, @@ -190,6 +202,9 @@ impl Configs { validate_field!(result, self.passive_arg_escape, default_passive_arg_escape()); validate_field!(result, self.passive_key, default_passive_key()); validate_field!(result, self.restore_clipboard_delay, default_restore_clipboard_delay()); + validate_field!(result, self.secure_input_watcher_enabled, default_secure_input_watcher_enabled()); + validate_field!(result, self.secure_input_watcher_interval, default_secure_input_watcher_interval()); + validate_field!(result, self.secure_input_notification, default_secure_input_notification()); result } diff --git a/src/context/linux.rs b/src/context/linux.rs index 876db30..13b583d 100644 --- a/src/context/linux.rs +++ b/src/context/linux.rs @@ -29,6 +29,7 @@ use std::{thread, time}; use std::sync::atomic::AtomicBool; use std::sync::Arc; use std::sync::atomic::Ordering::Acquire; +use crate::config::Configs; #[repr(C)] pub struct LinuxContext { @@ -37,7 +38,7 @@ pub struct LinuxContext { } impl LinuxContext { - pub fn new(send_channel: Sender, is_injecting: Arc) -> Box { + pub fn new(config: Configs, send_channel: Sender, is_injecting: Arc) -> Box { // Check if the X11 context is available let x11_available = unsafe { check_x11() diff --git a/src/context/macos.rs b/src/context/macos.rs index 0f31d21..7b6ec44 100644 --- a/src/context/macos.rs +++ b/src/context/macos.rs @@ -20,25 +20,30 @@ use std::sync::mpsc::Sender; use std::os::raw::{c_void, c_char}; use crate::bridge::macos::*; -use crate::event::{Event, KeyEvent, KeyModifier, ActionType}; +use crate::event::{Event, KeyEvent, KeyModifier, ActionType, SystemEvent}; use crate::event::KeyModifier::*; use std::ffi::{CString, CStr}; -use std::fs; +use std::{fs, thread}; use log::{info, error, debug}; use std::process::exit; use std::sync::atomic::AtomicBool; use std::sync::Arc; use std::sync::atomic::Ordering::Acquire; +use crate::config::Configs; +use std::cell::RefCell; +use crate::system::macos::MacSystemManager; const STATUS_ICON_BINARY : &[u8] = include_bytes!("../res/mac/icon.png"); pub struct MacContext { pub send_channel: Sender, is_injecting: Arc, + secure_input_watcher_enabled: bool, + secure_input_watcher_interval: i32, } impl MacContext { - pub fn new(send_channel: Sender, is_injecting: Arc) -> Box { + pub fn new(config: Configs, send_channel: Sender, is_injecting: Arc) -> Box { // Check accessibility unsafe { let res = prompt_accessibility(); @@ -53,7 +58,9 @@ impl MacContext { let context = Box::new(MacContext { send_channel, - is_injecting + is_injecting, + secure_input_watcher_enabled: config.secure_input_watcher_enabled, + secure_input_watcher_interval: config.secure_input_watcher_interval, }); // Initialize the status icon path @@ -81,10 +88,59 @@ impl MacContext { context } + + fn start_secure_input_watcher(&self) { + let send_channel = self.send_channel.clone(); + let secure_input_watcher_interval = self.secure_input_watcher_interval as u64; + + let secure_input_watcher = thread::Builder::new().name("secure_input_watcher".to_string()).spawn(move || { + let mut last_secure_input_pid: Option = None; + loop { + let pid = MacSystemManager::get_secure_input_pid(); + + if let Some(pid) = pid { // Some application is currently on SecureInput + let should_notify = if let Some(old_pid) = last_secure_input_pid { // We already detected a SecureInput app + if old_pid != pid { // The old app is different from the current one, we should take action + true + }else{ // We already notified this application before + false + } + }else{ // First time we see this SecureInput app, we should take action + true + }; + + if should_notify { + let secure_input_app = crate::system::macos::MacSystemManager::get_secure_input_application(); + + if let Some((app_name, path)) = secure_input_app { + let event = Event::System(SystemEvent::SecureInputEnabled(app_name, path)); + send_channel.send(event); + } + } + + last_secure_input_pid = Some(pid); + }else{ // No app is currently keeping SecureInput + if let Some(old_pid) = last_secure_input_pid { // If there was an app with SecureInput, notify that is now free + let event = Event::System(SystemEvent::SecureInputDisabled); + send_channel.send(event); + } + + last_secure_input_pid = None + } + + thread::sleep(std::time::Duration::from_millis(secure_input_watcher_interval)); + } + }); + } } impl super::Context for MacContext { fn eventloop(&self) { + // Start the SecureInput watcher thread + if self.secure_input_watcher_enabled { + self.start_secure_input_watcher(); + } + unsafe { eventloop(); } diff --git a/src/context/mod.rs b/src/context/mod.rs index 332ba4f..0508cde 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -32,6 +32,7 @@ use std::path::PathBuf; use std::fs::create_dir_all; use std::sync::{Once, Arc}; use std::sync::atomic::AtomicBool; +use crate::config::Configs; pub trait Context { fn eventloop(&self); @@ -39,20 +40,20 @@ pub trait Context { // MAC IMPLEMENTATION #[cfg(target_os = "macos")] -pub fn new(send_channel: Sender, is_injecting: Arc) -> Box { - macos::MacContext::new(send_channel, is_injecting) +pub fn new(config: Configs, send_channel: Sender, is_injecting: Arc) -> Box { + macos::MacContext::new(config, send_channel, is_injecting) } // LINUX IMPLEMENTATION #[cfg(target_os = "linux")] -pub fn new(send_channel: Sender, is_injecting: Arc) -> Box { - linux::LinuxContext::new(send_channel, is_injecting) +pub fn new(config: Configs, send_channel: Sender, is_injecting: Arc) -> Box { + linux::LinuxContext::new(config, send_channel, is_injecting) } // WINDOWS IMPLEMENTATION #[cfg(target_os = "windows")] -pub fn new(send_channel: Sender, is_injecting: Arc) -> Box { - windows::WindowsContext::new(send_channel, is_injecting) +pub fn new(config: Configs, send_channel: Sender, is_injecting: Arc) -> Box { + windows::WindowsContext::new(config, send_channel, is_injecting) } // espanso directories diff --git a/src/context/windows.rs b/src/context/windows.rs index c797d1f..0640d65 100644 --- a/src/context/windows.rs +++ b/src/context/windows.rs @@ -28,6 +28,7 @@ use log::{info, error, debug}; use std::sync::atomic::AtomicBool; use std::sync::Arc; use std::sync::atomic::Ordering::Acquire; +use crate::config::Configs; const BMP_BINARY : &[u8] = include_bytes!("../res/win/espanso.bmp"); const ICO_BINARY : &[u8] = include_bytes!("../res/win/espanso.ico"); @@ -38,7 +39,7 @@ pub struct WindowsContext { } impl WindowsContext { - pub fn new(send_channel: Sender, is_injecting: Arc) -> Box { + pub fn new(config: Configs, send_channel: Sender, is_injecting: Arc) -> Box { // Initialize image resources let espanso_dir = super::get_data_dir(); diff --git a/src/engine.rs b/src/engine.rs index 3d69476..bd5957e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -24,7 +24,7 @@ use crate::config::BackendType; use crate::clipboard::ClipboardManager; use log::{info, warn, debug, error}; use crate::ui::{UIManager, MenuItem, MenuItemType}; -use crate::event::{ActionEventReceiver, ActionType}; +use crate::event::{ActionEventReceiver, ActionType, SystemEventReceiver, SystemEvent}; use crate::extension::Extension; use crate::render::{Renderer, RenderResult}; use std::cell::RefCell; @@ -333,4 +333,24 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, _ => {} } } +} + +impl <'a, S: KeyboardManager, C: ClipboardManager, + M: ConfigManager<'a>, U: UIManager, R: Renderer> SystemEventReceiver for Engine<'a, S, C, M, U, R>{ + + fn on_system_event(&self, e: SystemEvent) { + match e { + // MacOS specific + SystemEvent::SecureInputEnabled(app_name, path) => { + info!("SecureInput has been acquired by {}, preventing espanso from working correctly. Full path: {}", app_name, path); + + if self.config_manager.default_config().secure_input_notification { + self.ui_manager.notify_delay(&format!("{} has activated SecureInput. Espanso won't work until you disable it.", app_name), 5000); + } + }, + SystemEvent::SecureInputDisabled => { + info!("SecureInput has been disabled."); + }, + } + } } \ No newline at end of file diff --git a/src/event/manager.rs b/src/event/manager.rs index b6cc0b1..db920c1 100644 --- a/src/event/manager.rs +++ b/src/event/manager.rs @@ -17,7 +17,7 @@ * along with espanso. If not, see . */ -use crate::event::{KeyEventReceiver, ActionEventReceiver, Event}; +use crate::event::{KeyEventReceiver, ActionEventReceiver, Event, SystemEventReceiver}; use std::sync::mpsc::Receiver; pub trait EventManager { @@ -28,15 +28,18 @@ pub struct DefaultEventManager<'a> { receive_channel: Receiver, key_receivers: Vec<&'a dyn KeyEventReceiver>, action_receivers: Vec<&'a dyn ActionEventReceiver>, + system_receivers: Vec<&'a dyn SystemEventReceiver>, } impl<'a> DefaultEventManager<'a> { pub fn new(receive_channel: Receiver, key_receivers: Vec<&'a dyn KeyEventReceiver>, - action_receivers: Vec<&'a dyn ActionEventReceiver>) -> DefaultEventManager<'a> { + action_receivers: Vec<&'a dyn ActionEventReceiver>, + system_receivers: Vec<&'a dyn SystemEventReceiver>) -> DefaultEventManager<'a> { DefaultEventManager { receive_channel, key_receivers, action_receivers, + system_receivers } } } @@ -53,6 +56,9 @@ impl <'a> EventManager for DefaultEventManager<'a> { Event::Action(action_event) => { self.action_receivers.iter().for_each(|&receiver| receiver.on_action_event(action_event.clone())); } + Event::System(system_event) => { + self.system_receivers.iter().for_each(move |&receiver| receiver.on_system_event(system_event.clone())); + } } }, Err(e) => panic!("Broken event channel {}", e), diff --git a/src/event/mod.rs b/src/event/mod.rs index 7d63a7f..2547bdf 100644 --- a/src/event/mod.rs +++ b/src/event/mod.rs @@ -24,7 +24,8 @@ use serde::{Serialize, Deserialize}; #[derive(Debug, Clone)] pub enum Event { Action(ActionType), - Key(KeyEvent) + Key(KeyEvent), + System(SystemEvent), } #[derive(Debug, Clone)] @@ -132,6 +133,13 @@ impl KeyModifier { } } +#[derive(Debug, Clone)] +pub enum SystemEvent { + // MacOS specific + SecureInputEnabled(String, String), // AppName, App Path + SecureInputDisabled, +} + // Receivers pub trait KeyEventReceiver { @@ -142,6 +150,10 @@ pub trait ActionEventReceiver { fn on_action_event(&self, e: ActionType); } +pub trait SystemEventReceiver { + fn on_system_event(&self, e: SystemEvent); +} + // TESTS #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index ebc371d..e4d5485 100644 --- a/src/main.rs +++ b/src/main.rs @@ -338,7 +338,7 @@ fn daemon_main(config_set: ConfigSet) { // we could reinterpret the characters we are injecting let is_injecting = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let context = context::new(send_channel.clone(), is_injecting.clone()); + let context = context::new(config_set.default.clone(), send_channel.clone(), is_injecting.clone()); let config_set_copy = config_set.clone(); thread::Builder::new().name("daemon_background".to_string()).spawn(move || { @@ -382,6 +382,7 @@ fn daemon_background(receive_channel: Receiver, config_set: ConfigSet, is receive_channel, vec!(&matcher), vec!(&engine, &matcher), + vec!(&engine), ); info!("espanso is running!"); diff --git a/src/system/macos.rs b/src/system/macos.rs index 8a04e77..d1b401e 100644 --- a/src/system/macos.rs +++ b/src/system/macos.rs @@ -20,7 +20,7 @@ use std::os::raw::c_char; use std::ffi::CStr; -use crate::bridge::macos::{get_active_app_bundle, get_active_app_identifier}; +use crate::bridge::macos::{get_active_app_bundle, get_active_app_identifier, get_secure_input_process, get_path_from_pid}; pub struct MacSystemManager { @@ -75,70 +75,65 @@ impl MacSystemManager { } } + /// Check whether an application is currently holding the Secure Input. + /// Return None if no application has claimed SecureInput, its PID otherwise. + pub fn get_secure_input_pid() -> Option { + unsafe { + let mut pid: i64 = -1; + let res = get_secure_input_process(&mut pid as *mut i64); + + if res > 0{ + Some(pid) + }else{ + None + } + } + } + /// Check whether an application is currently holding the Secure Input. /// Return None if no application has claimed SecureInput, Some((AppName, AppPath)) otherwise. pub fn get_secure_input_application() -> Option<(String, String)> { - use std::process::Command; use regex::Regex; - let output = Command::new("ioreg") - .arg("-d") - .arg("1") - .arg("-k") - .arg("IOConsoleUsers") - .arg("-w") - .arg("0") - .output(); - - lazy_static! { - static ref PID_REGEX: Regex = Regex::new("\"kCGSSessionSecureInputPID\"=(\\d+)").unwrap(); - }; - lazy_static! { static ref APP_REGEX: Regex = Regex::new("/([^/]+).app/").unwrap(); }; - if let Ok(output) = output { - let output_str = String::from_utf8_lossy(output.stdout.as_slice()); - let caps = PID_REGEX.captures(&output_str); + unsafe { + let pid = MacSystemManager::get_secure_input_pid(); - if let Some(caps) = caps { - // Get the PID of the process that is handling SecureInput - let pid_str = caps.get(1).map_or("", |m| m.as_str()); - let pid = pid_str.parse::().expect("Invalid pid value"); + if let Some(pid) = pid { + // Size of the buffer is ruled by the PROC_PIDPATHINFO_MAXSIZE constant. + // the underlying proc_pidpath REQUIRES a buffer of that dimension, otherwise it fail silently. + let mut buffer : [c_char; 4096] = [0; 4096]; + let res = get_path_from_pid(pid, buffer.as_mut_ptr(), buffer.len() as i32); - // Find the process that is handling the SecureInput - let output = Command::new("ps") - .arg("-p") - .arg(pid.to_string()) - .arg("-o") - .arg("command=") - .output(); + if res > 0 { + let c_string = CStr::from_ptr(buffer.as_ptr()); + let string = c_string.to_str(); + if let Ok(path) = string { + if !path.trim().is_empty() { + let process = path.trim().to_string(); + let caps = APP_REGEX.captures(&process); + let app_name = if let Some(caps) = caps { + caps.get(1).map_or("", |m| m.as_str()).to_owned() + }else{ + process.to_owned() + }; - if let Ok(output) = output { - let output_str = String::from_utf8_lossy(output.stdout.as_slice()); - - if !output_str.trim().is_empty() { - let process = output_str.trim().to_string(); - let caps = APP_REGEX.captures(&process); - let app_name = if let Some(caps) = caps { - caps.get(1).map_or("", |m| m.as_str()).to_owned() + Some((app_name, process)) }else{ - process.to_owned() - }; - - Some((app_name, process)) + None + } }else{ None } - }else{ // Can't obtain process name + }else{ None } - }else{ // No process is holding SecureInput + }else{ None } - }else{ // Can't execute the query to the IOKit registry - None } } } \ No newline at end of file diff --git a/src/ui/linux.rs b/src/ui/linux.rs index f45d65a..814e259 100644 --- a/src/ui/linux.rs +++ b/src/ui/linux.rs @@ -30,10 +30,14 @@ pub struct LinuxUIManager { impl super::UIManager for LinuxUIManager { fn notify(&self, message: &str) { + self.notify_delay(message, 2000); + } + + fn notify_delay(&self, message: &str, duration: i32) { let res = Command::new("notify-send") - .args(&["-i", self.icon_path.to_str().unwrap_or_default(), - "-t", "2000", "espanso", message]) - .output(); + .args(&["-i", self.icon_path.to_str().unwrap_or_default(), + "-t", &duration.to_string(), "espanso", message]) + .output(); if let Err(e) = res { error!("Could not send a notification, error: {}", e); diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 8bcf791..ebad9d0 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -29,7 +29,6 @@ use std::os::raw::c_char; use crate::context; const NOTIFY_HELPER_BINARY : &'static [u8] = include_bytes!("../res/mac/EspansoNotifyHelper.zip"); -const DEFAULT_NOTIFICATION_DELAY : f64 = 1.5; pub struct MacUIManager { notify_helper_path: PathBuf @@ -37,12 +36,18 @@ pub struct MacUIManager { impl super::UIManager for MacUIManager { fn notify(&self, message: &str) { + self.notify_delay(message, 1500); + } + + fn notify_delay(&self, message: &str, duration: i32) { let executable_path = self.notify_helper_path.join("Contents"); let executable_path = executable_path.join("MacOS"); let executable_path = executable_path.join("EspansoNotifyHelper"); + let duration_float = duration as f64 / 1000.0; + let res = Command::new(executable_path) - .args(&["espanso", message, &DEFAULT_NOTIFICATION_DELAY.to_string()]) + .args(&["espanso", message, &duration_float.to_string()]) .spawn(); if let Err(e) = res { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1b096c6..81798b7 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -28,6 +28,7 @@ mod macos; pub trait UIManager { fn notify(&self, message: &str); + fn notify_delay(&self, message: &str, duration: i32); fn show_menu(&self, menu: Vec); fn cleanup(&self); } diff --git a/src/ui/windows.rs b/src/ui/windows.rs index 4024939..f6df686 100644 --- a/src/ui/windows.rs +++ b/src/ui/windows.rs @@ -31,17 +31,23 @@ pub struct WindowsUIManager { impl super::UIManager for WindowsUIManager { fn notify(&self, message: &str) { + self.notify_delay(message, 2000); + } + + fn notify_delay(&self, message: &str, duration: i32) { let current_id: i32 = { let mut id = self.id.lock().unwrap(); *id += 1; *id }; + let step = duration / 10; + // Setup a timeout to close the notification let id = Arc::clone(&self.id); let _ = thread::Builder::new().name("notification_thread".to_string()).spawn(move || { for _ in 1..10 { - let duration = time::Duration::from_millis(200); + let duration = time::Duration::from_millis(step as u64); thread::sleep(duration); let new_id = id.lock().unwrap();