diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/arti/src/reload_cfg.rs | 196 | ||||
| -rw-r--r-- | crates/arti/src/rpc.rs | 15 | ||||
| -rw-r--r-- | crates/arti/src/rpc/session.rs | 4 | ||||
| -rw-r--r-- | crates/arti/src/rpc/superuser.rs | 8 | ||||
| -rw-r--r-- | crates/arti/src/subcommands/proxy.rs | 26 |
5 files changed, 197 insertions, 52 deletions
diff --git a/crates/arti/src/reload_cfg.rs b/crates/arti/src/reload_cfg.rs index f5406329b..a55afa0cc 100644 --- a/crates/arti/src/reload_cfg.rs +++ b/crates/arti/src/reload_cfg.rs @@ -1,5 +1,6 @@ //! Code to watch configuration files for any changes. +use std::collections::HashSet; use std::sync::{Arc, Mutex, Weak}; use std::time::Duration; @@ -7,10 +8,14 @@ use anyhow::Context; use arti_client::TorClient; use arti_client::config::Reconfigure; use futures::StreamExt; +use futures::stream::BoxStream; use futures::{FutureExt as _, Stream, select_biased}; +#[cfg(feature = "rpc")] +use tor_config::ConfigurationTree; use tor_config::file_watcher::{ self, FileEventReceiver, FileEventSender, FileWatcher, FileWatcherBuilder, }; +use tor_config::load::{ConfigResolveOptions, DisfavouredKey}; use tor_config::{ConfigurationSource, ConfigurationSources, sources::FoundConfigFiles}; use tor_rtcompat::Runtime; use tor_rtcompat::SpawnExt; @@ -58,18 +63,52 @@ pub(crate) struct CfgMgr<R> { /// A sender to use when constructing new [`FileWatcher`]s. tx: FileEventSender, - /// A list of modules to alert whenever the configuration has changed. - modules: Vec<Weak<dyn ReconfigurableModule>>, - /// Mutable state. inner: Mutex<CfgMgrInner>, } /// Mutable part of a CfgMgr. +#[derive(Default)] struct CfgMgrInner { + /// A list of modules to alert whenever the configuration has changed. + modules: Vec<Weak<dyn ReconfigurableModule>>, + /// If present, a [`FileWatcher`] that is currently watching for changes /// in the configuration files and directories. watcher: Option<FileWatcher>, + + /// RPC only: a fully populated, normalized configuration tree, based on the most recent time + /// that we called [`CfgMgr::reload_configuration`]. + #[cfg(feature = "rpc")] + normalized_cfg: ConfigurationTree, + + /// RPC only: a set of unrecognized options from the configuration. + #[cfg(feature = "rpc")] + unrecognized_keys: HashSet<DisfavouredKey>, + + /// RPC only: a set of deprecated options from the configuration + #[cfg(feature = "rpc")] + deprecated_keys: HashSet<DisfavouredKey>, +} + +/// A watcher process that we have not yet launched. +#[cfg_attr(feature = "experimental-api", visibility::make(pub))] +#[must_use = "UnlaunchedWatcher does nothing unless you launch it."] +pub(crate) struct UnlaunchedWatcher<R> { + /// The related [`CfgMgr`] that we should tell about reconfiguration events. + weak_mgr: Weak<CfgMgr<R>>, + + /// A stream on which we will get alerts about SIGHUP events. + sighup_stream: BoxStream<'static, ()>, + + /// A stream that will tell us when our files are changed. + watcher_rx: FileEventReceiver, + + /// An interval that we wait to debounce events from watcher_rx or sighup_stream. + debounce_interval: Option<Duration>, + + /// If true, we start watching for file changes immediately at launch. + watch_files_at_start: bool, } impl<R: Runtime> CfgMgr<R> { @@ -86,19 +125,21 @@ impl<R: Runtime> CfgMgr<R> { /// See the [`FileWatcher`](FileWatcher#Limitations) docs for limitations. #[cfg_attr(feature = "experimental-api", visibility::make(pub))] #[instrument(level = "trace", skip_all)] - pub(crate) fn launch( + pub(crate) fn new( runtime: R, sources: ConfigurationSources, config: &ArtiConfig, modules: Vec<Weak<dyn ReconfigurableModule>>, - ) -> anyhow::Result<Arc<Self>> { + ) -> anyhow::Result<(Arc<Self>, UnlaunchedWatcher<R>)> { let (tx, rx) = file_watcher::channel(); let mgr = Arc::new(CfgMgr { runtime, sources, tx, - modules, - inner: Mutex::new(CfgMgrInner { watcher: None }), + inner: Mutex::new(CfgMgrInner { + modules, + ..Default::default() + }), }); cfg_if::cfg_if! { @@ -108,27 +149,17 @@ impl<R: Runtime> CfgMgr<R> { let sighup_stream = stream::pending(); } } + let sighup_stream = sighup_stream.boxed(); + + let watcher = UnlaunchedWatcher { + weak_mgr: Arc::downgrade(&mgr), + sighup_stream, + watcher_rx: rx, + debounce_interval: Some(DEBOUNCE_INTERVAL), + watch_files_at_start: config.application().watch_configuration, + }; - let rt = mgr.runtime.clone(); - let weak_mgr = Arc::downgrade(&mgr); - mgr.runtime - .spawn(async move { - let res: anyhow::Result<()> = - run_watcher(rt, rx, sighup_stream, weak_mgr, Some(DEBOUNCE_INTERVAL)).await; - match res { - Ok(()) => debug!("Config watcher task exiting"), - // TODO: warn_report does not work on anyhow::Error. - Err(e) => error!("Config watcher task exiting: {}", tor_error::Report(e)), - } - }) - .context("failed to spawn task")?; - - if config.application().watch_configuration { - let (watcher, _files) = mgr.launch_file_watcher()?; - mgr.inner.lock().expect("lock poisoned").watcher = Some(watcher); - } - - Ok(mgr) + Ok((mgr, watcher)) } /// Create a new [`FileWatcher`] for the files in this configuration. @@ -146,10 +177,14 @@ impl<R: Runtime> CfgMgr<R> { /// Reload the configuration. #[instrument(level = "trace", skip_all)] - fn reload_configuration(&self) -> anyhow::Result<()> { + #[cfg_attr(feature = "experimental-api", visibility::make(pub))] + pub(crate) fn reload_configuration(&self) -> anyhow::Result<()> { let mut inner = self.inner.lock().expect("Lock poisoned"); // TODO RPC: Take 'how' as an argument. + // + // Question: I do not understand why we are making a new file watcher unconditionally + // at this point. -nm let found_files = if inner.watcher.is_some() { let (watcher, files) = self .launch_file_watcher() @@ -162,7 +197,7 @@ impl<R: Runtime> CfgMgr<R> { .context("FS watch: failed to rescan config")? }; - match reconfigure(found_files, &self.modules) { + match reconfigure(found_files, &mut inner) { Ok(watch) => { info!("Successfully reloaded configuration."); if watch && inner.watcher.is_none() { @@ -184,9 +219,75 @@ impl<R: Runtime> CfgMgr<R> { } } +impl<R: Runtime> UnlaunchedWatcher<R> { + /// Begin running the file watcher task for a given configuration manager. + #[cfg_attr(feature = "experimental-api", visibility::make(pub))] + #[instrument(level = "trace", skip_all)] + pub(crate) fn launch(self) -> anyhow::Result<()> { + let UnlaunchedWatcher { + weak_mgr, + sighup_stream, + watcher_rx, + debounce_interval, + watch_files_at_start, + } = self; + let Some(mgr) = weak_mgr.upgrade() else { + return Err(anyhow::anyhow!( + "CfgMgr disappeared before we could launch the monitor task" + )); + }; + + let rt = mgr.runtime.clone(); + let weak_mgr = Arc::downgrade(&mgr); + mgr.runtime + .spawn(async move { + let res: anyhow::Result<()> = + run_watcher(rt, watcher_rx, sighup_stream, weak_mgr, debounce_interval).await; + match res { + Ok(()) => debug!("Config watcher task exiting"), + // TODO: warn_report does not work on anyhow::Error. + Err(e) => error!("Config watcher task exiting: {}", tor_error::Report(e)), + } + }) + .context("failed to spawn task")?; + + if watch_files_at_start { + // Note: You might think that there was a race condition here, where launching the + // watcher _now_ would fail to catch any file changes that had happened between + // reading the configuration initially and now. + // + // You'd be right, except that the [`FileWatcher`] code starts every new FileWatcher + // with a pending `rescan` event. + let (watcher, _files) = mgr.launch_file_watcher()?; + mgr.inner.lock().expect("lock poisoned").watcher = Some(watcher); + } + + Ok(()) + } + + /// Add `module` to the set of modules that need to be reconfigured when the configuration changes. + /// + /// This method is on the [`UnlaunchedWatcher`] because is not (yet) meant to be called after + /// the watcher task is launched. + #[cfg_attr(feature = "experimental-api", visibility::make(pub))] + pub(crate) fn add_module(&self, module: &Arc<dyn ReconfigurableModule>) -> anyhow::Result<()> { + let weak_module = Arc::downgrade(module); + + let Some(mgr) = self.weak_mgr.upgrade() else { + return Err(anyhow::anyhow!( + "CfgMgr disappeared before launching watcher task." + )); + }; + + let mut inner = mgr.inner.lock().expect("poisoned lock"); + inner.modules.push(weak_module); + Ok(()) + } +} + /// Start watching for configuration changes. /// -/// Spawned from [`CfgMgr::launch`]. +/// Spawned from [`UnlaunchedWatcher::launch`]. #[instrument(level = "trace", skip_all)] async fn run_watcher<R: Runtime>( runtime: R, @@ -383,14 +484,29 @@ fn prepare<'a, R: Runtime>( #[instrument(level = "trace", skip_all)] fn reconfigure( found_files: FoundConfigFiles<'_>, - reconfigurable: &[Weak<dyn ReconfigurableModule>], + mgr_inner: &mut CfgMgrInner, ) -> anyhow::Result<bool> { - let _ = reconfigurable; let config = found_files.load()?; - let config = tor_config::resolve::<ArtiCombinedConfig>(config)?; + #[allow(unused_mut)] + let mut resolve_options = ConfigResolveOptions::default(); + #[cfg(feature = "rpc")] + { + resolve_options.want_output_tree = true; + } + + let rs = tor_config::resolve_return_results::<ArtiCombinedConfig>(config, &resolve_options)?; + let config = rs.value; + #[cfg(feature = "rpc")] + { + mgr_inner.normalized_cfg = rs + .output_tree + .expect("normalized cfg not exposed as expected!?"); + mgr_inner.deprecated_keys = rs.deprecated.into_iter().collect(); + mgr_inner.unrecognized_keys = rs.unrecognized.into_iter().collect(); + } // Filter out the modules that have been dropped - let reconfigurable = reconfigurable.iter().flat_map(Weak::upgrade); + let reconfigurable = mgr_inner.modules.iter().flat_map(Weak::upgrade); // If there are no more modules, we should exit. let mut has_modules = false; @@ -509,8 +625,10 @@ mod test { runtime: rt.clone(), sources: cfg_sources, tx: fw_tx, - modules: vec![Arc::downgrade(&module)], - inner: Mutex::new(CfgMgrInner { watcher: None }), + inner: Mutex::new(CfgMgrInner { + modules: vec![Arc::downgrade(&module)], + ..Default::default() + }), }); let (watcher, _) = mgr.launch_file_watcher().unwrap(); @@ -567,8 +685,10 @@ mod test { runtime: rt.clone(), sources: cfg_sources, tx: fw_tx, - modules: vec![Arc::downgrade(&module)], - inner: Mutex::new(CfgMgrInner { watcher: None }), + inner: Mutex::new(CfgMgrInner { + modules: vec![Arc::downgrade(&module)], + ..Default::default() + }), }); let (watcher, _) = mgr.launch_file_watcher().unwrap(); diff --git a/crates/arti/src/rpc.rs b/crates/arti/src/rpc.rs index 466224174..a40d5800c 100644 --- a/crates/arti/src/rpc.rs +++ b/crates/arti/src/rpc.rs @@ -24,7 +24,7 @@ mod superuser; use listener::RpcListenerSetConfig; pub(crate) use session::{RpcStateSender, RpcVisibleArtiState}; -use crate::reload_cfg::LaunchableTorClient; +use crate::reload_cfg::{CfgMgr, LaunchableTorClient}; use crate::rpc::superuser::RpcSuperuser; /// Configuration for Arti's RPC subsystem. @@ -135,6 +135,7 @@ pub(crate) async fn launch_rpc_mgr<R: Runtime>( mistrust: &Mistrust, client: Arc<TorClient<R>>, launchable: Arc<LaunchableTorClient<R>>, + cfg_mgr: Arc<CfgMgr<R>>, ) -> Result<Option<RpcProxySupport>> { if !cfg.enable { return Ok(None); @@ -164,6 +165,7 @@ pub(crate) async fn launch_rpc_mgr<R: Runtime>( rpc_mgr_clone, client, launchable, + cfg_mgr, rpc_state, ) .await; @@ -185,6 +187,7 @@ async fn run_rpc_listener<R: Runtime>( rpc_mgr: Arc<RpcMgr>, client: Arc<TorClient<R>>, launchable: Arc<LaunchableTorClient<R>>, + cfg_mgr: Arc<CfgMgr<R>>, rpc_state: Arc<RpcVisibleArtiState>, ) -> Result<()> { while let Some((stream, _addr, info)) = incoming.next().await.transpose()? { @@ -193,8 +196,16 @@ async fn run_rpc_listener<R: Runtime>( let client_clone = client.clone(); let rpc_state_clone = rpc_state.clone(); let launchable = launchable.clone(); + let cfg_mgr_clone = cfg_mgr.clone(); let connection = rpc_mgr.new_connection(info.auth.clone(), move |auth| { - ArtiRpcSession::new(auth, &client_clone, &launchable, &rpc_state_clone, &info) as _ + ArtiRpcSession::new( + auth, + &client_clone, + &launchable, + &rpc_state_clone, + &cfg_mgr_clone, + &info, + ) as _ }); let (input, output) = stream.split(); diff --git a/crates/arti/src/rpc/session.rs b/crates/arti/src/rpc/session.rs index 853e6ae21..be1bca55b 100644 --- a/crates/arti/src/rpc/session.rs +++ b/crates/arti/src/rpc/session.rs @@ -12,7 +12,7 @@ use tor_rtcompat::Runtime; use crate::{ proxy::port_info, - reload_cfg::LaunchableTorClient, + reload_cfg::{CfgMgr, LaunchableTorClient}, rpc::{listener::RpcConnInfo, superuser::RpcSuperuser}, }; @@ -82,6 +82,7 @@ impl ArtiRpcSession { client_root: &Arc<TorClient<R>>, launchable_client: &Arc<LaunchableTorClient<R>>, arti_state: &Arc<RpcVisibleArtiState>, + cfg_mgr: &Arc<CfgMgr<R>>, listener_info: &RpcConnInfo, ) -> Arc<Self> { let _ = auth; // This is currently unused; any authentication gives the same result. @@ -91,6 +92,7 @@ impl ArtiRpcSession { session.provide_superuser_permission(Arc::new(RpcSuperuser::new( client_root.clone(), launchable_client.clone(), + cfg_mgr.clone(), )) as _); } Arc::new(ArtiRpcSession { diff --git a/crates/arti/src/rpc/superuser.rs b/crates/arti/src/rpc/superuser.rs index c8a3a5cb8..9e36e608f 100644 --- a/crates/arti/src/rpc/superuser.rs +++ b/crates/arti/src/rpc/superuser.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use tor_rpcbase::{self as rpc}; use tor_rtcompat::Runtime; -use crate::reload_cfg::LaunchableTorClient; +use crate::reload_cfg::{CfgMgr, LaunchableTorClient}; /// An object representing superuser access to Arti over an RPC session. /// @@ -27,6 +27,10 @@ pub(super) struct RpcSuperuser<R: Runtime> { /// A wrapper around `tor_client` with the ability to launch a deferred-bootstrap client. launchable: Arc<LaunchableTorClient<R>>, + + /// A handle to the manager for configuration information. + #[allow(unused)] // TODO(rpc) remove + cfg_mgr: Arc<CfgMgr<R>>, } impl<R: Runtime> RpcSuperuser<R> { @@ -34,10 +38,12 @@ impl<R: Runtime> RpcSuperuser<R> { pub(super) fn new( tor_client: Arc<TorClient<R>>, launchable: Arc<LaunchableTorClient<R>>, + cfg_mgr: Arc<CfgMgr<R>>, ) -> Self { RpcSuperuser { tor_client, launchable, + cfg_mgr, } } diff --git a/crates/arti/src/subcommands/proxy.rs b/crates/arti/src/subcommands/proxy.rs index 95a10cdd4..17f480ae4 100644 --- a/crates/arti/src/subcommands/proxy.rs +++ b/crates/arti/src/subcommands/proxy.rs @@ -138,6 +138,9 @@ async fn run_proxy<R: ToplevelRuntime>( false => BootstrapBehavior::OnDemand, }; + let (cfg_mgr, cfg_watcher_task) = + reload_cfg::CfgMgr::new(runtime.clone(), config_sources, &arti_config, vec![])?; + let client_builder = TorClient::with_runtime(runtime.clone()) .config(client_config) .bootstrap_behavior(bootstrap_behavior); @@ -172,18 +175,17 @@ async fn run_proxy<R: ToplevelRuntime>( } }; - // We weak references here to prevent the thread spawned by watch_for_config_changes from + // The add_module function will use references here + // to prevent the task spawned by watch_for_config_changes from // keeping these modules alive after this function exits. // // NOTE: reconfigurable_modules stores the only strong references to these modules, - // so we must keep the variable alive until the end of the function - let weak_modules = reconfigurable_modules.iter().map(Arc::downgrade).collect(); - let _cfg_mgr = reload_cfg::CfgMgr::launch( - client.runtime().clone(), - config_sources, - &arti_config, - weak_modules, - )?; + // so we must keep that variable alive until the end of the function + reconfigurable_modules + .iter() + .try_for_each(|m| cfg_watcher_task.add_module(m))?; + + cfg_watcher_task.launch()?; cfg_if::cfg_if! { if #[cfg(feature = "rpc")] { @@ -194,6 +196,7 @@ async fn run_proxy<R: ToplevelRuntime>( &fs_mistrust, client.clone(), launchable_client.clone(), + cfg_mgr.clone(), ) .await?; let (rpc_mgr, mut rpc_state_sender) = rpc_data @@ -322,8 +325,11 @@ async fn run_proxy<R: ToplevelRuntime>( => r.context("bootstrap"), )?; - // The modules can be dropped now, because we are exiting. + // The modules and CfgMgr can be dropped now, because we are exiting. + // (We drop them explicitly to make sure that they were not dropped + // accidentally before.) drop(reconfigurable_modules); + drop(cfg_mgr); Ok(()) } |
