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
24const IROH_DIR: &str = "iroh";
26#[cfg(unix)]
27const DEFAULT_NOFILE_LIMIT: u64 = 65536;
28#[cfg(unix)]
29const MIN_NOFILE_LIMIT: u64 = 2048;
30
31pub 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 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
54pub 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
74pub fn iroh_config_path(file_name: &str) -> Result<PathBuf> {
76 let path = iroh_config_root()?.join(file_name);
77 Ok(path)
78}
79
80pub 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
101pub fn iroh_data_path(file_name: &str) -> Result<PathBuf> {
103 let path = iroh_data_root()?.join(file_name);
104 Ok(path)
105}
106
107pub 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
128pub fn iroh_cache_path(file_name: &str) -> Result<PathBuf> {
130 let path = iroh_cache_root()?.join(file_name);
131 Ok(path)
132}
133
134pub 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#[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
161pub 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 let mut builder = Config::builder().add_source(default);
185
186 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 builder = builder.add_source(
196 Environment::with_prefix(env_prefix)
197 .separator("__")
198 .try_parsing(true),
199 );
200
201 let mut metrics = Config::builder().add_source(
205 Environment::with_prefix("IROH_METRICS")
206 .separator("__")
207 .try_parsing(true),
208 );
209
210 if let Ok(instance_id) = env::var("IROH_INSTANCE_ID") {
212 metrics = metrics.set_override("instance_id", instance_id)?;
213 }
214 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 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
233pub 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#[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('\\', "/"); assert!(dbg!(got).ends_with("/iroh/foo.bar"));
270
271 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 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}