Files
IronStorage/apps/tui/src/runtime.rs
2026-08-10 11:39:45 +00:00

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);
}
}