use crate::env::environment::Environment; use nu_data::config::{Conf, NuConfig}; use nu_engine::Env; use nu_engine::EvaluationContext; use nu_errors::ShellError; use parking_lot::Mutex; use std::sync::{atomic::Ordering, Arc}; pub struct EnvironmentSyncer { pub env: Arc>>, pub config: Arc>>, } impl Default for EnvironmentSyncer { fn default() -> Self { Self::new() } } impl EnvironmentSyncer { pub fn with_config(config: Box) -> Self { EnvironmentSyncer { env: Arc::new(Mutex::new(Box::new(Environment::new()))), config: Arc::new(Mutex::new(config)), } } pub fn new() -> EnvironmentSyncer { EnvironmentSyncer { env: Arc::new(Mutex::new(Box::new(Environment::new()))), config: Arc::new(Mutex::new(Box::new(NuConfig::new()))), } } #[cfg(test)] pub fn set_config(&mut self, config: Box) { self.config = Arc::new(Mutex::new(config)); } pub fn get_config(&self) -> Box { let config = self.config.lock(); config.clone_box() } pub fn load_environment(&mut self) { let config = self.config.lock(); self.env = Arc::new(Mutex::new(Box::new(Environment::from_config(&*config)))); } pub fn did_config_change(&mut self) -> bool { let config = self.config.lock(); config.is_modified().unwrap_or(false) } pub fn reload(&mut self) { let mut config = self.config.lock(); config.reload(); let mut environment = self.env.lock(); environment.morph(&*config); } pub fn autoenv(&self, ctx: &mut EvaluationContext) -> Result<(), ShellError> { let mut environment = self.env.lock(); let recently_used = ctx .user_recently_used_autoenv_untrust .load(Ordering::SeqCst); let auto = environment.autoenv(recently_used); ctx.user_recently_used_autoenv_untrust .store(false, Ordering::SeqCst); auto } pub fn sync_env_vars(&mut self, ctx: &mut EvaluationContext) { let mut environment = self.env.lock(); if environment.env().is_some() { for (name, value) in ctx.with_host(|host| host.vars()) { if name != "path" && name != "PATH" { // account for new env vars present in the current session // that aren't loaded from config. environment.add_env(&name, &value); // clear the env var from the session // we are about to replace them ctx.with_host(|host| host.env_rm(std::ffi::OsString::from(name))); } } if let Some(variables) = environment.env() { for var in variables.row_entries() { if let Ok(string) = var.1.as_string() { ctx.with_host(|host| { host.env_set( std::ffi::OsString::from(var.0), std::ffi::OsString::from(&string), ) }); ctx.scope.add_env_var_to_base(var.0, string); } } } } } pub fn sync_path_vars(&mut self, ctx: &mut EvaluationContext) { let mut environment = self.env.lock(); if environment.path().is_some() { let native_paths = ctx.with_host(|host| host.env_get(std::ffi::OsString::from("PATH"))); if let Some(native_paths) = native_paths { environment.add_path(native_paths); ctx.with_host(|host| { host.env_rm(std::ffi::OsString::from("PATH")); }); } if let Some(new_paths) = environment.path() { let prepared = std::env::join_paths( new_paths .table_entries() .map(|p| p.as_string()) .filter_map(Result::ok), ); if let Ok(paths_ready) = prepared { ctx.with_host(|host| { host.env_set( std::ffi::OsString::from("PATH"), std::ffi::OsString::from(&paths_ready), ); }); ctx.scope .add_env_var_to_base("PATH", paths_ready.to_string_lossy().to_string()); } } } } #[cfg(test)] pub fn clear_env_vars(&mut self, ctx: &mut EvaluationContext) { for (key, _value) in ctx.with_host(|host| host.vars()) { if key != "path" && key != "PATH" { ctx.with_host(|host| host.env_rm(std::ffi::OsString::from(key))); } } } #[cfg(test)] pub fn clear_path_var(&mut self, ctx: &mut EvaluationContext) { ctx.with_host(|host| host.env_rm(std::ffi::OsString::from("PATH"))); } } #[cfg(test)] mod tests { use super::EnvironmentSyncer; use indexmap::IndexMap; use nu_data::config::tests::FakeConfig; use nu_engine::Env; use nu_engine::EvaluationContext; use nu_errors::ShellError; use nu_test_support::fs::Stub::FileWithContent; use nu_test_support::playground::Playground; use parking_lot::Mutex; use std::path::PathBuf; use std::sync::Arc; // This test fails on Linux. // It's possible it has something to do with the fake configuration // TODO: More tests. #[cfg(not(target_os = "linux"))] #[test] fn syncs_env_if_new_env_entry_is_added_to_an_existing_configuration() -> Result<(), ShellError> { let mut ctx = EvaluationContext::basic()?; ctx.host = Arc::new(Mutex::new(Box::new(nu_engine::FakeHost::new()))); let mut expected = IndexMap::new(); expected.insert( "SHELL".to_string(), "/usr/bin/you_already_made_the_nu_choice".to_string(), ); Playground::setup("syncs_env_from_config_updated_test_1", |dirs, sandbox| { sandbox.with_files(vec![ FileWithContent( "configuration.toml", r#" [env] SHELL = "/usr/bin/you_already_made_the_nu_choice" "#, ), FileWithContent( "updated_configuration.toml", r#" [env] SHELL = "/usr/bin/you_already_made_the_nu_choice" USER = "NUNO" "#, ), ]); let file = dirs.test().join("configuration.toml"); let new_file = dirs.test().join("updated_configuration.toml"); let fake_config = FakeConfig::new(&file); let mut actual = EnvironmentSyncer::with_config(Box::new(fake_config)); // Here, the environment variables from the current session // are cleared since we will load and set them from the // configuration file actual.clear_env_vars(&mut ctx); // Nu loads the environment variables from the configuration file actual.load_environment(); actual.sync_env_vars(&mut ctx); { let environment = actual.env.lock(); let mut vars = IndexMap::new(); environment .env() .expect("No variables in the environment.") .row_entries() .for_each(|(name, value)| { vars.insert( name.to_string(), value.as_string().expect("Couldn't convert to string"), ); }); for k in expected.keys() { assert!(vars.contains_key(k)); } } assert!(!actual.did_config_change()); // Replacing the newer configuration file to the existing one. let new_config_contents = std::fs::read_to_string(new_file).expect("Failed"); std::fs::write(&file, &new_config_contents).expect("Failed"); // A change has happened assert!(actual.did_config_change()); // Syncer should reload and add new envs actual.reload(); actual.sync_env_vars(&mut ctx); expected.insert("USER".to_string(), "NUNO".to_string()); { let environment = actual.env.lock(); let mut vars = IndexMap::new(); environment .env() .expect("No variables in the environment.") .row_entries() .for_each(|(name, value)| { vars.insert( name.to_string(), value.as_string().expect("Couldn't convert to string"), ); }); for k in expected.keys() { assert!(vars.contains_key(k)); } } }); Ok(()) } #[test] fn syncs_env_if_new_env_entry_in_session_is_not_in_configuration_file() -> Result<(), ShellError> { let mut ctx = EvaluationContext::basic()?; ctx.host = Arc::new(Mutex::new(Box::new(nu_engine::FakeHost::new()))); let mut expected = IndexMap::new(); expected.insert( "SHELL".to_string(), "/usr/bin/you_already_made_the_nu_choice".to_string(), ); expected.insert("USER".to_string(), "NUNO".to_string()); Playground::setup("syncs_env_test_1", |dirs, sandbox| { sandbox.with_files(vec![FileWithContent( "configuration.toml", r#" [env] SHELL = "/usr/bin/you_already_made_the_nu_choice" "#, )]); let mut file = dirs.test().clone(); file.push("configuration.toml"); let fake_config = FakeConfig::new(&file); let mut actual = EnvironmentSyncer::new(); actual.set_config(Box::new(fake_config)); // Here, the environment variables from the current session // are cleared since we will load and set them from the // configuration file (if any) actual.clear_env_vars(&mut ctx); // We explicitly simulate and add the USER variable to the current // session's environment variables with the value "NUNO". ctx.with_host(|test_host| { test_host.env_set( std::ffi::OsString::from("USER"), std::ffi::OsString::from("NUNO"), ) }); // Nu loads the environment variables from the configuration file (if any) actual.load_environment(); // By this point, Nu has already loaded the environment variables // stored in the configuration file. Before continuing we check // if any new environment variables have been added from the ones loaded // in the configuration file. // // Nu sees the missing "USER" variable and accounts for it. actual.sync_env_vars(&mut ctx); // Confirms session environment variables are replaced from Nu configuration file // including the newer one accounted for. ctx.with_host(|test_host| { let var_user = test_host .env_get(std::ffi::OsString::from("USER")) .expect("Couldn't get USER var from host.") .into_string() .expect("Couldn't convert to string."); let var_shell = test_host .env_get(std::ffi::OsString::from("SHELL")) .expect("Couldn't get SHELL var from host.") .into_string() .expect("Couldn't convert to string."); let mut found = IndexMap::new(); found.insert("SHELL".to_string(), var_shell); found.insert("USER".to_string(), var_user); for k in found.keys() { assert!(expected.contains_key(k)); } }); // Now confirm in-memory environment variables synced appropriately // including the newer one accounted for. let environment = actual.env.lock(); let mut vars = IndexMap::new(); environment .env() .expect("No variables in the environment.") .row_entries() .for_each(|(name, value)| { vars.insert( name.to_string(), value.as_string().expect("Couldn't convert to string"), ); }); for k in expected.keys() { assert!(vars.contains_key(k)); } }); Ok(()) } #[test] fn nu_envs_have_higher_priority_and_does_not_get_overwritten() -> Result<(), ShellError> { let mut ctx = EvaluationContext::basic()?; ctx.host = Arc::new(Mutex::new(Box::new(nu_engine::FakeHost::new()))); let mut expected = IndexMap::new(); expected.insert( "SHELL".to_string(), "/usr/bin/you_already_made_the_nu_choice".to_string(), ); Playground::setup("syncs_env_test_2", |dirs, sandbox| { sandbox.with_files(vec![FileWithContent( "configuration.toml", r#" [env] SHELL = "/usr/bin/you_already_made_the_nu_choice" "#, )]); let mut file = dirs.test().clone(); file.push("configuration.toml"); let fake_config = FakeConfig::new(&file); let mut actual = EnvironmentSyncer::new(); actual.set_config(Box::new(fake_config)); actual.clear_env_vars(&mut ctx); ctx.with_host(|test_host| { test_host.env_set( std::ffi::OsString::from("SHELL"), std::ffi::OsString::from("/usr/bin/sh"), ) }); actual.load_environment(); actual.sync_env_vars(&mut ctx); ctx.with_host(|test_host| { let var_shell = test_host .env_get(std::ffi::OsString::from("SHELL")) .expect("Couldn't get SHELL var from host.") .into_string() .expect("Couldn't convert to string."); let mut found = IndexMap::new(); found.insert("SHELL".to_string(), var_shell); for k in found.keys() { assert!(expected.contains_key(k)); } }); let environment = actual.env.lock(); let mut vars = IndexMap::new(); environment .env() .expect("No variables in the environment.") .row_entries() .for_each(|(name, value)| { vars.insert( name.to_string(), value.as_string().expect("couldn't convert to string"), ); }); for k in expected.keys() { assert!(vars.contains_key(k)); } }); Ok(()) } #[test] fn syncs_path_if_new_path_entry_in_session_is_not_in_configuration_file( ) -> Result<(), ShellError> { let mut ctx = EvaluationContext::basic()?; ctx.host = Arc::new(Mutex::new(Box::new(nu_engine::FakeHost::new()))); let expected = std::env::join_paths(vec![ PathBuf::from("/Users/andresrobalino/.volta/bin"), PathBuf::from("/Users/mosqueteros/bin"), PathBuf::from("/path/to/be/added"), ]) .expect("Couldn't join paths.") .into_string() .expect("Couldn't convert to string."); Playground::setup("syncs_path_test_1", |dirs, sandbox| { sandbox.with_files(vec![FileWithContent( "configuration.toml", r#" path = ["/Users/andresrobalino/.volta/bin", "/Users/mosqueteros/bin"] "#, )]); let mut file = dirs.test().clone(); file.push("configuration.toml"); let fake_config = FakeConfig::new(&file); let mut actual = EnvironmentSyncer::new(); actual.set_config(Box::new(fake_config)); // Here, the environment variables from the current session // are cleared since we will load and set them from the // configuration file (if any) actual.clear_path_var(&mut ctx); // We explicitly simulate and add the PATH variable to the current // session with the path "/path/to/be/added". ctx.with_host(|test_host| { test_host.env_set( std::ffi::OsString::from("PATH"), std::env::join_paths(vec![PathBuf::from("/path/to/be/added")]) .expect("Couldn't join paths."), ) }); // Nu loads the path variables from the configuration file (if any) actual.load_environment(); // By this point, Nu has already loaded environment path variable // stored in the configuration file. Before continuing we check // if any new paths have been added from the ones loaded in the // configuration file. // // Nu sees the missing "/path/to/be/added" and accounts for it. actual.sync_path_vars(&mut ctx); ctx.with_host(|test_host| { let actual = test_host .env_get(std::ffi::OsString::from("PATH")) .expect("Couldn't get PATH var from host.") .into_string() .expect("Couldn't convert to string."); assert_eq!(actual, expected); }); let environment = actual.env.lock(); let paths = std::env::join_paths( &environment .path() .expect("No path variable in the environment.") .table_entries() .map(|value| value.as_string().expect("Couldn't convert to string")) .map(PathBuf::from) .collect::>(), ) .expect("Couldn't join paths.") .into_string() .expect("Couldn't convert to string."); assert_eq!(paths, expected); }); Ok(()) } #[test] fn nu_paths_have_higher_priority_and_new_paths_get_appended_to_the_end( ) -> Result<(), ShellError> { let mut ctx = EvaluationContext::basic()?; ctx.host = Arc::new(Mutex::new(Box::new(nu_engine::FakeHost::new()))); let expected = std::env::join_paths(vec![ PathBuf::from("/Users/andresrobalino/.volta/bin"), PathBuf::from("/Users/mosqueteros/bin"), PathBuf::from("/path/to/be/added"), ]) .expect("Couldn't join paths.") .into_string() .expect("Couldn't convert to string."); Playground::setup("syncs_path_test_2", |dirs, sandbox| { sandbox.with_files(vec![FileWithContent( "configuration.toml", r#" path = ["/Users/andresrobalino/.volta/bin", "/Users/mosqueteros/bin"] "#, )]); let mut file = dirs.test().clone(); file.push("configuration.toml"); let fake_config = FakeConfig::new(&file); let mut actual = EnvironmentSyncer::new(); actual.set_config(Box::new(fake_config)); actual.clear_path_var(&mut ctx); ctx.with_host(|test_host| { test_host.env_set( std::ffi::OsString::from("PATH"), std::env::join_paths(vec![PathBuf::from("/path/to/be/added")]) .expect("Couldn't join paths."), ) }); actual.load_environment(); actual.sync_path_vars(&mut ctx); ctx.with_host(|test_host| { let actual = test_host .env_get(std::ffi::OsString::from("PATH")) .expect("Couldn't get PATH var from host.") .into_string() .expect("Couldn't convert to string."); assert_eq!(actual, expected); }); let environment = actual.env.lock(); let paths = std::env::join_paths( &environment .path() .expect("No path variable in the environment.") .table_entries() .map(|value| value.as_string().expect("Couldn't convert to string")) .map(PathBuf::from) .collect::>(), ) .expect("Couldn't join paths.") .into_string() .expect("Couldn't convert to string."); assert_eq!(paths, expected); }); Ok(()) } }