/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/util/env.rs
420 строк
13 KB
Nathan Whitaker
fix(os): serialize process environment updates (#36074)
10 авг 2026, 22:22
Не верифицирован
10 авг 2026, 22:22
19c25e2
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::collections::HashMap; use std::collections::HashSet; use std::ffi::OsStr; use std::ffi::OsString; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use std::sync::Mutex; use std::sync::OnceLock; use deno_terminal::colors; use crate::sys::CliSys; /// Environment variables that configure Deno's own runtime behavior and must /// not be settable from an `.env` file loaded via `--env` / `--env-file`. /// /// An `.env` file ships alongside the code it accompanies, so letting it set /// Deno's own control variables would let that file change runtime behavior /// the user did not opt into, for example silently enabling tunnel mode /// (`DENO_CONNECTED`) or overriding the tunnel control endpoint /// (`DENO_DEPLOY_TUNNEL_ENDPOINT`). Env files are meant to provide /// configuration to the user's program, not to reconfigure Deno itself, so /// these keys are ignored when they originate from an env file. const ENV_FILE_DENYLIST: &[&str] = &["DENO_CONNECTED", "DENO_DEPLOY_TUNNEL_ENDPOINT"]; /// Whether `key` is a Deno-internal control variable that must not be set from /// an env file. The comparison is ASCII case-insensitive because environment /// variable lookups are case-insensitive on Windows. fn is_denied_env_file_key(key: &OsStr) -> bool { match key.to_str() { Some(key) => ENV_FILE_DENYLIST .iter() .any(|denied| key.eq_ignore_ascii_case(denied)), None => false, } } pub fn handle_denied_env_file_key( key: &OsStr, file_path: &Path, log_level: Option<log::Level>, ) { #[allow(clippy::print_stderr, reason = "can't use log crate yet")] if log_level.map(|l| l >= log::Level::Info).unwrap_or(true) { eprintln!( "{} Ignoring '{}' from environment file '{}': this variable controls Deno's own runtime behavior and cannot be set from an env file.", colors::yellow("Warning"), key.to_string_lossy(), file_path.display(), ) } } pub fn resolve_cwd( initial_cwd: Option<&Path>, ) -> Result<Cow<'_, Path>, std::io::Error> { match initial_cwd { Some(initial_cwd) => Ok(Cow::Borrowed(initial_cwd)), #[allow( clippy::disallowed_methods, reason = "ok because the lint recommends using this method" )] None => std::env::current_dir().map(Cow::Owned).map_err(|err| { std::io::Error::new( err.kind(), format!("could not read current working directory: {err}"), ) }), } } /// Like `resolve_cwd`, but falls back to a sensible default (the system /// root) when the current working directory can't be determined — for /// example when it has been unlinked. This matches Node.js semantics where /// the REPL still starts even if the parent process's cwd was deleted. pub fn resolve_cwd_or_fallback(initial_cwd: Option<&Path>) -> PathBuf { match resolve_cwd(initial_cwd) { Ok(cwd) => cwd.into_owned(), Err(_) => fallback_cwd(), } } fn fallback_cwd() -> PathBuf { if cfg!(windows) { // System drive root, e.g. `C:\`. std::env::var_os("SystemDrive") .map(|d| { let mut p = PathBuf::from(d); p.push("\\"); p }) .unwrap_or_else(|| PathBuf::from("C:\\")) } else { PathBuf::from("/") } } #[derive(Debug, Clone)] struct WatchEnvTrackerInner { // Track all loaded variables and their values loaded_variables: HashSet<OsString>, // Track variables that are no longer present in any loaded file unused_variables: HashSet<OsString>, // Track original env vars that existed before we started original_env: HashMap<OsString, OsString>, } impl WatchEnvTrackerInner { fn new() -> Self { // Capture the original environment state let process_env = deno_os::ProcessEnvGuard::lock(); let original_env: HashMap<OsString, OsString> = process_env.vars_os().into_iter().collect(); Self { loaded_variables: Default::default(), unused_variables: Default::default(), original_env, } } } #[derive(Debug, Clone)] pub struct WatchEnvTracker { inner: Arc<Mutex<WatchEnvTrackerInner>>, } // Global singleton instance static WATCH_ENV_TRACKER: OnceLock<WatchEnvTracker> = OnceLock::new(); impl WatchEnvTracker { /// Get the global singleton instance pub fn snapshot() -> &'static WatchEnvTracker { WATCH_ENV_TRACKER.get_or_init(|| WatchEnvTracker { inner: Arc::new(Mutex::new(WatchEnvTrackerInner::new())), }) } // Internal method that accepts an already-acquired lock to avoid deadlocks fn load_env_file_inner( &self, cwd: &Path, env_file: &str, log_level: Option<log::Level>, inner: &mut WatchEnvTrackerInner, ) { let (file_path, content) = match deno_dotenv::find_path_and_content( &CliSys::default(), cwd, env_file, ) { Ok(Some(result)) => result, Ok(None) => { handle_dotenv_not_found(env_file, log_level); return; } Err(err) => { handle_dotenv_io_error(&err, log_level); return; } }; // Keep substitutions and the mutations they produce consistent with each // other while allowing file I/O to proceed without the process-env lock. let process_env = deno_os::ProcessEnvGuard::lock(); match deno_dotenv::from_content_sanitized_iter_with_substitution( &CliSys::default(), &content, ) { Ok(iter) => { for item in iter { match item { Ok((key, value)) => { // Convert to OsString for consistency let key_os = OsString::from(key); let value_os = OsString::from(value); // Process-level env vars should always take precedence over env files. if inner.original_env.contains_key(&key_os) { #[allow( clippy::print_stderr, reason = "can't use log crate yet" )] if log_level.map(|l| l >= log::Level::Debug).unwrap_or(false) { eprintln!( "{} Variable '{}' already exists in the process environment, skipping value from '{}'", colors::yellow("Debug"), key_os.to_string_lossy(), file_path.display() ); } continue; } // Check if this variable is already loaded from a previous file if inner.loaded_variables.contains(&key_os) { // Variable already exists from a previous file, skip it #[allow( clippy::print_stderr, reason = "can't use log crate yet" )] if log_level.map(|l| l >= log::Level::Debug).unwrap_or(false) { eprintln!( "{} Variable '{}' already loaded from '{}', skipping value from '{}'", colors::yellow("Debug"), key_os.to_string_lossy(), inner .loaded_variables .get(&key_os) .map(|k| k.to_string_lossy().to_string()) .unwrap_or_else(|| "unknown".to_string()), file_path.display() ); } continue; } if is_denied_env_file_key(&key_os) { handle_denied_env_file_key(&key_os, &file_path, log_level); continue; } // Watched env reloads run between user-code isolates. Refreshing // libc here is sufficient for TZ because the replacement // isolate has not been created yet and has no cached timezone to // invalidate. process_env.set_var(&key_os, &value_os); // Track this variable inner.loaded_variables.insert(key_os.clone()); inner.unused_variables.remove(&key_os); } Err(e) => { handle_dotenv_error(&e, &file_path, log_level); } } } } Err(e) => { handle_dotenv_error(&e, &file_path, log_level); } } } /// Clean up variables that are no longer present in any loaded file fn _cleanup_removed_variables( &self, inner: &mut WatchEnvTrackerInner, log_level: Option<log::Level>, process_env: &deno_os::ProcessEnvGuard, ) { for var_name in inner.unused_variables.iter() { // As with loading watched env values, cleanup runs before the // replacement isolate is created, so TZ changes do not need a V8 // notification here. if !inner.original_env.contains_key(var_name) { process_env.remove_var(var_name); #[allow(clippy::print_stderr, reason = "can't use log crate yet")] if log_level.map(|l| l >= log::Level::Debug).unwrap_or(false) { eprintln!( "{} Variable '{}' removed from environment as it's no longer present in any loaded file", colors::yellow("Debug"), var_name.to_string_lossy() ); } } else { let original_value = inner.original_env.get(var_name).unwrap(); process_env.set_var(var_name, original_value); #[allow(clippy::print_stderr, reason = "can't use log crate yet")] if log_level.map(|l| l >= log::Level::Debug).unwrap_or(false) { eprintln!( "{} Variable '{}' restored to original value as it's no longer present in any loaded file", colors::yellow("Debug"), var_name.to_string_lossy() ); } } } } // Load multiple env files in reverse order (later files take precedence over earlier ones) pub fn load_env_variables_from_env_files( &self, cwd: &Path, env_files: &[String], log_level: Option<log::Level>, ) { let mut inner = self.inner.lock().unwrap(); inner.unused_variables = std::mem::take(&mut inner.loaded_variables); inner.loaded_variables = HashSet::new(); for env_file_path in env_files.iter().rev() { self.load_env_file_inner(cwd, env_file_path, log_level, &mut inner); } let process_env = deno_os::ProcessEnvGuard::lock(); self._cleanup_removed_variables(&mut inner, log_level, &process_env); } } pub fn load_env_variables_from_env_files( cwd: &Path, env_file_names: &[String], flags_log_level: Option<log::Level>, ) { let original_env_keys: HashSet<OsString> = { let process_env = deno_os::ProcessEnvGuard::lock(); process_env .vars_os() .into_iter() .map(|(key, _)| key) .collect() }; let mut loaded_keys = HashSet::new(); for env_file_name in env_file_names.iter().rev() { let (env_file_path, content) = match deno_dotenv::find_path_and_content( &CliSys::default(), cwd, env_file_name, ) { Ok(Some(resolved)) => resolved, Ok(None) => { handle_dotenv_not_found(env_file_name, flags_log_level); continue; } Err(err) => { handle_dotenv_io_error(&err, flags_log_level); continue; } }; // Keep substitutions and the mutations they produce consistent with each // other while allowing file I/O to proceed without the process-env lock. let process_env = deno_os::ProcessEnvGuard::lock(); let iter = match deno_dotenv::from_content_sanitized_iter_with_substitution( &sys_traits::impls::RealSys, &content, ) { Ok(iter) => iter, Err(err) => { handle_dotenv_error(&err, &env_file_path, flags_log_level); continue; } }; for item in iter { let (key, value) = match item { Ok(pair) => pair, Err(error) => { handle_dotenv_error(&error, &env_file_path, flags_log_level); break; } }; let key_os = OsString::from(key); if original_env_keys.contains(&key_os) || loaded_keys.contains(&key_os) { continue; } if is_denied_env_file_key(&key_os) { handle_denied_env_file_key(&key_os, &env_file_path, flags_log_level); continue; } process_env.set_var(&key_os, value); loaded_keys.insert(key_os); } } } pub fn handle_dotenv_error( error: &deno_dotenv::ParseError, file_path: &Path, log_level: Option<log::Level>, ) { #[allow(clippy::print_stderr, reason = "can't use log crate yet")] if log_level.map(|l| l >= log::Level::Info).unwrap_or(true) { eprintln!( "{} Failed parsing value '{}' at index {} within the specified environment file.\n at {}", colors::yellow("Warning"), error.line, error.index, file_path.display(), ) } } pub fn handle_dotenv_io_error( error: &deno_dotenv::FindPathAndContentError, log_level: Option<log::Level>, ) { #[allow(clippy::print_stderr, reason = "can't use log crate yet")] if log_level.map(|l| l >= log::Level::Info).unwrap_or(true) { eprintln!( "{} Error reading from environment file: {}\n at {}", colors::yellow("Warning"), error.source, error.path.display(), ) } } pub fn handle_dotenv_not_found(specifier: &str, log_level: Option<log::Level>) { #[allow(clippy::print_stderr, reason = "can't use log crate yet")] if log_level.map(|l| l >= log::Level::Info).unwrap_or(true) { eprintln!( "{} The `--env-file` flag was used, but the environment file specified '{}' was not found.", colors::yellow("Warning"), specifier, ) } }