//! ## ActivityManager //! //! `activity_manager` is the module which provides run methods and handling for activities use std::env; use std::path::PathBuf; use std::time::Duration; use remotefs_ssh::SshKeyStorage as SshKeyStorageTrait; use crate::cli::{Remote, RemoteArgs}; use crate::filetransfer::{ FileTransferParams, FileTransferProtocol, HostBridgeParams, ProtocolParams, }; use crate::host::HostError; use crate::system::bookmarks_client::BookmarksClient; use crate::system::config_client::ConfigClient; use crate::system::environment; use crate::system::sshkey_storage::SshKeyStorage; use crate::system::theme_provider::ThemeProvider; use crate::ui::activities::auth::AuthActivity; use crate::ui::activities::filetransfer::FileTransferActivity; use crate::ui::activities::setup::SetupActivity; use crate::ui::activities::{Activity, ExitReason}; use crate::ui::context::Context; use crate::utils::{fmt, tty}; /// NextActivity identifies the next identity to run once the current has ended pub enum NextActivity { Authentication, FileTransfer, SetupActivity, } pub enum Host { HostBridge, Remote, } pub enum HostParams { HostBridge(HostBridgeParams), Remote(FileTransferParams), } /// The activity manager takes care of running activities and handling them until the application has ended pub struct ActivityManager { context: Option, ticks: Duration, } impl ActivityManager { /// Initializes a new Activity Manager pub fn new(ticks: Duration, keyring: bool) -> Result { // Prepare Context // Initialize configuration client let (config_client, error_config): (ConfigClient, Option) = match Self::init_config_client() { Ok(cli) => (cli, None), Err(err) => { error!("Failed to initialize config client: {}", err); (ConfigClient::degraded(), Some(err)) } }; let (bookmarks_client, error_bookmark) = match Self::init_bookmarks_client(keyring) { Ok(cli) => (cli, None), Err(err) => (None, Some(err)), }; let error = error_config.or(error_bookmark); let theme_provider: ThemeProvider = Self::init_theme_provider(); let ctx: Context = Context::new(bookmarks_client, config_client, theme_provider, error); Ok(ActivityManager { context: Some(ctx), ticks, }) } /// Configure remote args pub fn configure_remote_args(&mut self, remote_args: RemoteArgs) -> Result<(), String> { // Set for host bridge match remote_args.host_bridge { Remote::Bookmark(params) => self.resolve_bookmark_name( Host::HostBridge, ¶ms.name, params.password.as_deref(), ), Remote::Host(host_params) => self.set_host_params( HostParams::HostBridge(HostBridgeParams::Remote( host_params.file_transfer_params.protocol, host_params.file_transfer_params.params, )), host_params.password.as_deref(), ), Remote::None => { // local dir is remote_args.local_dir if set, otherwise current dir let local_dir = remote_args .local_dir .unwrap_or_else(|| env::current_dir().unwrap()); debug!("host bridge is None, setting local dir to {:?}", local_dir,); self.set_host_params( HostParams::HostBridge(HostBridgeParams::Localhost(local_dir)), None, ) } }?; // set remote match remote_args.remote { Remote::Bookmark(params) => { self.resolve_bookmark_name(Host::Remote, ¶ms.name, params.password.as_deref()) } Remote::Host(host_params) => self.set_host_params( HostParams::Remote(host_params.file_transfer_params), host_params.password.as_deref(), ), Remote::None => Ok(()), } } /// Set file transfer params pub fn set_host_params( &mut self, host: HostParams, password: Option<&str>, ) -> Result<(), String> { let (remote_local_path, remote_remote_path) = match &host { HostParams::Remote(params) => (params.local_path.clone(), params.remote_path.clone()), _ => (None, None), }; let mut remote_params = match &host { HostParams::HostBridge(HostBridgeParams::Remote(protocol, protocol_params)) => { Some((*protocol, protocol_params.clone())) } HostParams::HostBridge(HostBridgeParams::Localhost(_)) => None, HostParams::Remote(ft_params) => Some((ft_params.protocol, ft_params.params.clone())), }; // Put params into the context if let Some((protocol, params)) = remote_params.as_mut() { self.resolve_password_for_protocol_params(*protocol, params, password)?; } match host { HostParams::HostBridge(HostBridgeParams::Localhost(path)) => { self.context .as_mut() .unwrap() .set_host_bridge_params(HostBridgeParams::Localhost(path)); } HostParams::HostBridge(HostBridgeParams::Remote(_, _)) => { let (protocol, params) = remote_params.unwrap(); self.context .as_mut() .unwrap() .set_host_bridge_params(HostBridgeParams::Remote(protocol, params)); } HostParams::Remote(_) => { let (protocol, params) = remote_params.unwrap(); let params = FileTransferParams { local_path: remote_local_path, remote_path: remote_remote_path, protocol, params, }; self.context.as_mut().unwrap().set_remote_params(params); } } Ok(()) } fn resolve_password_for_protocol_params( &mut self, protocol: FileTransferProtocol, params: &mut ProtocolParams, password: Option<&str>, ) -> Result<(), String> { // Set password if provided if params.password_missing() { if let Some(password) = password { params.set_default_secret(password.to_string()); } else if matches!( protocol, FileTransferProtocol::Scp | FileTransferProtocol::Sftp, ) && params.generic_params().is_some() { // * if protocol is SCP or SFTP check whether a SSH key is registered for this remote, in case not ask password let storage = SshKeyStorage::from(self.context.as_ref().unwrap().config()); let generic_params = params.generic_params().unwrap(); if storage .resolve( &generic_params.address, &generic_params .username .clone() .unwrap_or(whoami::username()), ) .is_none() { debug!( "storage could not find any suitable key for {}... prompting for password", generic_params.address ); self.prompt_password(params)?; } else { debug!( "a key is already set for {}; password is not required", generic_params.address ); } } else { self.prompt_password(params)?; } } Ok(()) } /// Prompt user for password to set into params. fn prompt_password(&mut self, params: &mut ProtocolParams) -> Result<(), String> { let ctx = self.context.as_mut().unwrap(); let prompt = format!("Password for {}: ", params.host_name()); match tty::read_secret_from_tty(ctx.terminal(), prompt) { Err(err) => Err(format!("Could not read password: {err}")), Ok(Some(secret)) => { debug!( "Read password from tty: {}", fmt::shadow_password(secret.as_str()) ); params.set_default_secret(secret); Ok(()) } Ok(None) => Ok(()), } } /// Resolve provided bookmark name and set it as file transfer params. /// Returns error if bookmark is not found pub fn resolve_bookmark_name( &mut self, host: Host, bookmark_name: &str, password: Option<&str>, ) -> Result<(), String> { if let Some(bookmarks_client) = self.context.as_mut().unwrap().bookmarks_client_mut() { let params = match bookmarks_client.get_bookmark(bookmark_name) { None => { return Err(format!( r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"# )); } Some(params) => params, }; let params = match host { Host::Remote => HostParams::Remote(params), Host::HostBridge => { HostParams::HostBridge(HostBridgeParams::Remote(params.protocol, params.params)) } }; self.set_host_params(params, password) } else { Err(String::from( "Could not resolve bookmark name: bookmarks client not initialized", )) } } /// /// Loop for activity manager. You need to provide the activity to start with /// Returns the exitcode pub fn run(&mut self, launch_activity: NextActivity) { let mut current_activity: Option = Some(launch_activity); loop { current_activity = match current_activity { Some(activity) => match activity { NextActivity::Authentication => self.run_authentication(), NextActivity::FileTransfer => self.run_filetransfer(), NextActivity::SetupActivity => self.run_setup(), }, None => break, // Exit } } // Drop context drop(self.context.take()); } // -- Activity Loops /// Loop for Authentication activity. /// Returns when activity terminates. /// Returns the next activity to run fn run_authentication(&mut self) -> Option { info!("Starting AuthActivity..."); // Prepare activity let mut activity: AuthActivity = AuthActivity::new(self.ticks); // Prepare result let result: Option; // Get context let ctx: Context = match self.context.take() { Some(ctx) => ctx, None => { error!("Failed to start AuthActivity: context is None"); return None; } }; // Create activity activity.on_create(ctx); loop { // Draw activity activity.on_draw(); // Check if has to be terminated if let Some(exit_reason) = activity.will_umount() { match exit_reason { ExitReason::Quit => { info!("AuthActivity terminated due to 'Quit'"); result = None; break; } ExitReason::EnterSetup => { // User requested activity info!("AuthActivity terminated due to 'EnterSetup'"); result = Some(NextActivity::SetupActivity); break; } ExitReason::Connect => { // User submitted, set next activity info!("AuthActivity terminated due to 'Connect'"); result = Some(NextActivity::FileTransfer); break; } _ => { /* Nothing to do */ } } } } // Destroy activity self.context = activity.on_destroy(); info!("AuthActivity destroyed"); result } /// Loop for FileTransfer activity. /// Returns when activity terminates. /// Returns the next activity to run fn run_filetransfer(&mut self) -> Option { info!("Starting FileTransferActivity"); // Get context let mut ctx: Context = match self.context.take() { Some(ctx) => ctx, None => { error!("Failed to start FileTransferActivity: context is None"); return None; } }; let host_bridge_params = match ctx.host_bridge_params() { Some(params) => params.clone(), None => { error!("Failed to start FileTransferActivity: host bridge params is None"); return None; } }; // If ft params is None, return None let remote_params: &FileTransferParams = match ctx.remote_params() { Some(ft_params) => ft_params, None => { error!("Failed to start FileTransferActivity: file transfer params is None"); return None; } }; // try to setup activity let mut activity = match FileTransferActivity::new(host_bridge_params, remote_params, self.ticks) { Ok(activity) => activity, Err(err) => { error!("Failed to start FileTransferActivity: {}", err); ctx.set_error(err); self.context = Some(ctx); // Return to authentication return Some(NextActivity::Authentication); } }; // Prepare result let result: Option; // Create activity activity.on_create(ctx); loop { // Draw activity activity.on_draw(); // Check if has to be terminated if let Some(exit_reason) = activity.will_umount() { match exit_reason { ExitReason::Quit => { info!("FileTransferActivity terminated due to 'Quit'"); result = None; break; } ExitReason::Disconnect => { // User disconnected, set next activity to authentication info!("FileTransferActivity terminated due to 'Authentication'"); result = Some(NextActivity::Authentication); break; } _ => { /* Nothing to do */ } } } } // Destroy activity self.context = activity.on_destroy(); result } /// `SetupActivity` run loop. /// Returns when activity terminates. /// Returns the next activity to run fn run_setup(&mut self) -> Option { // Prepare activity let mut activity: SetupActivity = SetupActivity::new(self.ticks); // Get context let ctx: Context = match self.context.take() { Some(ctx) => ctx, None => { error!("Failed to start SetupActivity: context is None"); return None; } }; // Create activity activity.on_create(ctx); loop { // Draw activity activity.on_draw(); // Check if activity has terminated if let Some(ExitReason::Quit) = activity.will_umount() { info!("SetupActivity terminated due to 'Quit'"); break; } } // Destroy activity self.context = activity.on_destroy(); // This activity always returns to AuthActivity Some(NextActivity::Authentication) } // -- misc fn init_bookmarks_client(keyring: bool) -> Result, String> { crate::support::bookmarks_client(keyring) } /// Initialize configuration client fn init_config_client() -> Result { // Get config dir match environment::init_config_dir() { Ok(config_dir) => { match config_dir { Some(config_dir) => { // Get config client paths let (config_path, ssh_dir): (PathBuf, PathBuf) = environment::get_config_paths(config_dir.as_path()); match ConfigClient::new(config_path.as_path(), ssh_dir.as_path()) { Ok(cli) => Ok(cli), Err(err) => Err(format!("Could not read configuration: {err}")), } } None => Err(String::from( "Your system doesn't provide a configuration directory", )), } } Err(err) => Err(format!( "Could not initialize configuration directory: {err}" )), } } fn init_theme_provider() -> ThemeProvider { match environment::init_config_dir() { Ok(config_dir) => { match config_dir { Some(config_dir) => { // Get config client paths let theme_path: PathBuf = environment::get_theme_path(config_dir.as_path()); match ThemeProvider::new(theme_path.as_path()) { Ok(provider) => provider, Err(err) => { error!( "Could not initialize theme provider with file '{}': {}; using theme provider in degraded mode", theme_path.display(), err ); ThemeProvider::degraded() } } } None => { error!( "This system doesn't provide a configuration directory; using theme provider in degraded mode" ); ThemeProvider::degraded() } } } Err(err) => { error!( "Could not initialize configuration directory: {}; using theme provider in degraded mode", err ); ThemeProvider::degraded() } } } }