@@ -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",
@@ -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) };
}