| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| crate::ConfigLevel, |
| anyhow::{Context, Result}, |
| errors::ffx_error, |
| serde::{Deserialize, Serialize}, |
| std::{ |
| collections::HashMap, |
| fmt, |
| fs::{File, OpenOptions}, |
| io::{BufReader, Write}, |
| path::Path, |
| sync::Mutex, |
| }, |
| }; |
| |
| #[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] |
| pub struct Environment { |
| pub user: Option<String>, |
| pub build: Option<HashMap<String, String>>, |
| pub global: Option<String>, |
| } |
| |
| // This lock protects from concurrent [Environment]s from modifying the same underlying file. |
| // While in the normal case we typically only have one [Environment], it's possible to have |
| // multiple instances during tests. If we're not careful, it's possible concurrent [Environment]s |
| // could stomp on each other if they happen to use the same underlying file. To protect against |
| // this, we hold a lock while we read or write to the underlying file. |
| // |
| // It is inefficient to hold the lock for all [Environment] files, since we only need it when |
| // we're reading and writing to the same file. We could be more efficient if we a global map to |
| // control access to individual files, but we only encounter multiple [Environment]s in tests, so |
| // it's probably not worth the overhead. |
| lazy_static::lazy_static! { |
| static ref ENV_MUTEX: Mutex<()> = Mutex::default(); |
| } |
| |
| impl Environment { |
| pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> { |
| let path = path.as_ref(); |
| |
| // Grab the lock because we're reading from the environment file. |
| let _e = ENV_MUTEX.lock().unwrap(); |
| let file = File::open(path).context("opening file for read")?; |
| |
| serde_json::from_reader(BufReader::new(file)).context("reading environment from disk") |
| } |
| |
| pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> { |
| let path = path.as_ref(); |
| |
| // First save the config to a temp file in the same location as the file, then atomically |
| // rename the file to the final location to avoid partially written files. |
| let parent = path.parent().unwrap_or_else(|| Path::new(".")); |
| let mut tmp = tempfile::NamedTempFile::new_in(parent)?; |
| |
| // Grab the lock because we're writing to the environment file. |
| let _e = ENV_MUTEX.lock().unwrap(); |
| serde_json::to_writer_pretty(&mut tmp, &self).context("writing environment to disk")?; |
| |
| tmp.flush().context("flushing environment")?; |
| |
| let _ = tmp.persist(path)?; |
| |
| Ok(()) |
| } |
| |
| fn display_user(&self) -> String { |
| self.user.as_ref().map_or_else(|| format!(" User: none\n"), |u| format!(" User: {}\n", u)) |
| } |
| |
| fn display_build(&self) -> String { |
| let mut res = format!(" Build:"); |
| match self.build.as_ref() { |
| Some(m) => { |
| if m.is_empty() { |
| res.push_str(&format!(" none\n")); |
| } |
| res.push_str(&format!("\n")); |
| for (key, val) in m.iter() { |
| res.push_str(&format!(" {} => {}\n", key, val)); |
| } |
| } |
| None => { |
| res.push_str(&format!(" none\n")); |
| } |
| } |
| res |
| } |
| |
| fn display_global(&self) -> String { |
| self.global |
| .as_ref() |
| .map_or_else(|| format!(" Global: none\n"), |g| format!(" Global: {}\n", g)) |
| } |
| |
| pub fn display(&self, level: &Option<ConfigLevel>) -> String { |
| level.map_or_else( |
| || { |
| let mut res = format!("\nEnvironment:\n"); |
| res.push_str(&self.display_user()); |
| res.push_str(&self.display_build()); |
| res.push_str(&self.display_global()); |
| res |
| }, |
| |l| match l { |
| ConfigLevel::User => self.display_user(), |
| ConfigLevel::Build => self.display_build(), |
| ConfigLevel::Global => self.display_global(), |
| _ => format!(" This level is not saved in the environment file."), |
| }, |
| ) |
| } |
| |
| pub fn init_env_file(path: &Path) -> Result<()> { |
| let _e = ENV_MUTEX.lock().unwrap(); |
| let mut f = OpenOptions::new() |
| .read(true) |
| .write(true) |
| .create(true) |
| .truncate(true) |
| .open(path) |
| .map_err(|e| { |
| ffx_error!( |
| "Could not create envinronment file from given path \"{}\": {}", |
| path.display(), |
| e |
| ) |
| })?; |
| f.write_all(b"{}")?; |
| f.sync_all()?; |
| Ok(()) |
| } |
| } |
| |
| impl fmt::Display for Environment { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| writeln!(f, "{}", self.display(&None)) |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // tests |
| |
| #[cfg(test)] |
| mod test { |
| use {super::*, std::fs, tempfile::NamedTempFile}; |
| |
| const ENVIRONMENT: &'static str = r#" |
| { |
| "user": "/tmp/user.json", |
| "build": { |
| "/tmp/build/1": "/tmp/build/1/build.json" |
| }, |
| "global": "/tmp/global.json" |
| }"#; |
| |
| #[test] |
| fn test_loading_and_saving_environment() { |
| let env: Environment = serde_json::from_str(ENVIRONMENT).unwrap(); |
| |
| // Write out the initial test environment. |
| let mut tmp_load = NamedTempFile::new().unwrap(); |
| serde_json::to_writer(&mut tmp_load, &env).unwrap(); |
| tmp_load.flush().unwrap(); |
| |
| // Load the environment back in, and make sure it's correct. |
| let env_load = Environment::load(tmp_load.path()).unwrap(); |
| assert_eq!(env, env_load); |
| |
| // Save the environment, then read the saved file and make sure it's correct. |
| let mut tmp_save = NamedTempFile::new().unwrap(); |
| env.save(tmp_save.path()).unwrap(); |
| tmp_save.flush().unwrap(); |
| |
| let env_file = fs::read(tmp_save.path()).unwrap(); |
| let env_save: Environment = serde_json::from_slice(&env_file).unwrap(); |
| |
| assert_eq!(env, env_save); |
| } |
| } |