iroh_util/
lib.rs

1use std::{
2    cell::RefCell,
3    collections::HashMap,
4    env,
5    path::{Path, PathBuf},
6    sync::{
7        atomic::{AtomicUsize, Ordering},
8        Arc,
9    },
10};
11
12use anyhow::{anyhow, Result};
13use cid::{
14    multihash::{Code, MultihashDigest},
15    Cid,
16};
17use config::{Config, ConfigError, Environment, File, Map, Source, Value, ValueKind};
18use tracing::debug;
19
20pub mod exitcodes;
21pub mod human;
22pub mod lock;
23
24/// name of directory that wraps all iroh files in a given application directory
25const IROH_DIR: &str = "iroh";
26#[cfg(unix)]
27const DEFAULT_NOFILE_LIMIT: u64 = 65536;
28#[cfg(unix)]
29const MIN_NOFILE_LIMIT: u64 = 2048;
30
31/// Blocks current thread until ctrl-c is received
32pub async fn block_until_sigint() {
33    let (ctrlc_send, ctrlc_oneshot) = futures::channel::oneshot::channel();
34    let ctrlc_send_c = RefCell::new(Some(ctrlc_send));
35
36    let running = Arc::new(AtomicUsize::new(0));
37    ctrlc::set_handler(move || {
38        let prev = running.fetch_add(1, Ordering::SeqCst);
39        if prev == 0 {
40            println!("Got interrupt, shutting down...");
41            // Send sig int in channel to blocking task
42            if let Some(ctrlc_send) = ctrlc_send_c.try_borrow_mut().unwrap().take() {
43                ctrlc_send.send(()).expect("Error sending ctrl-c message");
44            }
45        } else {
46            std::process::exit(0);
47        }
48    })
49    .expect("Error setting Ctrl-C handler");
50
51    ctrlc_oneshot.await.unwrap();
52}
53
54/// Returns the path to the user's iroh config directory.
55///
56/// If the `IROH_CONFIG_DIR` environment variable is set it will be used unconditionally.
57/// Otherwise the returned value depends on the operating system according to the following
58/// table.
59///
60/// | Platform | Value                                 | Example                          |
61/// | -------- | ------------------------------------- | -------------------------------- |
62/// | Linux    | `$XDG_CONFIG_HOME` or `$HOME`/.config/iroh | /home/alice/.config/iroh              |
63/// | macOS    | `$HOME`/Library/Application Support/iroh   | /Users/Alice/Library/Application Support/iroh |
64/// | Windows  | `{FOLDERID_RoamingAppData}`/iroh           | C:\Users\Alice\AppData\Roaming\iroh   |
65pub fn iroh_config_root() -> Result<PathBuf> {
66    if let Some(val) = env::var_os("IROH_CONFIG_DIR") {
67        return Ok(PathBuf::from(val));
68    }
69    let cfg = dirs_next::config_dir()
70        .ok_or_else(|| anyhow!("operating environment provides no directory for configuration"))?;
71    Ok(cfg.join(IROH_DIR))
72}
73
74// Path that leads to a file in the iroh config directory.
75pub fn iroh_config_path(file_name: &str) -> Result<PathBuf> {
76    let path = iroh_config_root()?.join(file_name);
77    Ok(path)
78}
79
80/// Returns the path to the user's iroh data directory.
81///
82/// If the `IROH_DATA_DIR` environment variable is set it will be used unconditionally.
83/// Otherwise the returned value depends on the operating system according to the following
84/// table.
85///
86/// | Platform | Value                                         | Example                                  |
87/// | -------- | --------------------------------------------- | ---------------------------------------- |
88/// | Linux    | `$XDG_DATA_HOME`/iroh or `$HOME`/.local/share/iroh | /home/alice/.local/share/iroh                 |
89/// | macOS    | `$HOME`/Library/Application Support/iroh      | /Users/Alice/Library/Application Support/iroh |
90/// | Windows  | `{FOLDERID_RoamingAppData}/iroh`              | C:\Users\Alice\AppData\Roaming\iroh           |
91pub fn iroh_data_root() -> Result<PathBuf> {
92    if let Some(val) = env::var_os("IROH_DATA_DIR") {
93        return Ok(PathBuf::from(val));
94    }
95    let path = dirs_next::data_dir().ok_or_else(|| {
96        anyhow!("operating environment provides no directory for application data")
97    })?;
98    Ok(path.join(IROH_DIR))
99}
100
101/// Path that leads to a file in the iroh data directory.
102pub fn iroh_data_path(file_name: &str) -> Result<PathBuf> {
103    let path = iroh_data_root()?.join(file_name);
104    Ok(path)
105}
106
107/// Returns the path to the user's iroh cache directory.
108///
109/// If the `IROH_CACHE_DIR` environment variable is set it will be used unconditionally.
110/// Otherwise the returned value depends on the operating system according to the following
111/// table.
112///
113/// | Platform | Value                                         | Example                                  |
114/// | -------- | --------------------------------------------- | ---------------------------------------- |
115/// | Linux    | `$XDG_CACHE_HOME`/iroh or `$HOME`/.cache/iroh | /home/.cache/iroh                        |
116/// | macOS    | `$HOME`/Library/Caches/iroh                   | /Users/Alice/Library/Caches/iroh         |
117/// | Windows  | `{FOLDERID_LocalAppData}/iroh`                | C:\Users\Alice\AppData\Roaming\iroh      |
118pub fn iroh_cache_root() -> Result<PathBuf> {
119    if let Some(val) = env::var_os("IROH_CACHE_DIR") {
120        return Ok(PathBuf::from(val));
121    }
122    let path = dirs_next::cache_dir().ok_or_else(|| {
123        anyhow!("operating environment provides no directory for application data")
124    })?;
125    Ok(path.join(IROH_DIR))
126}
127
128/// Path that leads to a file in the iroh cache directory.
129pub fn iroh_cache_path(file_name: &str) -> Result<PathBuf> {
130    let path = iroh_cache_root()?.join(file_name);
131    Ok(path)
132}
133
134/// insert a value into a `config::Map`
135pub fn insert_into_config_map<I: Into<String>, V: Into<ValueKind>>(
136    map: &mut Map<String, Value>,
137    field: I,
138    val: V,
139) {
140    map.insert(field.into(), Value::new(None, val));
141}
142
143// struct made to shoe-horn in the ability to use the `IROH_METRICS` env var prefix
144#[derive(Debug, Clone)]
145struct MetricsSource {
146    metrics: Config,
147}
148
149impl Source for MetricsSource {
150    fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
151        Box::new(self.clone())
152    }
153    fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
154        let metrics = self.metrics.collect()?;
155        let mut map = Map::new();
156        insert_into_config_map(&mut map, "metrics", metrics);
157        Ok(map)
158    }
159}
160
161/// Make a config using a default, files, environment variables, and commandline flags.
162///
163/// Later items in the *file_paths* slice will have a higher priority than earlier ones.
164///
165/// Environment variables are expected to start with the *env_prefix*. Nested fields can be
166/// accessed using `.`, if your environment allows env vars with `.`
167///
168/// Note: For the metrics configuration env vars, it is recommended to use the metrics
169/// specific prefix `IROH_METRICS` to set a field in the metrics config. You can use the
170/// above dot notation to set a metrics field, eg, `IROH_CONFIG_METRICS.SERVICE_NAME`, but
171/// only if your environment allows it
172pub fn make_config<T, S, V>(
173    default: T,
174    file_paths: &[Option<&Path>],
175    env_prefix: &str,
176    flag_overrides: HashMap<S, V>,
177) -> Result<T>
178where
179    T: serde::de::DeserializeOwned + Source + Send + Sync + 'static,
180    S: AsRef<str>,
181    V: Into<Value>,
182{
183    // create config builder and add default as first source
184    let mut builder = Config::builder().add_source(default);
185
186    // layer on config options from files
187    for path in file_paths.iter().flatten() {
188        if path.exists() {
189            let p = path.to_str().ok_or_else(|| anyhow::anyhow!("empty path"))?;
190            builder = builder.add_source(File::with_name(p));
191        }
192    }
193
194    // next, add any environment variables
195    builder = builder.add_source(
196        Environment::with_prefix(env_prefix)
197            .separator("__")
198            .try_parsing(true),
199    );
200
201    // pull metrics config from env variables
202    // nesting into this odd `MetricsSource` struct, gives us the option of
203    // using the more convienient prefix `IROH_METRICS` to set metrics env vars
204    let mut metrics = Config::builder().add_source(
205        Environment::with_prefix("IROH_METRICS")
206            .separator("__")
207            .try_parsing(true),
208    );
209
210    // allow custom `IROH_INSTANCE_ID` env var
211    if let Ok(instance_id) = env::var("IROH_INSTANCE_ID") {
212        metrics = metrics.set_override("instance_id", instance_id)?;
213    }
214    // allow custom `IROH_ENV` env var
215    if let Ok(service_env) = env::var("IROH_ENV") {
216        metrics = metrics.set_override("service_env", service_env)?;
217    }
218    let metrics = metrics.build().unwrap();
219
220    builder = builder.add_source(MetricsSource { metrics });
221
222    // finally, override any values
223    for (flag, val) in flag_overrides.into_iter() {
224        builder = builder.set_override(flag, val)?;
225    }
226
227    let cfg = builder.build()?;
228    debug!("make_config:\n{:#?}\n", cfg);
229    let cfg: T = cfg.try_deserialize()?;
230    Ok(cfg)
231}
232
233/// Verifies that the provided bytes hash to the given multihash.
234pub fn verify_hash(cid: &Cid, bytes: &[u8]) -> Option<bool> {
235    Code::try_from(cid.hash().code()).ok().map(|code| {
236        let calculated_hash = code.digest(bytes);
237        &calculated_hash == cid.hash()
238    })
239}
240
241/// If supported sets a preffered limit for file descriptors.
242#[cfg(unix)]
243pub fn increase_fd_limit() -> std::io::Result<u64> {
244    let (_, hard) = rlimit::Resource::NOFILE.get()?;
245    let target = std::cmp::min(hard, DEFAULT_NOFILE_LIMIT);
246    rlimit::Resource::NOFILE.set(target, hard)?;
247    let (soft, _) = rlimit::Resource::NOFILE.get()?;
248    if soft < MIN_NOFILE_LIMIT {
249        return Err(std::io::Error::new(
250            std::io::ErrorKind::Other,
251            format!("NOFILE limit too low: {soft}"),
252        ));
253    }
254    Ok(soft)
255}
256
257#[cfg(test)]
258mod tests {
259    use serde::Deserialize;
260    use testdir::testdir;
261
262    use super::*;
263
264    #[test]
265    fn test_iroh_directory_paths() {
266        let got = iroh_config_path("foo.bar").unwrap();
267        let got = got.to_str().unwrap().to_string();
268        let got = got.replace('\\', "/"); // handle windows paths
269        assert!(dbg!(got).ends_with("/iroh/foo.bar"));
270
271        // Now test the overrides by environment variable.  We have to do this in the same
272        // test since tests are run in parallel but changing environment variables affects
273        // the entire process.
274        temp_env::with_var("IROH_CONFIG_DIR", Some("/a/config/dir"), || {
275            let res = iroh_config_path("iroh-test").unwrap();
276            assert_eq!(res, PathBuf::from("/a/config/dir/iroh-test"));
277        });
278
279        temp_env::with_var("IROH_DATA_DIR", Some("/a/data/dir"), || {
280            let res = iroh_data_path("iroh-test").unwrap();
281            assert_eq!(res, PathBuf::from("/a/data/dir/iroh-test"));
282        });
283
284        temp_env::with_var("IROH_CACHE_DIR", Some("/a/cache/dir"), || {
285            let res = iroh_cache_path("iroh-test").unwrap();
286            assert_eq!(res, PathBuf::from("/a/cache/dir/iroh-test"));
287        });
288    }
289
290    #[derive(Debug, Clone, Deserialize)]
291    struct Config {
292        item: String,
293    }
294
295    impl Source for Config {
296        fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
297            Box::new(self.clone())
298        }
299
300        fn collect(&self) -> Result<Map<String, Value>, ConfigError> {
301            let mut map = Map::new();
302            insert_into_config_map(&mut map, "item", self.item.clone());
303            Ok(map)
304        }
305    }
306
307    #[test]
308    fn test_make_config_priority() {
309        // Asserting that later items have a higher priority
310        let cfgdir = testdir!();
311        let cfgfile0 = cfgdir.join("cfg0.toml");
312        std::fs::write(&cfgfile0, r#"item = "zero""#).unwrap();
313        let cfgfile1 = cfgdir.join("cfg1.toml");
314        std::fs::write(&cfgfile1, r#"item = "one""#).unwrap();
315        let cfg = make_config(
316            Config {
317                item: String::from("default"),
318            },
319            &[Some(cfgfile0.as_path()), Some(cfgfile1.as_path())],
320            "NO_PREFIX_PLEASE_",
321            HashMap::<String, String>::new(),
322        )
323        .unwrap();
324        assert_eq!(cfg.item, "one");
325    }
326}