292 lines
8.3 KiB
Rust
292 lines
8.3 KiB
Rust
//! 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<AsyncResult>,
|
|
receiver: Receiver<AsyncResult>,
|
|
tasks: Mutex<Vec<thread::JoinHandle<()>>>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum AuthenticationEvent {
|
|
Granted(AuthenticationTarget),
|
|
Failed { workflow: bool, message: String },
|
|
Expired,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum AuthenticationTarget {
|
|
Entry(String),
|
|
Workflow(Box<WorkflowSubmission>),
|
|
Git(ironstorage::command::GitRequest),
|
|
Otp(crate::app::OtpUiRequest),
|
|
Show(ironstorage::command::ShowRequest),
|
|
}
|
|
|
|
struct AuthenticationCompletion {
|
|
generation: u64,
|
|
target: AuthenticationTarget,
|
|
result: Result<NativeAuthenticationHandle, String>,
|
|
}
|
|
|
|
pub struct AuthenticationCoordinator {
|
|
session: NativeAuthenticationSession,
|
|
key: KeyInfo,
|
|
handle: Option<NativeAuthenticationHandle>,
|
|
sender: Sender<AuthenticationCompletion>,
|
|
receiver: Receiver<AuthenticationCompletion>,
|
|
generation: u64,
|
|
}
|
|
|
|
impl AuthenticationCoordinator {
|
|
pub fn system(
|
|
timeout: ironstorage::authentication::AuthenticationTimeout,
|
|
key: KeyInfo,
|
|
) -> Result<Self, String> {
|
|
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,
|
|
})
|
|
}
|
|
|
|
pub fn request_entry(&mut self, entry: String) {
|
|
self.request(AuthenticationTarget::Entry(entry));
|
|
}
|
|
|
|
pub fn request_workflow(&mut self, submission: Box<WorkflowSubmission>) {
|
|
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);
|
|
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<AuthenticationEvent> {
|
|
let completion = self
|
|
.receiver
|
|
.try_iter()
|
|
.filter(|completion| completion.generation == self.generation)
|
|
.last()?;
|
|
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<AuthenticationEvent> {
|
|
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<AuthenticationEvent> {
|
|
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<Duration> {
|
|
self.handle
|
|
.as_ref()
|
|
.and_then(|handle| handle.remaining_time().ok())
|
|
}
|
|
|
|
pub fn handle(&self) -> Option<NativeAuthenticationHandle> {
|
|
self.handle.clone()
|
|
}
|
|
|
|
pub fn lock(&mut self) -> Result<(), String> {
|
|
self.generation = self.generation.wrapping_add(1);
|
|
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<F>(&self, token: RequestToken, work: F)
|
|
where
|
|
F: FnOnce() -> Result<AsyncPayload, String> + 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 drain(&self) -> impl Iterator<Item = AsyncResult> + '_ {
|
|
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);
|
|
}
|
|
}
|