//! Small in-process executor for storage calls. It never launches a process. use std::{ sync::{ Mutex, mpsc::{self, Receiver, Sender}, }, thread, time::Duration, }; use ironstorage::{ authentication::{NativeAuthenticationHandle, NativeAuthenticationSession}, crypto::KeyInfo, secret_store::SecretProtectionPolicy, }; use crate::app::{AsyncPayload, AsyncResult, RequestToken}; use crate::workflow::WorkflowSubmission; pub struct AsyncExecutor { sender: Sender, receiver: Receiver, tasks: Mutex>>, } #[derive(Debug)] pub enum AuthenticationEvent { Granted(AuthenticationTarget), Failed { workflow: bool, message: String }, Expired, } #[derive(Debug)] pub enum AuthenticationTarget { Entry(String), Workflow(Box), Git(ironstorage::command::GitRequest), Otp(crate::app::OtpUiRequest), Show(ironstorage::command::ShowRequest), } struct AuthenticationCompletion { generation: u64, target: AuthenticationTarget, result: Result, } pub struct AuthenticationCoordinator { session: NativeAuthenticationSession, key: KeyInfo, handle: Option, sender: Sender, receiver: Receiver, generation: u64, native_prompt_pending: bool, } impl AuthenticationCoordinator { pub fn system( timeout: ironstorage::authentication::AuthenticationTimeout, key: KeyInfo, ) -> Result { let session = NativeAuthenticationSession::system(SecretProtectionPolicy::default(), timeout) .map_err(|error| error.to_string())?; let (sender, receiver) = mpsc::channel(); Ok(Self { session, key, handle: None, sender, receiver, generation: 0, native_prompt_pending: false, }) } pub fn request_entry(&mut self, entry: String) { self.request(AuthenticationTarget::Entry(entry)); } pub fn request_workflow(&mut self, submission: Box) { self.request(AuthenticationTarget::Workflow(submission)); } pub fn request_git(&mut self, request: ironstorage::command::GitRequest) { self.request(AuthenticationTarget::Git(request)); } pub fn request_otp(&mut self, request: crate::app::OtpUiRequest) { self.request(AuthenticationTarget::Otp(request)); } pub fn request_show(&mut self, request: ironstorage::command::ShowRequest) { self.request(AuthenticationTarget::Show(request)); } fn request(&mut self, target: AuthenticationTarget) { self.generation = self.generation.wrapping_add(1); self.native_prompt_pending = self.handle.is_none(); let generation = self.generation; let session = self.session.clone(); let key = self.key.clone(); let sender = self.sender.clone(); thread::spawn(move || { let result = session .authenticate(&key) .map_err(|error| error.to_string()); let _ignored = sender.send(AuthenticationCompletion { generation, target, result, }); }); } pub fn completion(&mut self) -> Option { let completion = self .receiver .try_iter() .filter(|completion| completion.generation == self.generation) .last()?; self.native_prompt_pending = false; match completion.result { Ok(handle) => { self.handle = Some(handle); Some(AuthenticationEvent::Granted(completion.target)) } Err(error) => { self.handle = None; Some(AuthenticationEvent::Failed { workflow: matches!(completion.target, AuthenticationTarget::Workflow(_)), message: error, }) } } } pub fn touch_user_activity(&mut self) -> Option { let handle = self.handle.as_ref()?; if let Err(error) = handle.touch_user_activity() { self.handle = None; return Some(AuthenticationEvent::Failed { workflow: false, message: error.to_string(), }); } None } pub fn poll_lease(&mut self) -> Option { match self.session.expire() { Ok(true) => { self.handle = None; Some(AuthenticationEvent::Expired) } Ok(false) => None, Err(error) => { self.handle = None; Some(AuthenticationEvent::Failed { workflow: false, message: error.to_string(), }) } } } pub fn remaining_time(&self) -> Option { self.handle .as_ref() .and_then(|handle| handle.remaining_time().ok()) } pub fn handle(&self) -> Option { self.handle.clone() } pub const fn native_prompt_pending(&self) -> bool { self.native_prompt_pending } pub fn lock(&mut self) -> Result<(), String> { self.generation = self.generation.wrapping_add(1); self.native_prompt_pending = false; self.handle = None; self.session .manual_lock() .map_err(|error| error.to_string()) } } impl Default for AsyncExecutor { fn default() -> Self { Self::new() } } impl AsyncExecutor { pub fn new() -> Self { let (sender, receiver) = mpsc::channel(); Self { sender, receiver, tasks: Mutex::new(Vec::new()), } } pub fn submit(&self, token: RequestToken, work: F) where F: FnOnce() -> Result + Send + 'static, { let sender = self.sender.clone(); let task = thread::spawn(move || { let _ignored = sender.send(AsyncResult { token, payload: work(), }); }); self.tasks .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .push(task); } pub fn progress_reporter( &self, token: RequestToken, ) -> impl Fn(ironstorage::git::GitProgressPhase) + Send + Sync + 'static { let sender = self.sender.clone(); move |phase| { let _ignored = sender.send(AsyncResult { token, payload: Ok(AsyncPayload::GitProgress(phase)), }); } } pub fn clipboard_started_reporter( &self, token: RequestToken, ) -> impl Fn(crate::app::ClipboardPresentationId, std::time::Instant) + Send + 'static { let sender = self.sender.clone(); move |presentation, deadline| { let _ignored = sender.send(AsyncResult { token, payload: Ok(AsyncPayload::ClipboardStarted { presentation, deadline, }), }); } } pub fn drain(&self) -> impl Iterator + '_ { self.receiver.try_iter() } } impl Drop for AsyncExecutor { fn drop(&mut self) { let tasks = self .tasks .get_mut() .unwrap_or_else(std::sync::PoisonError::into_inner); for task in tasks.drain(..) { let _ignored = task.join(); } } } #[cfg(test)] mod tests { use std::time::Duration; use super::*; use crate::app::{App, ResultDisposition}; #[test] fn work_completes_off_the_input_thread() { let executor = AsyncExecutor::new(); let mut app = App::new(); let token = app.begin_request(); executor.submit(token, || Err("test result".to_owned())); let result = executor .receiver .recv_timeout(Duration::from_secs(2)) .expect("worker should return a typed result"); assert_eq!(app.apply_result(result), ResultDisposition::Applied); } #[test] fn blocked_storage_work_never_blocks_interaction_state() { let executor = AsyncExecutor::new(); let mut app = App::new(); let token = app.begin_request(); let (release, blocked) = std::sync::mpsc::channel(); executor.submit(token, move || { blocked.recv().expect("release slow storage"); Err("slow result".to_owned()) }); app.resize(60, 12); app.tick(); assert!(app.transition(crate::app::Transition::OpenHelp)); assert_eq!(app.mode(), crate::app::Mode::Help); release.send(()).expect("release worker"); let result = executor .receiver .recv_timeout(Duration::from_secs(2)) .expect("worker result"); assert_eq!(app.apply_result(result), ResultDisposition::Applied); } }