Use unix pipe to capture environment variables (#32136)
The use of `NamedTempFile` in #31799 was not secure and could potentially cause write permission issues (see [this comment](https://github.com/zed-industries/zed/issues/29528#issuecomment-2939672433)). Therefore, it has been replaced with a Unix pipe. Release Notes: - N/A
This commit is contained in:
parent
ee2a329981
commit
3bed830a1f
5 changed files with 87 additions and 29 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
@ -3161,6 +3161,16 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "command-fds"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ec1052629a80c28594777d1252efc8a6b005d13f9edfd8c3fc0f44d5b32489a"
|
||||
dependencies = [
|
||||
"nix 0.30.1",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "command_palette"
|
||||
version = "0.1.0"
|
||||
|
@ -10132,6 +10142,18 @@ dependencies = [
|
|||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases 0.2.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "node_runtime"
|
||||
version = "0.1.0"
|
||||
|
@ -17124,6 +17146,7 @@ dependencies = [
|
|||
"async-fs",
|
||||
"async_zip",
|
||||
"collections",
|
||||
"command-fds",
|
||||
"dirs 4.0.0",
|
||||
"dunce",
|
||||
"futures 0.3.31",
|
||||
|
|
|
@ -249,7 +249,7 @@ async fn load_shell_environment(
|
|||
use util::shell_env;
|
||||
|
||||
let dir_ = dir.to_owned();
|
||||
let mut envs = match smol::unblock(move || shell_env::capture(Some(dir_))).await {
|
||||
let mut envs = match smol::unblock(move || shell_env::capture(&dir_)).await {
|
||||
Ok(envs) => envs,
|
||||
Err(err) => {
|
||||
util::log_err(&err);
|
||||
|
|
|
@ -42,6 +42,7 @@ walkdir.workspace = true
|
|||
workspace-hack.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
command-fds = "0.3.1"
|
||||
libc.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
#![cfg_attr(not(unix), allow(unused))]
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use std::borrow::Cow;
|
||||
use std::ffi::OsStr;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
/// Capture all environment variables from the login shell.
|
||||
pub fn capture(change_dir: Option<impl AsRef<Path>>) -> Result<HashMap<String, String>> {
|
||||
let shell_path = std::env::var("SHELL").map(PathBuf::from)?;
|
||||
let shell_name = shell_path.file_name().and_then(OsStr::to_str);
|
||||
#[cfg(unix)]
|
||||
pub fn capture(directory: &std::path::Path) -> Result<collections::HashMap<String, String>> {
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::Stdio;
|
||||
|
||||
let shell_path = std::env::var("SHELL").map(std::path::PathBuf::from)?;
|
||||
let shell_name = shell_path.file_name().and_then(std::ffi::OsStr::to_str);
|
||||
|
||||
let mut command = std::process::Command::new(&shell_path);
|
||||
command.stdin(Stdio::null());
|
||||
command.stdout(Stdio::piped());
|
||||
command.stderr(Stdio::piped());
|
||||
|
||||
let mut command_string = String::new();
|
||||
|
||||
|
@ -18,10 +23,7 @@ pub fn capture(change_dir: Option<impl AsRef<Path>>) -> Result<HashMap<String, S
|
|||
// the project directory to get the env in there as if the user
|
||||
// `cd`'d into it. We do that because tools like direnv, asdf, ...
|
||||
// hook into `cd` and only set up the env after that.
|
||||
if let Some(dir) = change_dir {
|
||||
let dir_str = dir.as_ref().to_string_lossy();
|
||||
command_string.push_str(&format!("cd '{dir_str}';"));
|
||||
}
|
||||
command_string.push_str(&format!("cd '{}';", directory.display()));
|
||||
|
||||
// In certain shells we need to execute additional_command in order to
|
||||
// trigger the behavior of direnv, etc.
|
||||
|
@ -30,26 +32,26 @@ pub fn capture(change_dir: Option<impl AsRef<Path>>) -> Result<HashMap<String, S
|
|||
_ => "",
|
||||
});
|
||||
|
||||
let mut env_output_file = NamedTempFile::new()?;
|
||||
command_string.push_str(&format!(
|
||||
"sh -c 'export -p' > '{}';",
|
||||
env_output_file.path().to_string_lossy(),
|
||||
));
|
||||
|
||||
let mut command = Command::new(&shell_path);
|
||||
// In some shells, file descriptors greater than 2 cannot be used in interactive mode,
|
||||
// so file descriptor 0 is used instead.
|
||||
const ENV_OUTPUT_FD: std::os::fd::RawFd = 0;
|
||||
command_string.push_str(&format!("sh -c 'export -p >&{ENV_OUTPUT_FD}';"));
|
||||
|
||||
// For csh/tcsh, the login shell option is set by passing `-` as
|
||||
// the 0th argument instead of using `-l`.
|
||||
if let Some("tcsh" | "csh") = shell_name {
|
||||
#[cfg(unix)]
|
||||
std::os::unix::process::CommandExt::arg0(&mut command, "-");
|
||||
command.arg0("-");
|
||||
} else {
|
||||
command.arg("-l");
|
||||
}
|
||||
|
||||
command.args(["-i", "-c", &command_string]);
|
||||
|
||||
let process_output = super::set_pre_exec_to_start_new_session(&mut command).output()?;
|
||||
super::set_pre_exec_to_start_new_session(&mut command);
|
||||
|
||||
let (env_output, process_output) = spawn_and_read_fd(command, ENV_OUTPUT_FD)?;
|
||||
let env_output = String::from_utf8_lossy(&env_output);
|
||||
|
||||
anyhow::ensure!(
|
||||
process_output.status.success(),
|
||||
"login shell exited with {}. stdout: {:?}, stderr: {:?}",
|
||||
|
@ -58,15 +60,36 @@ pub fn capture(change_dir: Option<impl AsRef<Path>>) -> Result<HashMap<String, S
|
|||
String::from_utf8_lossy(&process_output.stderr),
|
||||
);
|
||||
|
||||
let mut env_output = String::new();
|
||||
env_output_file.read_to_string(&mut env_output)?;
|
||||
|
||||
parse(&env_output)
|
||||
.filter_map(|entry| match entry {
|
||||
Ok((name, value)) => Some(Ok((name.into(), value?.into()))),
|
||||
Err(err) => Some(Err(err)),
|
||||
})
|
||||
.collect::<Result<HashMap<String, String>>>()
|
||||
.collect::<Result<_>>()
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn spawn_and_read_fd(
|
||||
mut command: std::process::Command,
|
||||
child_fd: std::os::fd::RawFd,
|
||||
) -> anyhow::Result<(Vec<u8>, std::process::Output)> {
|
||||
use command_fds::{CommandFdExt, FdMapping};
|
||||
use std::io::Read;
|
||||
|
||||
let (mut reader, writer) = std::io::pipe()?;
|
||||
|
||||
command.fd_mappings(vec![FdMapping {
|
||||
parent_fd: writer.into(),
|
||||
child_fd,
|
||||
}])?;
|
||||
|
||||
let process = command.spawn()?;
|
||||
drop(command);
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
reader.read_to_end(&mut buffer)?;
|
||||
|
||||
Ok((buffer, process.wait_with_output()?))
|
||||
}
|
||||
|
||||
/// Parse the result of calling `sh -c 'export -p'`.
|
||||
|
@ -154,6 +177,17 @@ fn parse_literal_double_quoted(input: &str) -> Option<(String, &str)> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_spawn_and_read_fd() -> anyhow::Result<()> {
|
||||
let mut command = std::process::Command::new("sh");
|
||||
super::super::set_pre_exec_to_start_new_session(&mut command);
|
||||
command.args(["-lic", "printf 'abc%.0s' $(seq 1 65536) >&0"]);
|
||||
let (bytes, _) = spawn_and_read_fd(command, 0)?;
|
||||
assert_eq!(bytes.len(), 65536 * 3);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse() {
|
||||
let input = indoc::indoc! {r#"
|
||||
|
|
|
@ -315,7 +315,7 @@ pub fn load_login_shell_environment() -> Result<()> {
|
|||
// into shell's `cd` command (and hooks) to manipulate env.
|
||||
// We do this so that we get the env a user would have when spawning a shell
|
||||
// in home directory.
|
||||
for (name, value) in shell_env::capture(Some(paths::home_dir()))? {
|
||||
for (name, value) in shell_env::capture(paths::home_dir())? {
|
||||
unsafe { env::set_var(&name, &value) };
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue