use std::sync::Weak;
use std::time::Duration;
use anyhow::Context;
use arti_client::config::Reconfigure;
use arti_client::TorClient;
use futures::{select_biased, FutureExt as _, Stream};
use tor_config::file_watcher::{self, FileWatcherBuilder, FileEventSender, FileWatcher};
use tor_config::{sources::FoundConfigFiles, ConfigurationSource, ConfigurationSources};
use tor_rtcompat::Runtime;
use tracing::{debug, error, info, instrument, warn};
use tor_rtcompat::SpawnExt;
use futures::StreamExt;
#[cfg(target_family = "unix")]
use crate::process::sighup_stream;
#[cfg(not(target_family = "unix"))]
use futures::stream;
use crate::{ArtiCombinedConfig, ArtiConfig};
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
pub(crate) trait ReconfigurableModule: Send + Sync {
fn reconfigure(&self, new: &ArtiCombinedConfig) -> anyhow::Result<()>;
}
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
#[instrument(level = "trace", skip_all)]
pub(crate) fn watch_for_config_changes<R: Runtime>(
runtime: &R,
sources: ConfigurationSources,
config: &ArtiConfig,
modules: Vec<Weak<dyn ReconfigurableModule>>,
) -> anyhow::Result<()> {
let watch_file = config.application().watch_configuration;
cfg_if::cfg_if! {
if #[cfg(target_family = "unix")] {
let sighup_stream = sighup_stream()?;
} else {
let sighup_stream = stream::pending();
}
}
let rt = runtime.clone();
let () = runtime.clone().spawn(async move {
let res: anyhow::Result<()> = run_watcher(
rt,
sources,
modules,
watch_file,
sighup_stream,
Some(DEBOUNCE_INTERVAL)
).await;
match res {
Ok(()) => debug!("Config watcher task exiting"),
Err(e) => error!("Config watcher task exiting: {}", tor_error::Report(e)),
}
}).context("failed to spawn task")?;
Ok(())
}
#[allow(clippy::cognitive_complexity)] #[instrument(level = "trace", skip_all)]
async fn run_watcher<R: Runtime>(
runtime: R,
sources: ConfigurationSources,
modules: Vec<Weak<dyn ReconfigurableModule>>,
watch_file: bool,
mut sighup_stream: impl Stream<Item = ()> + Unpin,
debounce_interval: Option<Duration>,
) -> anyhow::Result<()> {
let (tx, mut rx) = file_watcher::channel();
let mut watcher = if watch_file {
let mut watcher = FileWatcher::builder(runtime.clone());
prepare(&mut watcher, &sources)?;
Some(watcher.start_watching(tx.clone())?)
} else {
None
};
debug!("Entering FS event loop");
loop {
select_biased! {
event = sighup_stream.next().fuse() => {
let Some(()) = event else {
break;
};
info!("Received SIGHUP");
watcher = reload_configuration(
runtime.clone(),
watcher,
&sources,
&modules,
tx.clone()
).await?;
},
event = rx.next().fuse() => {
if let Some(debounce_interval) = debounce_interval {
runtime.sleep(debounce_interval).await;
}
while let Some(_ignore) = rx.try_recv() {
}
debug!("Config reload event {:?}: reloading configuration.", event);
watcher = reload_configuration(
runtime.clone(),
watcher,
&sources,
&modules,
tx.clone()
).await?;
},
}
}
Ok(())
}
#[allow(clippy::cognitive_complexity)] #[instrument(level = "trace", skip_all)]
async fn reload_configuration<R: Runtime>(
runtime: R,
mut watcher: Option<FileWatcher>,
sources: &ConfigurationSources,
modules: &[Weak<dyn ReconfigurableModule>],
tx: FileEventSender,
) -> anyhow::Result<Option<FileWatcher>> {
let found_files = if watcher.is_some() {
let mut new_watcher = FileWatcher::builder(runtime.clone());
let found_files = prepare(&mut new_watcher, sources)
.context("FS watch: failed to rescan config and re-establish watch")?;
let new_watcher = new_watcher
.start_watching(tx.clone())
.context("FS watch: failed to start watching config")?;
watcher = Some(new_watcher);
found_files
} else {
sources
.scan()
.context("FS watch: failed to rescan config")?
};
match reconfigure(found_files, modules) {
Ok(watch) => {
info!("Successfully reloaded configuration.");
if watch && watcher.is_none() {
info!("Starting watching over configuration.");
let mut new_watcher = FileWatcher::builder(runtime.clone());
let _found_files = prepare(&mut new_watcher, sources).context(
"FS watch: failed to rescan config and re-establish watch: {}",
)?;
let new_watcher = new_watcher.start_watching(tx.clone()).context(
"FS watch: failed to rescan config and re-establish watch: {}",
)?;
watcher = Some(new_watcher);
} else if !watch && watcher.is_some() {
info!("Stopped watching over configuration.");
watcher = None;
}
}
Err(e) => warn!("Couldn't reload configuration: {}", tor_error::Report(e)),
}
Ok(watcher)
}
impl<R: Runtime> ReconfigurableModule for TorClient<R> {
#[instrument(level = "trace", skip_all)]
fn reconfigure(&self, new: &ArtiCombinedConfig) -> anyhow::Result<()> {
TorClient::reconfigure(self, &new.1, Reconfigure::WarnOnFailures)?;
Ok(())
}
}
pub(crate) struct Application {
original_config: ArtiConfig,
}
impl Application {
pub(crate) fn new(cfg: ArtiConfig) -> Self {
Self {
original_config: cfg,
}
}
}
impl ReconfigurableModule for Application {
#[allow(clippy::cognitive_complexity)]
#[instrument(level = "trace", skip_all)]
fn reconfigure(&self, new: &ArtiCombinedConfig) -> anyhow::Result<()> {
let original = &self.original_config;
let config = &new.0;
if config.proxy() != original.proxy() {
warn!("Can't (yet) reconfigure proxy settings while arti is running.");
}
if config.logging() != original.logging() {
warn!("Can't (yet) reconfigure logging settings while arti is running.");
}
#[cfg(feature = "rpc")]
if config.rpc != original.rpc {
warn!("Can't (yet) change RPC settings while arti is running.");
}
if config.application().permit_debugging && !original.application().permit_debugging {
warn!("Cannot disable application hardening when it has already been enabled.");
}
if !config.application().permit_debugging {
#[cfg(feature = "harden")]
crate::process::enable_process_hardening()?;
}
Ok(())
}
}
fn prepare<'a, R: Runtime>(
watcher: &mut FileWatcherBuilder<R>,
sources: &'a ConfigurationSources,
) -> anyhow::Result<FoundConfigFiles<'a>> {
let sources = sources.scan()?;
for source in sources.iter() {
match source {
ConfigurationSource::Dir(dir) => watcher.watch_dir(dir, "toml")?,
ConfigurationSource::File(file) => watcher.watch_path(file)?,
ConfigurationSource::Verbatim(_) => {}
}
}
Ok(sources)
}
#[instrument(level = "trace", skip_all)]
fn reconfigure(
found_files: FoundConfigFiles<'_>,
reconfigurable: &[Weak<dyn ReconfigurableModule>],
) -> anyhow::Result<bool> {
let _ = reconfigurable;
let config = found_files.load()?;
let config = tor_config::resolve::<ArtiCombinedConfig>(config)?;
let reconfigurable = reconfigurable.iter().flat_map(Weak::upgrade);
let mut has_modules = false;
for module in reconfigurable {
has_modules = true;
module.reconfigure(&config)?;
}
Ok(has_modules && config.0.application().watch_configuration)
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use crate::ArtiConfigBuilder;
use super::*;
use futures::channel::mpsc;
use futures::SinkExt as _;
use tor_config::sources::MustRead;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use test_temp_dir::{test_temp_dir, TestTempDir};
use postage::watch;
use tor_async_utils::PostageWatchSenderExt;
const CONFIG_NAME1: &str = "config1.toml";
const CONFIG_NAME2: &str = "config2.toml";
const CONFIG_NAME3: &str = "config3.toml";
struct TestModule {
tx: Arc<Mutex<watch::Sender<ArtiCombinedConfig>>>,
}
impl ReconfigurableModule for TestModule {
fn reconfigure(&self, new: &ArtiCombinedConfig) -> anyhow::Result<()> {
let config = new.clone();
self.tx.lock().unwrap().maybe_send(|_| config);
Ok(())
}
}
async fn create_module(
) -> (Arc<dyn ReconfigurableModule>, watch::Receiver<ArtiCombinedConfig>) {
let (tx, mut rx) = watch::channel();
let _: ArtiCombinedConfig = rx.next().await.unwrap();
(Arc::new(TestModule { tx: Arc::new(Mutex::new(tx)) }), rx)
}
fn write_file(dir: &TestTempDir, name: &str, data: &[u8]) -> PathBuf {
let tmp = dir.as_path_untracked().join("tmp");
std::fs::write(&tmp, data).unwrap();
let path = dir.as_path_untracked().join(name);
std::fs::rename(tmp, &path).unwrap();
path
}
fn write_config(dir: &TestTempDir, name: &str, config: &ArtiConfigBuilder) -> PathBuf {
let s = toml::to_string(&config).unwrap();
write_file(dir, name, s.as_bytes())
}
#[test]
fn watch_single_file() {
tor_rtcompat::test_with_one_runtime!(|rt| async move {
let temp_dir = test_temp_dir!();
let mut config_builder = ArtiConfigBuilder::default();
config_builder.application().watch_configuration(true);
let cfg_file = write_config(&temp_dir, CONFIG_NAME1, &config_builder);
let mut cfg_sources = ConfigurationSources::new_empty();
cfg_sources.push_source(ConfigurationSource::File(cfg_file), MustRead::MustRead);
let (module, mut rx) = create_module().await;
config_builder.logging().log_sensitive_information(true);
let _: PathBuf = write_config(&temp_dir, CONFIG_NAME1, &config_builder);
let (mut sighup_tx, sighup_rx) = mpsc::unbounded();
let runtime = rt.clone();
let () = rt.spawn(async move {
run_watcher(
runtime,
cfg_sources,
vec![Arc::downgrade(&module)],
true,
sighup_rx,
None,
).await.unwrap();
}).unwrap();
sighup_tx.send(()).await.unwrap();
let config = rx.next().await.unwrap();
assert_eq!(config.0, config_builder.build().unwrap());
config_builder.logging().log_sensitive_information(false);
let _: PathBuf = write_config(&temp_dir, CONFIG_NAME1, &config_builder);
let config = rx.next().await.unwrap();
assert_eq!(config.0, config_builder.build().unwrap());
});
}
#[test]
#[ignore]
fn watch_multiple() {
tor_rtcompat::test_with_one_runtime!(|rt| async move {
let temp_dir = test_temp_dir!();
let mut config_builder1 = ArtiConfigBuilder::default();
config_builder1.application().watch_configuration(true);
let _: PathBuf = write_config(&temp_dir, CONFIG_NAME1, &config_builder1);
let mut cfg_sources = ConfigurationSources::new_empty();
cfg_sources.push_source(
ConfigurationSource::Dir(temp_dir.as_path_untracked().to_path_buf()),
MustRead::MustRead
);
let (module, mut rx) = create_module().await;
let (mut sighup_tx, sighup_rx) = mpsc::unbounded();
let runtime = rt.clone();
let () = rt.spawn(async move {
run_watcher(
runtime,
cfg_sources,
vec![Arc::downgrade(&module)],
true,
sighup_rx,
None,
).await.unwrap();
}).unwrap();
config_builder1.logging().log_sensitive_information(true);
let _: PathBuf = write_config(&temp_dir, CONFIG_NAME1, &config_builder1);
sighup_tx.send(()).await.unwrap();
let config = rx.next().await.unwrap();
assert_eq!(config.0, config_builder1.build().unwrap());
let mut config_builder2 = ArtiConfigBuilder::default();
config_builder2.application().watch_configuration(true);
config_builder2.system().max_files(0_u64);
let _: PathBuf = write_config(&temp_dir, CONFIG_NAME2, &config_builder2);
let mut config_builder_combined = config_builder1.clone();
config_builder_combined.system().max_files(0_u64);
let config = rx.next().await.unwrap();
assert_eq!(config.0, config_builder_combined.build().unwrap());
config_builder2.logging().console("foo".to_string());
let mut config_builder_combined2 = config_builder_combined.clone();
config_builder_combined2.logging().console("foo".to_string());
let config3: PathBuf = write_config(&temp_dir, CONFIG_NAME3, &config_builder2);
let config = rx.next().await.unwrap();
assert_eq!(config.0, config_builder_combined2.build().unwrap());
std::fs::remove_file(config3).unwrap();
let config = rx.next().await.unwrap();
assert_eq!(config.0, config_builder_combined.build().unwrap());
});
}
}