// Copyright (c) Facebook, Inc. and its affiliates. use anyhow::{bail, Context, Result}; use log::{debug, info}; use serde::{de::DeserializeOwned, Serialize}; use std::default::Default; use std::fs; use std::io::{self, prelude::*}; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; fn read_json<P: AsRef<Path>>(path: P) -> Result<(String, String)> { let mut f = fs::OpenOptions::new().read(true).open(path)?; let mut buf = String::new(); f.read_to_string(&mut buf)?; let mut preamble = String::new(); let mut body = String::new(); let mut seen_body = false; for line in buf.lines() { let trimmed = line.trim(); if trimmed.starts_with("//") || trimmed.starts_with("#") { if !seen_body { preamble = preamble + line + "\n"; } body = body + "\n"; } else { seen_body = true; body = body + line + "\n" } } Ok((preamble, body)) } pub trait JsonLoad where Self: DeserializeOwned, { fn loaded(&mut self, _prev: Option<&mut Self>) -> Result<()> { Ok(()) } fn load<P: AsRef<Path>>(path: P) -> Result<Self> { let (_, body) = read_json(path)?; Ok(serde_json::from_str::<Self>(&body)?) } } pub trait JsonSave where Self: Default + Serialize, { fn preamble() -> Option<String> { None } fn maybe_create_dfl<P: AsRef<Path>>(path_in: P) -> Result<bool> { let path = path_in.as_ref(); if let Some(parent) = path.parent() { fs::create_dir_all(&parent)?; } match fs::OpenOptions::new() .write(true) .create_new(true) .open(&path) { Ok(mut f) => { let data: Self = Default::default(); f.write_all(data.as_json()?.as_ref())?; Ok(true) } Err(e) => match e.kind() { io::ErrorKind::AlreadyExists => Ok(false), _ => Err(e.into()), }, } } fn as_json(&self) -> Result<String> { let mut serialized = serde_json::to_string_pretty(&self)?; if !serialized.ends_with("\n") { serialized += "\n"; } match Self::preamble() { Some(pre) => Ok(pre + &serialized), None => Ok(serialized), } } fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> { let path: &Path = path.as_ref(); let fname = match path.file_name() { Some(v) => v, None => bail!("can't save to null path"), }; let mut tmp_path = PathBuf::from(path); tmp_path.pop(); tmp_path.push(format!(".{}.json-save-staging", &fname.to_string_lossy())); let mut f = fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .open(&tmp_path) .with_context(|| format!("opening staging file {:?}", &tmp_path))?; f.write_all(self.as_json()?.as_ref()) .with_context(|| format!("writing staging file {:?}", &tmp_path))?; fs::rename(&tmp_path, path) .with_context(|| format!("moving {:?} to {:?}", &tmp_path, path))?; Ok(()) } } #[derive(Clone, Debug)] pub struct JsonConfigFile<T: JsonLoad + JsonSave> { pub path: Option<PathBuf>, pub loaded_mod: SystemTime, pub data: T, } impl<T: JsonLoad + JsonSave + Default> Default for JsonConfigFile<T> { fn default() -> Self { Self { path: None, loaded_mod: UNIX_EPOCH, data: Default::default(), } } } impl<T: JsonLoad + JsonSave> JsonConfigFile<T> { pub fn load<P: AsRef<Path>>(path_in: P) -> Result<Self> { let path = AsRef::<Path>::as_ref(&path_in); let modified = path.metadata()?.modified()?; let mut data = T::load(&path)?; data.loaded(None)?; Ok(Self { path: Some(PathBuf::from(path)), loaded_mod: modified, data, }) } pub fn load_or_create<P: AsRef<Path>>(path_opt: Option<P>) -> Result<Self> { match path_opt { Some(path_in) => { let path = AsRef::<Path>::as_ref(&path_in); if T::maybe_create_dfl(&path)? { info!("cfg: Created {:?}", &path); } Self::load(path) } None => { let mut data: T = Default::default(); data.loaded(None)?; Ok(Self { path: None, loaded_mod: UNIX_EPOCH, data, }) } } } pub fn save(&self) -> Result<()> { if let Some(path) = self.path.as_deref() { self.data.save(&path) } else { Ok(()) } } pub fn maybe_reload(&mut self) -> Result<bool> { let path = match self.path.as_ref() { Some(p) => p, None => return Ok(false), }; let modified = fs::metadata(&path)?.modified()?; // Consider the file iff it stayed the same for at least 10ms. match SystemTime::now().duration_since(modified) { Ok(dur) if dur.as_millis() < 10 => return Ok(false), _ => {} } // The same as loaded? if self.loaded_mod == modified { return Ok(false); } self.loaded_mod = modified; let mut data = T::load(&path)?; data.loaded(Some(&mut self.data))?; self.data = data; Ok(true) } } pub trait JsonArgs where Self: JsonLoad + JsonSave, { fn match_cmdline() -> clap::ArgMatches<'static>; fn verbosity(matches: &clap::ArgMatches) -> u32; fn log_file(matches: &clap::ArgMatches) -> String; fn system_configuration_overrides( _matches: &clap::ArgMatches, ) -> (Option<usize>, Option<usize>, Option<usize>) { (None, None, None) } fn process_cmdline(&mut self, matches: &clap::ArgMatches) -> bool; } pub trait JsonArgsHelper where Self: JsonArgs, { fn init_args_and_logging_nosave() -> Result<(JsonConfigFile<Self>, bool)>; fn save_args(args_file: &JsonConfigFile<Self>) -> Result<()>; fn init_args_and_logging() -> Result<JsonConfigFile<Self>>; } impl<T> JsonArgsHelper for T where T: JsonArgs, { fn init_args_and_logging_nosave() -> Result<(JsonConfigFile<T>, bool)> { let matches = T::match_cmdline(); super::init_logging(T::verbosity(&matches), T::log_file(&matches)); let overrides = T::system_configuration_overrides(&matches); super::override_system_configuration(overrides.0, overrides.1, overrides.2); let mut args_file = JsonConfigFile::<T>::load_or_create(matches.value_of("args").as_ref())?; let updated = args_file.data.process_cmdline(&matches); Ok((args_file, updated)) } fn save_args(args_file: &JsonConfigFile<T>) -> Result<()> { if args_file.path.is_some() { debug!( "Updating command line arguments file {:?}", &args_file.path.as_deref().unwrap() ); args_file.save()?; } Ok(()) } fn init_args_and_logging() -> Result<JsonConfigFile<T>> { let (args_file, updated) = Self::init_args_and_logging_nosave()?; if updated { Self::save_args(&args_file)?; } Ok(args_file) } } #[derive(Debug)] pub struct JsonReportFile<T: JsonSave> { pub path: Option<PathBuf>, pub staging: PathBuf, pub data: T, } impl<T: JsonSave> JsonReportFile<T> { pub fn new<P: AsRef<Path>>(path_opt: Option<P>) -> Self { let (path, staging) = match path_opt { Some(p) => { let pb = PathBuf::from(p.as_ref()); let mut st = pb.clone().into_os_string(); st.push(".staging"); (Some(pb), PathBuf::from(st)) } None => (None, PathBuf::new()), }; Self { path, staging, data: Default::default(), } } pub fn commit(&self) -> Result<()> { let path = match self.path.as_ref() { Some(v) => v, None => return Ok(()), }; self.data.save(&self.staging)?; fs::rename(&self.staging, &path)?; Ok(()) } } pub struct JsonRawFile { pub path: PathBuf, pub preamble: String, pub value: serde_json::Value, } impl JsonRawFile { pub fn load<P: AsRef<Path>>(path_in: P) -> Result<Self> { let path = PathBuf::from(path_in.as_ref()); let (preamble, body) = read_json(&path)?; Ok(Self { path, preamble, value: serde_json::from_str(&body)?, }) } pub fn save(&self) -> Result<()> { let output = self.preamble.clone() + &serde_json::ser::to_string_pretty(&self.value)?; let mut f = fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .open(&self.path)?; f.write_all(output.as_ref())?; Ok(()) } }