util: Always use posix_spawn on macOS even with pre_exec hooks (#49090)

Jakub Konka and Cole Miller created

Here's some backstory:
* on macOS, @cole-miller and I noticed that since roughly Oct 2025, due
to some changes to latest macOS Tahoe, for any spawned child process we
needed to reset Mach exception ports
(https://github.com/zed-industries/zed/issues/36754 +
https://github.com/RemiKalbe/zed/commit/6e8f2d2ebe5a93753625a3026aeb996de8cb436b)
* the changes in that PR achieve that via `pre_exec` hook on
`std::process::Command` which then abandons `posix_spawn` syscall for
`fork` + `execve` dance on macOS (we tracked it down in Rust's std
implementation)
* as it turns out, `fork` + `execve` is pretty expensive on macOS
(apparently way more so than on other OSes like Linux) and `fork` takes
a process-wide lock on the allocator which is bad
* however, since we wanna reset exception ports on the child, the only
official way supported by Rust's std is to use `pre_exec` hook
* posix_spawn on macOS exposes this tho via a macOS specific extension
to that syscall `posix_spawnattr_setexceptionports_np` but there is no
way to use that via any standard interfaces in `std::process::Command`
* thus, it seemed like a good idea to instead create our own custom
Command wrapper that on non-macOS hosts is a zero-cost wrapper of
`smol::process::Command`, while on macOS we reimplement the minimum to
achieve `smol::process::Command`  with `posix_spawn` under-the-hood

Notably, this changeset improves git-blame in very large repos
significantly.

Release Notes:

- Fixed performance spawning child processes on macOS by always forcing
`posix_spawn` no matter what.

---------

Co-authored-by: Cole Miller <cole@zed.dev>

Change summary

crates/auto_update/src/auto_update.rs                   |  16 
crates/copilot/src/copilot.rs                           |   2 
crates/dap_adapters/src/go.rs                           |   2 
crates/dap_adapters/src/python.rs                       |   8 
crates/dev_container/src/devcontainer_api.rs            |  20 
crates/eval/src/instance.rs                             |   4 
crates/extension/src/extension_builder.rs               |  22 
crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs |   2 
crates/fs/src/fs.rs                                     |  10 
crates/git/src/blame.rs                                 |   4 
crates/git/src/commit.rs                                |   9 
crates/git/src/repository.rs                            | 100 
crates/gpui/src/platform/linux/platform.rs              |   4 
crates/gpui/src/platform/mac/platform.rs                |   4 
crates/inspector_ui/src/inspector.rs                    |   4 
crates/languages/src/go.rs                              |   6 
crates/languages/src/python.rs                          |  14 
crates/languages/src/rust.rs                            |  16 
crates/lsp/src/lsp.rs                                   |   6 
crates/node_runtime/src/node_runtime.rs                 |  10 
crates/project/src/debugger/locators/cargo.rs           |   8 
crates/project/src/debugger/session.rs                  |  10 
crates/project/src/environment.rs                       |   4 
crates/project/src/lsp_store.rs                         |  13 
crates/recent_projects/src/remote_connections.rs        |   2 
crates/remote/src/transport.rs                          |  23 
crates/remote/src/transport/docker.rs                   |  12 
crates/remote/src/transport/ssh.rs                      |  26 
crates/remote/src/transport/wsl.rs                      |  13 
crates/remote_server/src/server.rs                      |  12 
crates/repl/src/kernels/mod.rs                          |   2 
crates/repl/src/kernels/native_kernel.rs                |  15 
crates/repl/src/repl_editor.rs                          |   2 
crates/supermaven/src/supermaven.rs                     |  19 
crates/util/src/command.rs                              | 176 +
crates/util/src/command/darwin.rs                       | 825 +++++++++++
crates/util/src/shell_env.rs                            |   2 
crates/util/src/util.rs                                 |   2 
38 files changed, 1,136 insertions(+), 293 deletions(-)

Detailed changes

crates/auto_update/src/auto_update.rs 🔗

@@ -26,7 +26,7 @@ use std::{
     sync::Arc,
     time::{Duration, SystemTime},
 };
-use util::command::new_smol_command;
+use util::command::new_command;
 use workspace::Workspace;
 
 const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
@@ -127,7 +127,7 @@ impl Drop for MacOsUnmounter<'_> {
         let mount_path = mem::take(&mut self.mount_path);
         self.background_executor
             .spawn(async move {
-                let unmount_output = new_smol_command("hdiutil")
+                let unmount_output = new_command("hdiutil")
                     .args(["detach", "-force"])
                     .arg(&mount_path)
                     .output()
@@ -902,7 +902,7 @@ async fn install_release_linux(
         .await
         .context("failed to create directory into which to extract update")?;
 
-    let output = new_smol_command("tar")
+    let output = new_command("tar")
         .arg("-xzf")
         .arg(&downloaded_tar_gz)
         .arg("-C")
@@ -937,7 +937,7 @@ async fn install_release_linux(
         to = PathBuf::from(prefix);
     }
 
-    let output = new_smol_command("rsync")
+    let output = new_command("rsync")
         .args(["-av", "--delete"])
         .arg(&from)
         .arg(&to)
@@ -969,7 +969,7 @@ async fn install_release_macos(
     let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
 
     mounted_app_path.push("/");
-    let output = new_smol_command("hdiutil")
+    let output = new_command("hdiutil")
         .args(["attach", "-nobrowse"])
         .arg(&downloaded_dmg)
         .arg("-mountroot")
@@ -989,7 +989,7 @@ async fn install_release_macos(
         background_executor: cx.background_executor(),
     };
 
-    let output = new_smol_command("rsync")
+    let output = new_command("rsync")
         .args(["-av", "--delete"])
         .arg(&mounted_app_path)
         .arg(&running_app_path)
@@ -1020,7 +1020,7 @@ async fn cleanup_windows() -> Result<()> {
 }
 
 async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option<PathBuf>> {
-    let output = new_smol_command(downloaded_installer)
+    let output = new_command(downloaded_installer)
         .arg("/verysilent")
         .arg("/update=true")
         .arg("!desktopicon")
@@ -1058,7 +1058,7 @@ pub async fn finalize_auto_update_on_quit() {
             .parent()
             .map(|p| p.join("tools").join("auto_update_helper.exe"))
     {
-        let mut command = util::command::new_smol_command(helper);
+        let mut command = util::command::new_command(helper);
         command.arg("--launch");
         command.arg("false");
         if let Ok(mut cmd) = command.spawn() {

crates/copilot/src/copilot.rs 🔗

@@ -1402,7 +1402,7 @@ async fn ensure_node_version_for_copilot(node_path: &Path) -> anyhow::Result<()>
 
     log::info!("Checking Node.js version for Copilot at: {:?}", node_path);
 
-    let output = util::command::new_smol_command(node_path)
+    let output = util::command::new_command(node_path)
         .arg("--version")
         .output()
         .await

crates/dap_adapters/src/go.rs 🔗

@@ -429,7 +429,7 @@ impl DebugAdapter for GoDebugAdapter {
 
             let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
 
-            let install_output = util::command::new_smol_command(&go)
+            let install_output = util::command::new_command(&go)
                 .env("GO111MODULE", "on")
                 .env("GOBIN", &adapter_path)
                 .args(&["install", "github.com/go-delve/delve/cmd/dlv@latest"])

crates/dap_adapters/src/python.rs 🔗

@@ -20,7 +20,7 @@ use std::{
     ffi::OsStr,
     path::{Path, PathBuf},
 };
-use util::command::new_smol_command;
+use util::command::new_command;
 use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
 
 enum DebugpyLaunchMode<'a> {
@@ -121,7 +121,7 @@ impl PythonDebugAdapter {
         std::fs::create_dir_all(&download_dir)?;
         let venv_python = self.base_venv_path(toolchain, delegate).await?;
 
-        let installation_succeeded = util::command::new_smol_command(venv_python.as_ref())
+        let installation_succeeded = util::command::new_command(venv_python.as_ref())
             .args([
                 "-m",
                 "pip",
@@ -259,7 +259,7 @@ impl PythonDebugAdapter {
                 };
 
                 let debug_adapter_path = paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref());
-                let output = util::command::new_smol_command(&base_python)
+                let output = util::command::new_command(&base_python)
                     .args(["-m", "venv", "zed_base_venv"])
                     .current_dir(
                         &debug_adapter_path,
@@ -308,7 +308,7 @@ impl PythonDebugAdapter {
             // Try to detect situations where `python3` exists but is not a real Python interpreter.
             // Notably, on fresh Windows installs, `python3` is a shim that opens the Microsoft Store app
             // when run with no arguments, and just fails otherwise.
-            let Some(output) = new_smol_command(&path)
+            let Some(output) = new_command(&path)
                 .args(["-c", "print(1 + 2)"])
                 .output()
                 .await

crates/dev_container/src/devcontainer_api.rs 🔗

@@ -7,7 +7,8 @@ use std::{
 use node_runtime::NodeRuntime;
 use serde::Deserialize;
 use settings::DevContainerConnection;
-use smol::{fs, process::Command};
+use smol::fs;
+use util::command::Command;
 use util::rel_path::RelPath;
 use workspace::Workspace;
 use worktree::Snapshot;
@@ -73,13 +74,12 @@ pub(crate) struct DevContainerCli {
 impl DevContainerCli {
     fn command(&self, use_podman: bool) -> Command {
         let mut command = if let Some(node_runtime_path) = &self.node_runtime_path {
-            let mut command = util::command::new_smol_command(
-                node_runtime_path.as_os_str().display().to_string(),
-            );
+            let mut command =
+                util::command::new_command(node_runtime_path.as_os_str().display().to_string());
             command.arg(self.path.display().to_string());
             command
         } else {
-            util::command::new_smol_command(self.path.display().to_string())
+            util::command::new_command(self.path.display().to_string())
         };
 
         if use_podman {
@@ -297,9 +297,9 @@ fn dev_container_script() -> String {
 
 async fn check_for_docker(use_podman: bool) -> Result<(), DevContainerError> {
     let mut command = if use_podman {
-        util::command::new_smol_command("podman")
+        util::command::new_command("podman")
     } else {
-        util::command::new_smol_command("docker")
+        util::command::new_command("docker")
     };
     command.arg("--version");
 
@@ -315,7 +315,7 @@ async fn check_for_docker(use_podman: bool) -> Result<(), DevContainerError> {
 pub(crate) async fn ensure_devcontainer_cli(
     node_runtime: &NodeRuntime,
 ) -> Result<DevContainerCli, DevContainerError> {
-    let mut command = util::command::new_smol_command(&dev_container_cli());
+    let mut command = util::command::new_command(&dev_container_cli());
     command.arg("--version");
 
     if let Err(e) = command.output().await {
@@ -340,7 +340,7 @@ pub(crate) async fn ensure_devcontainer_cli(
         );
 
         let mut command =
-            util::command::new_smol_command(node_runtime_path.as_os_str().display().to_string());
+            util::command::new_command(node_runtime_path.as_os_str().display().to_string());
         command.arg(datadir_cli_path.display().to_string());
         command.arg("--version");
 
@@ -385,7 +385,7 @@ pub(crate) async fn ensure_devcontainer_cli(
         };
 
         let mut command =
-            util::command::new_smol_command(node_runtime_path.as_os_str().display().to_string());
+            util::command::new_command(node_runtime_path.as_os_str().display().to_string());
         command.arg(datadir_cli_path.display().to_string());
         command.arg("--version");
         if let Err(e) = command.output().await {

crates/eval/src/instance.rs 🔗

@@ -26,7 +26,7 @@ use std::{
     time::Duration,
 };
 use unindent::Unindent as _;
-use util::{ResultExt as _, command::new_smol_command, markdown::MarkdownCodeBlock};
+use util::{ResultExt as _, command::new_command, markdown::MarkdownCodeBlock};
 
 use crate::{
     AgentAppState, ToolMetrics,
@@ -1072,7 +1072,7 @@ pub fn repo_path_for_url(repos_dir: &Path, repo_url: &str) -> PathBuf {
 }
 
 pub async fn run_git(repo_path: &Path, args: &[&str]) -> Result<String> {
-    let output = new_smol_command("git")
+    let output = new_command("git")
         .current_dir(repo_path)
         .args(args)
         .output()

crates/extension/src/extension_builder.rs 🔗

@@ -11,10 +11,10 @@ use serde::Deserialize;
 use std::{
     env, fs, mem,
     path::{Path, PathBuf},
-    process::Stdio,
     str::FromStr,
     sync::Arc,
 };
+use util::command::Stdio;
 use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
 use wasmparser::Parser;
 
@@ -157,7 +157,7 @@ impl ExtensionBuilder {
             "compiling Rust crate for extension {}",
             extension_dir.display()
         );
-        let output = util::command::new_smol_command("cargo")
+        let output = util::command::new_command("cargo")
             .args(["build", "--target", RUST_TARGET])
             .args(options.release.then_some("--release"))
             .arg("--target-dir")
@@ -263,7 +263,7 @@ impl ExtensionBuilder {
             );
         } else {
             log::info!("compiling {grammar_name} parser");
-            let clang_output = util::command::new_smol_command(&clang_path)
+            let clang_output = util::command::new_command(&clang_path)
                 .args(["-fPIC", "-shared", "-Os"])
                 .arg(format!("-Wl,--export=tree_sitter_{grammar_name}"))
                 .arg("-o")
@@ -292,7 +292,7 @@ impl ExtensionBuilder {
         let git_dir = directory.join(".git");
 
         if directory.exists() {
-            let remotes_output = util::command::new_smol_command("git")
+            let remotes_output = util::command::new_command("git")
                 .arg("--git-dir")
                 .arg(&git_dir)
                 .args(["remote", "-v"])
@@ -316,7 +316,7 @@ impl ExtensionBuilder {
             fs::create_dir_all(directory).with_context(|| {
                 format!("failed to create grammar directory {}", directory.display(),)
             })?;
-            let init_output = util::command::new_smol_command("git")
+            let init_output = util::command::new_command("git")
                 .arg("init")
                 .current_dir(directory)
                 .output()
@@ -328,7 +328,7 @@ impl ExtensionBuilder {
                 );
             }
 
-            let remote_add_output = util::command::new_smol_command("git")
+            let remote_add_output = util::command::new_command("git")
                 .arg("--git-dir")
                 .arg(&git_dir)
                 .args(["remote", "add", "origin", url])
@@ -343,7 +343,7 @@ impl ExtensionBuilder {
             }
         }
 
-        let fetch_output = util::command::new_smol_command("git")
+        let fetch_output = util::command::new_command("git")
             .arg("--git-dir")
             .arg(&git_dir)
             .args(["fetch", "--depth", "1", "origin", rev])
@@ -351,7 +351,7 @@ impl ExtensionBuilder {
             .await
             .context("failed to execute `git fetch`")?;
 
-        let checkout_output = util::command::new_smol_command("git")
+        let checkout_output = util::command::new_command("git")
             .arg("--git-dir")
             .arg(&git_dir)
             .args(["checkout", rev])
@@ -379,7 +379,7 @@ impl ExtensionBuilder {
     }
 
     async fn install_rust_wasm_target_if_needed(&self) -> Result<()> {
-        let rustc_output = util::command::new_smol_command("rustc")
+        let rustc_output = util::command::new_command("rustc")
             .arg("--print")
             .arg("sysroot")
             .output()
@@ -397,7 +397,7 @@ impl ExtensionBuilder {
             return Ok(());
         }
 
-        let output = util::command::new_smol_command("rustup")
+        let output = util::command::new_command("rustup")
             .args(["target", "add", RUST_TARGET])
             .stderr(Stdio::piped())
             .stdout(Stdio::inherit())
@@ -452,7 +452,7 @@ impl ExtensionBuilder {
         log::info!("un-tarring wasi-sdk to {}", tar_out_dir.display());
 
         // Shell out to tar to extract the archive
-        let tar_output = util::command::new_smol_command("tar")
+        let tar_output = util::command::new_command("tar")
             .arg("-xzf")
             .arg(&tar_gz_path)
             .arg("-C")

crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs 🔗

@@ -868,7 +868,7 @@ impl process::Host for WasmState {
             self.capability_granter
                 .grant_exec(&command.command, &command.args)?;
 
-            let output = util::command::new_smol_command(command.command.as_str())
+            let output = util::command::new_command(command.command.as_str())
                 .args(&command.args)
                 .envs(command.env)
                 .output()

crates/fs/src/fs.rs 🔗

@@ -15,7 +15,7 @@ use gpui::Global;
 use gpui::ReadGlobal as _;
 use gpui::SharedString;
 use std::borrow::Cow;
-use util::command::new_smol_command;
+use util::command::new_command;
 
 #[cfg(unix)]
 use std::os::fd::{AsFd, AsRawFd};
@@ -516,7 +516,7 @@ impl Fs for RealFs {
 
         #[cfg(windows)]
         if smol::fs::metadata(&target).await?.is_dir() {
-            let status = new_smol_command("cmd")
+            let status = new_command("cmd")
                 .args(["/C", "mklink", "/J"])
                 .args([path, target.as_path()])
                 .status()
@@ -1057,7 +1057,7 @@ impl Fs for RealFs {
         abs_work_directory_path: &Path,
         fallback_branch_name: String,
     ) -> Result<()> {
-        let config = new_smol_command("git")
+        let config = new_command("git")
             .current_dir(abs_work_directory_path)
             .args(&["config", "--global", "--get", "init.defaultBranch"])
             .output()
@@ -1071,7 +1071,7 @@ impl Fs for RealFs {
             branch_name = Cow::Borrowed(fallback_branch_name.as_str());
         }
 
-        new_smol_command("git")
+        new_command("git")
             .current_dir(abs_work_directory_path)
             .args(&["init", "-b"])
             .arg(branch_name.trim())
@@ -1091,7 +1091,7 @@ impl Fs for RealFs {
 
         let _job_tracker = JobTracker::new(job_info, self.job_event_subscribers.clone());
 
-        let output = new_smol_command("git")
+        let output = new_command("git")
             .current_dir(abs_work_directory)
             .args(&["clone", repo_url])
             .output()

crates/git/src/blame.rs 🔗

@@ -5,12 +5,12 @@ use anyhow::{Context as _, Result};
 use collections::{HashMap, HashSet};
 use futures::AsyncWriteExt;
 use serde::{Deserialize, Serialize};
-use std::process::Stdio;
 use std::{ops::Range, path::Path};
 use text::{LineEnding, Rope};
 use time::OffsetDateTime;
 use time::UtcOffset;
 use time::macros::format_description;
+use util::command::Stdio;
 
 pub use git2 as libgit;
 
@@ -61,7 +61,7 @@ async fn run_git_blame(
     let mut child = {
         let span = ztracing::debug_span!("spawning git-blame command", path = path.as_unix_str());
         let _enter = span.enter();
-        util::command::new_smol_command(git_binary)
+        util::command::new_command(git_binary)
             .current_dir(working_directory)
             .arg("blame")
             .arg("--incremental")

crates/git/src/commit.rs 🔗

@@ -80,16 +80,15 @@ pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<Hash
 
 async fn get_messages_impl(working_directory: &Path, shas: &[Oid]) -> Result<Vec<String>> {
     const MARKER: &str = "<MARKER>";
-    let mut cmd = util::command::new_smol_command("git");
-    cmd.current_dir(working_directory)
+    let output = util::command::new_command("git")
+        .current_dir(working_directory)
         .arg("show")
         .arg("-s")
         .arg(format!("--format=%B{}", MARKER))
-        .args(shas.iter().map(ToString::to_string));
-    let output = cmd
+        .args(shas.iter().map(ToString::to_string))
         .output()
         .await
-        .with_context(|| format!("starting git blame process: {:?}", cmd))?;
+        .context("starting git show process")?;
     anyhow::ensure!(
         output.status.success(),
         "'git show' failed with error {:?}",

crates/git/src/repository.rs 🔗

@@ -21,7 +21,7 @@ use text::LineEnding;
 
 use std::collections::HashSet;
 use std::ffi::{OsStr, OsString};
-use std::process::{ExitStatus, Stdio};
+use std::process::ExitStatus;
 use std::str::FromStr;
 use std::{
     cmp::Ordering,
@@ -31,7 +31,7 @@ use std::{
 };
 use sum_tree::MapSeekTarget;
 use thiserror::Error;
-use util::command::new_smol_command;
+use util::command::{Stdio, new_command};
 use util::paths::PathStyle;
 use util::rel_path::RelPath;
 use util::{ResultExt, paths};
@@ -954,7 +954,7 @@ impl GitRepository for RealGitRepository {
         self.executor
             .spawn(async move {
                 let working_directory = working_directory?;
-                let output = new_smol_command(git_binary_path)
+                let output = new_command(git_binary_path)
                     .current_dir(&working_directory)
                     .args([
                         "--no-optional-locks",
@@ -993,7 +993,7 @@ impl GitRepository for RealGitRepository {
         };
         let git_binary_path = self.any_git_binary_path.clone();
         cx.background_spawn(async move {
-            let show_output = util::command::new_smol_command(&git_binary_path)
+            let show_output = util::command::new_command(&git_binary_path)
                 .current_dir(&working_directory)
                 .args([
                     "--no-optional-locks",
@@ -1016,7 +1016,7 @@ impl GitRepository for RealGitRepository {
             let changes = parse_git_diff_name_status(&show_stdout);
             let parent_sha = format!("{}^", commit);
 
-            let mut cat_file_process = util::command::new_smol_command(&git_binary_path)
+            let mut cat_file_process = util::command::new_command(&git_binary_path)
                 .current_dir(&working_directory)
                 .args(["--no-optional-locks", "cat-file", "--batch=%(objectsize)"])
                 .stdin(Stdio::piped())
@@ -1133,7 +1133,7 @@ impl GitRepository for RealGitRepository {
                 ResetMode::Soft => "--soft",
             };
 
-            let output = new_smol_command(&self.any_git_binary_path)
+            let output = new_command(&self.any_git_binary_path)
                 .envs(env.iter())
                 .current_dir(&working_directory?)
                 .args(["reset", mode_flag, &commit])
@@ -1162,7 +1162,7 @@ impl GitRepository for RealGitRepository {
                 return Ok(());
             }
 
-            let output = new_smol_command(&git_binary_path)
+            let output = new_command(&git_binary_path)
                 .current_dir(&working_directory?)
                 .envs(env.iter())
                 .args(["checkout", &commit, "--"])
@@ -1260,7 +1260,7 @@ impl GitRepository for RealGitRepository {
                 let mode = if is_executable { "100755" } else { "100644" };
 
                 if let Some(content) = content {
-                    let mut child = new_smol_command(&git_binary_path)
+                    let mut child = new_command(&git_binary_path)
                         .current_dir(&working_directory)
                         .envs(env.iter())
                         .args(["hash-object", "-w", "--stdin"])
@@ -1276,7 +1276,7 @@ impl GitRepository for RealGitRepository {
 
                     log::debug!("indexing SHA: {sha}, path {path:?}");
 
-                    let output = new_smol_command(&git_binary_path)
+                    let output = new_command(&git_binary_path)
                         .current_dir(&working_directory)
                         .envs(env.iter())
                         .args(["update-index", "--add", "--cacheinfo", mode, sha])
@@ -1291,7 +1291,7 @@ impl GitRepository for RealGitRepository {
                     );
                 } else {
                     log::debug!("removing path {path:?} from the index");
-                    let output = new_smol_command(&git_binary_path)
+                    let output = new_command(&git_binary_path)
                         .current_dir(&working_directory)
                         .envs(env.iter())
                         .args(["update-index", "--force-remove"])
@@ -1328,7 +1328,7 @@ impl GitRepository for RealGitRepository {
         self.executor
             .spawn(async move {
                 let working_directory = working_directory?;
-                let mut process = new_smol_command(&git_binary_path)
+                let mut process = new_command(&git_binary_path)
                     .current_dir(&working_directory)
                     .args([
                         "--no-optional-locks",
@@ -1391,7 +1391,7 @@ impl GitRepository for RealGitRepository {
         let args = git_status_args(path_prefixes);
         log::debug!("Checking for git status in {path_prefixes:?}");
         self.executor.spawn(async move {
-            let output = new_smol_command(&git_binary_path)
+            let output = new_command(&git_binary_path)
                 .current_dir(working_directory)
                 .args(args)
                 .output()
@@ -1434,7 +1434,7 @@ impl GitRepository for RealGitRepository {
 
         self.executor
             .spawn(async move {
-                let output = new_smol_command(&git_binary_path)
+                let output = new_command(&git_binary_path)
                     .current_dir(working_directory)
                     .args(args)
                     .output()
@@ -1455,7 +1455,7 @@ impl GitRepository for RealGitRepository {
         let working_directory = self.working_directory();
         self.executor
             .spawn(async move {
-                let output = new_smol_command(&git_binary_path)
+                let output = new_command(&git_binary_path)
                     .current_dir(working_directory?)
                     .args(&["stash", "list", "--pretty=format:%gd%x00%H%x00%ct%x00%s"])
                     .output()
@@ -1496,7 +1496,7 @@ impl GitRepository for RealGitRepository {
                     &fields,
                 ];
                 let working_directory = working_directory?;
-                let output = new_smol_command(&git_binary_path)
+                let output = new_command(&git_binary_path)
                     .current_dir(&working_directory)
                     .args(args)
                     .output()
@@ -1514,7 +1514,7 @@ impl GitRepository for RealGitRepository {
                 if branches.is_empty() {
                     let args = vec!["symbolic-ref", "--quiet", "HEAD"];
 
-                    let output = new_smol_command(&git_binary_path)
+                    let output = new_command(&git_binary_path)
                         .current_dir(&working_directory)
                         .args(args)
                         .output()
@@ -1544,7 +1544,7 @@ impl GitRepository for RealGitRepository {
         let working_directory = self.working_directory();
         self.executor
             .spawn(async move {
-                let output = new_smol_command(&git_binary_path)
+                let output = new_command(&git_binary_path)
                     .current_dir(working_directory?)
                     .args(&["--no-optional-locks", "worktree", "list", "--porcelain"])
                     .output()
@@ -1584,7 +1584,7 @@ impl GitRepository for RealGitRepository {
         }
         self.executor
             .spawn(async move {
-                let output = new_smol_command(&git_binary_path)
+                let output = new_command(&git_binary_path)
                     .current_dir(working_directory?)
                     .args(args)
                     .output()
@@ -1768,7 +1768,7 @@ impl GitRepository for RealGitRepository {
 
                 args.push("--");
 
-                let output = new_smol_command(&git_binary_path)
+                let output = new_command(&git_binary_path)
                     .current_dir(&working_directory)
                     .args(&args)
                     .arg(path.as_unix_str())
@@ -1824,7 +1824,7 @@ impl GitRepository for RealGitRepository {
                     DiffType::HeadToWorktree => None,
                 };
 
-                let output = new_smol_command(&git_binary_path)
+                let output = new_command(&git_binary_path)
                     .current_dir(&working_directory?)
                     .args(["diff"])
                     .args(args)
@@ -1851,7 +1851,7 @@ impl GitRepository for RealGitRepository {
         self.executor
             .spawn(async move {
                 if !paths.is_empty() {
-                    let output = new_smol_command(&git_binary_path)
+                    let output = new_command(&git_binary_path)
                         .current_dir(&working_directory?)
                         .envs(env.iter())
                         .args(["update-index", "--add", "--remove", "--"])
@@ -1880,7 +1880,7 @@ impl GitRepository for RealGitRepository {
         self.executor
             .spawn(async move {
                 if !paths.is_empty() {
-                    let output = new_smol_command(&git_binary_path)
+                    let output = new_command(&git_binary_path)
                         .current_dir(&working_directory?)
                         .envs(env.iter())
                         .args(["reset", "--quiet", "--"])
@@ -1908,7 +1908,7 @@ impl GitRepository for RealGitRepository {
         let git_binary_path = self.any_git_binary_path.clone();
         self.executor
             .spawn(async move {
-                let mut cmd = new_smol_command(&git_binary_path);
+                let mut cmd = new_command(&git_binary_path);
                 cmd.current_dir(&working_directory?)
                     .envs(env.iter())
                     .args(["stash", "push", "--quiet"])
@@ -1937,7 +1937,7 @@ impl GitRepository for RealGitRepository {
         let git_binary_path = self.any_git_binary_path.clone();
         self.executor
             .spawn(async move {
-                let mut cmd = new_smol_command(git_binary_path);
+                let mut cmd = new_command(git_binary_path);
                 let mut args = vec!["stash".to_string(), "pop".to_string()];
                 if let Some(index) = index {
                     args.push(format!("stash@{{{}}}", index));
@@ -1967,7 +1967,7 @@ impl GitRepository for RealGitRepository {
         let git_binary_path = self.any_git_binary_path.clone();
         self.executor
             .spawn(async move {
-                let mut cmd = new_smol_command(git_binary_path);
+                let mut cmd = new_command(git_binary_path);
                 let mut args = vec!["stash".to_string(), "apply".to_string()];
                 if let Some(index) = index {
                     args.push(format!("stash@{{{}}}", index));
@@ -1997,7 +1997,7 @@ impl GitRepository for RealGitRepository {
         let git_binary_path = self.any_git_binary_path.clone();
         self.executor
             .spawn(async move {
-                let mut cmd = new_smol_command(git_binary_path);
+                let mut cmd = new_command(git_binary_path);
                 let mut args = vec!["stash".to_string(), "drop".to_string()];
                 if let Some(index) = index {
                     args.push(format!("stash@{{{}}}", index));
@@ -2032,15 +2032,15 @@ impl GitRepository for RealGitRepository {
         // Note: Do not spawn this command on the background thread, it might pop open the credential helper
         // which we want to block on.
         async move {
-            let mut cmd = new_smol_command(git_binary_path);
+            let mut cmd = new_command(git_binary_path);
             cmd.current_dir(&working_directory?)
                 .envs(env.iter())
                 .args(["commit", "--quiet", "-m"])
                 .arg(&message.to_string())
                 .arg("--cleanup=strip")
                 .arg("--no-verify")
-                .stdout(smol::process::Stdio::piped())
-                .stderr(smol::process::Stdio::piped());
+                .stdout(Stdio::piped())
+                .stderr(Stdio::piped());
 
             if options.amend {
                 cmd.arg("--amend");
@@ -2079,7 +2079,7 @@ impl GitRepository for RealGitRepository {
         async move {
             let git_binary_path = git_binary_path.context("git not found on $PATH, can't push")?;
             let working_directory = working_directory?;
-            let mut command = new_smol_command(git_binary_path);
+            let mut command = new_command(git_binary_path);
             command
                 .envs(env.iter())
                 .current_dir(&working_directory)
@@ -2090,9 +2090,9 @@ impl GitRepository for RealGitRepository {
                 }))
                 .arg(remote_name)
                 .arg(format!("{}:{}", branch_name, remote_branch_name))
-                .stdin(smol::process::Stdio::null())
-                .stdout(smol::process::Stdio::piped())
-                .stderr(smol::process::Stdio::piped());
+                .stdin(Stdio::null())
+                .stdout(Stdio::piped())
+                .stderr(Stdio::piped());
 
             run_git_command(env, ask_pass, command, executor).await
         }
@@ -2115,7 +2115,7 @@ impl GitRepository for RealGitRepository {
         // which we want to block on.
         async move {
             let git_binary_path = git_binary_path.context("git not found on $PATH, can't pull")?;
-            let mut command = new_smol_command(git_binary_path);
+            let mut command = new_command(git_binary_path);
             command
                 .envs(env.iter())
                 .current_dir(&working_directory?)
@@ -2128,8 +2128,8 @@ impl GitRepository for RealGitRepository {
             command
                 .arg(remote_name)
                 .args(branch_name)
-                .stdout(smol::process::Stdio::piped())
-                .stderr(smol::process::Stdio::piped());
+                .stdout(Stdio::piped())
+                .stderr(Stdio::piped());
 
             run_git_command(env, ask_pass, command, executor).await
         }
@@ -2151,13 +2151,13 @@ impl GitRepository for RealGitRepository {
         // which we want to block on.
         async move {
             let git_binary_path = git_binary_path.context("git not found on $PATH, can't fetch")?;
-            let mut command = new_smol_command(git_binary_path);
+            let mut command = new_command(git_binary_path);
             command
                 .envs(env.iter())
                 .current_dir(&working_directory?)
                 .args(["fetch", &remote_name])
-                .stdout(smol::process::Stdio::piped())
-                .stderr(smol::process::Stdio::piped());
+                .stdout(Stdio::piped())
+                .stderr(Stdio::piped());
 
             run_git_command(env, ask_pass, command, executor).await
         }
@@ -2170,7 +2170,7 @@ impl GitRepository for RealGitRepository {
         self.executor
             .spawn(async move {
                 let working_directory = working_directory?;
-                let output = new_smol_command(&git_binary_path)
+                let output = new_command(&git_binary_path)
                     .current_dir(&working_directory)
                     .args(["rev-parse", "--abbrev-ref"])
                     .arg(format!("{branch}@{{push}}"))
@@ -2197,7 +2197,7 @@ impl GitRepository for RealGitRepository {
         self.executor
             .spawn(async move {
                 let working_directory = working_directory?;
-                let output = new_smol_command(&git_binary_path)
+                let output = new_command(&git_binary_path)
                     .current_dir(&working_directory)
                     .args(["config", "--get"])
                     .arg(format!("branch.{branch}.remote"))
@@ -2221,7 +2221,7 @@ impl GitRepository for RealGitRepository {
         self.executor
             .spawn(async move {
                 let working_directory = working_directory?;
-                let output = new_smol_command(&git_binary_path)
+                let output = new_command(&git_binary_path)
                     .current_dir(&working_directory)
                     .args(["remote", "-v"])
                     .output()
@@ -2280,7 +2280,7 @@ impl GitRepository for RealGitRepository {
             .spawn(async move {
                 let working_directory = working_directory?;
                 let git_cmd = async |args: &[&str]| -> Result<String> {
-                    let output = new_smol_command(&git_binary_path)
+                    let output = new_command(&git_binary_path)
                         .current_dir(&working_directory)
                         .args(args)
                         .output()
@@ -2540,7 +2540,7 @@ impl GitRepository for RealGitRepository {
             {
                 let hook_abs_path = repository.lock().path().join("hooks").join(hook.as_str());
                 if hook_abs_path.is_file() {
-                    let output = new_smol_command(&hook_abs_path)
+                    let output = new_command(&hook_abs_path)
                         .envs(env.iter())
                         .current_dir(&working_directory)
                         .output()
@@ -2706,8 +2706,8 @@ async fn run_commit_data_reader(
     Ok(())
 }
 
-async fn read_single_commit_response(
-    stdout: &mut BufReader<smol::process::ChildStdout>,
+async fn read_single_commit_response<R: smol::io::AsyncBufRead + Unpin>(
+    stdout: &mut R,
     sha: &Oid,
 ) -> Result<GraphCommitData> {
     let mut header_bytes = Vec::new();
@@ -2964,11 +2964,11 @@ impl GitBinary {
         Ok(String::from_utf8(output.stdout)?)
     }
 
-    fn build_command<S>(&self, args: impl IntoIterator<Item = S>) -> smol::process::Command
+    fn build_command<S>(&self, args: impl IntoIterator<Item = S>) -> util::command::Command
     where
         S: AsRef<OsStr>,
     {
-        let mut command = new_smol_command(&self.git_binary_path);
+        let mut command = new_command(&self.git_binary_path);
         command.current_dir(&self.working_directory);
         command.args(args);
         if let Some(index_file_path) = self.index_file_path.as_ref() {
@@ -2990,7 +2990,7 @@ struct GitBinaryCommandError {
 async fn run_git_command(
     env: Arc<HashMap<String, String>>,
     ask_pass: AskPassDelegate,
-    mut command: smol::process::Command,
+    mut command: util::command::Command,
     executor: BackgroundExecutor,
 ) -> Result<RemoteCommandOutput> {
     if env.contains_key("GIT_ASKPASS") {
@@ -3019,7 +3019,7 @@ async fn run_git_command(
 
 async fn run_askpass_command(
     mut ask_pass: AskPassSession,
-    git_process: smol::process::Child,
+    git_process: util::command::Child,
 ) -> anyhow::Result<RemoteCommandOutput> {
     select_biased! {
         result = ask_pass.run().fuse() => {

crates/gpui/src/platform/linux/platform.rs 🔗

@@ -17,7 +17,7 @@ use anyhow::{Context as _, anyhow};
 use calloop::LoopSignal;
 use futures::channel::oneshot;
 use util::ResultExt as _;
-use util::command::{new_smol_command, new_std_command};
+use util::command::{new_command, new_std_command};
 #[cfg(any(feature = "wayland", feature = "x11"))]
 use xkbcommon::xkb::{self, Keycode, Keysym, State};
 
@@ -475,7 +475,7 @@ impl<P: LinuxClient + 'static> Platform for P {
         let path = path.to_owned();
         self.background_executor()
             .spawn(async move {
-                let _ = new_smol_command("xdg-open")
+                let _ = new_command("xdg-open")
                     .arg(path)
                     .spawn()
                     .context("invoking xdg-open")

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -54,7 +54,7 @@ use std::{
 };
 use util::{
     ResultExt,
-    command::{new_smol_command, new_std_command},
+    command::{new_command, new_std_command},
 };
 
 #[allow(non_upper_case_globals)]
@@ -848,7 +848,7 @@ impl Platform for MacPlatform {
             .lock()
             .background_executor
             .spawn(async move {
-                if let Some(mut child) = new_smol_command("open")
+                if let Some(mut child) = new_command("open")
                     .arg(path)
                     .spawn()
                     .context("invoking open command")

crates/inspector_ui/src/inspector.rs 🔗

@@ -2,7 +2,7 @@ use anyhow::{Context as _, anyhow};
 use gpui::{App, DivInspectorState, Inspector, InspectorElementId, IntoElement, Window};
 use std::{cell::OnceCell, path::Path, sync::Arc};
 use ui::{Label, Tooltip, prelude::*, utils::platform_title_bar_height};
-use util::{ResultExt as _, command::new_smol_command};
+use util::{ResultExt as _, command::new_command};
 use workspace::AppState;
 
 use crate::div_inspector::DivInspector;
@@ -169,7 +169,7 @@ async fn open_zed_source_location(
         location.column()
     );
 
-    let output = new_smol_command("zed")
+    let output = new_command("zed")
         .arg(&path_arg)
         .output()
         .await

crates/languages/src/go.rs 🔗

@@ -111,7 +111,7 @@ impl LspInstaller for GoLspAdapter {
         delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let go = delegate.which("go".as_ref()).await.unwrap_or("go".into());
-        let go_version_output = util::command::new_smol_command(&go)
+        let go_version_output = util::command::new_command(&go)
             .args(["version"])
             .output()
             .await
@@ -140,7 +140,7 @@ impl LspInstaller for GoLspAdapter {
 
         let gobin_dir = container_dir.join("gobin");
         fs::create_dir_all(&gobin_dir).await?;
-        let install_output = util::command::new_smol_command(go)
+        let install_output = util::command::new_command(go)
             .env("GO111MODULE", "on")
             .env("GOBIN", &gobin_dir)
             .args(["install", "golang.org/x/tools/gopls@latest"])
@@ -159,7 +159,7 @@ impl LspInstaller for GoLspAdapter {
         }
 
         let installed_binary_path = gobin_dir.join(BINARY);
-        let version_output = util::command::new_smol_command(&installed_binary_path)
+        let version_output = util::command::new_command(&installed_binary_path)
             .arg("version")
             .output()
             .await

crates/languages/src/python.rs 🔗

@@ -30,9 +30,9 @@ use terminal::terminal_settings::TerminalSettings;
 use smol::lock::OnceCell;
 use std::cmp::{Ordering, Reverse};
 use std::env::consts;
-use std::process::Stdio;
+use util::command::Stdio;
 
-use util::command::new_smol_command;
+use util::command::new_command;
 use util::fs::{make_file_executable, remove_matching};
 use util::paths::PathStyle;
 use util::rel_path::RelPath;
@@ -1620,7 +1620,7 @@ impl PyLspAdapter {
         let mut path = PathBuf::from(work_dir.as_ref());
         path.push("pylsp-venv");
         if !path.exists() {
-            util::command::new_smol_command(python_path)
+            util::command::new_command(python_path)
                 .arg("-m")
                 .arg("venv")
                 .arg("pylsp-venv")
@@ -1641,7 +1641,7 @@ impl PyLspAdapter {
             // Try to detect situations where `python3` exists but is not a real Python interpreter.
             // Notably, on fresh Windows installs, `python3` is a shim that opens the Microsoft Store app
             // when run with no arguments, and just fails otherwise.
-            let Some(output) = new_smol_command(&path)
+            let Some(output) = new_command(&path)
                 .args(["-c", "print(1 + 2)"])
                 .output()
                 .await
@@ -1853,7 +1853,7 @@ impl LspInstaller for PyLspAdapter {
         let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?;
         let pip_path = venv.join(BINARY_DIR).join("pip3");
         ensure!(
-            util::command::new_smol_command(pip_path.as_path())
+            util::command::new_command(pip_path.as_path())
                 .arg("install")
                 .arg("python-lsp-server[all]")
                 .arg("--upgrade")
@@ -1864,7 +1864,7 @@ impl LspInstaller for PyLspAdapter {
             "python-lsp-server[all] installation failed"
         );
         ensure!(
-            util::command::new_smol_command(pip_path)
+            util::command::new_command(pip_path)
                 .arg("install")
                 .arg("pylsp-mypy")
                 .arg("--upgrade")
@@ -2421,7 +2421,7 @@ impl LspAdapter for RuffLspAdapter {
             .0
             .ok()?;
 
-        let mut command = util::command::new_smol_command(&binary.path);
+        let mut command = util::command::new_command(&binary.path);
         command
             .args(&["config", "--output-format", "json"])
             .stdout(Stdio::piped())

crates/languages/src/rust.rs 🔗

@@ -19,13 +19,13 @@ use smol::fs::{self};
 use std::cmp::Reverse;
 use std::fmt::Display;
 use std::ops::Range;
-use std::process::Stdio;
 use std::{
     borrow::Cow,
     path::{Path, PathBuf},
     sync::{Arc, LazyLock},
 };
 use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
+use util::command::Stdio;
 use util::fs::{make_file_executable, remove_matching};
 use util::merge_json_value_into;
 use util::rel_path::RelPath;
@@ -144,13 +144,9 @@ impl RustLspAdapter {
         use futures::pin_mut;
 
         async fn from_ldd_version() -> Option<LibcType> {
-            use util::command::new_smol_command;
+            use util::command::new_command;
 
-            let ldd_output = new_smol_command("ldd")
-                .arg("--version")
-                .output()
-                .await
-                .ok()?;
+            let ldd_output = new_command("ldd").arg("--version").output().await.ok()?;
             let ldd_version = String::from_utf8_lossy(&ldd_output.stdout);
 
             if ldd_version.contains("GNU libc") || ldd_version.contains("GLIBC") {
@@ -543,7 +539,7 @@ impl LspAdapter for RustLspAdapter {
             .0
             .ok()?;
 
-        let mut command = util::command::new_smol_command(&binary.path);
+        let mut command = util::command::new_command(&binary.path);
         command
             .arg("--print-config-schema")
             .stdout(Stdio::piped())
@@ -1140,7 +1136,7 @@ async fn target_info_from_abs_path(
     abs_path: &Path,
     project_env: Option<&HashMap<String, String>>,
 ) -> Option<(Option<TargetInfo>, Arc<Path>)> {
-    let mut command = util::command::new_smol_command("cargo");
+    let mut command = util::command::new_command("cargo");
     if let Some(envs) = project_env {
         command.envs(envs);
     }
@@ -1215,7 +1211,7 @@ async fn human_readable_package_name(
     package_directory: &Path,
     project_env: Option<&HashMap<String, String>>,
 ) -> Option<String> {
-    let mut command = util::command::new_smol_command("cargo");
+    let mut command = util::command::new_command("cargo");
     if let Some(envs) = project_env {
         command.envs(envs);
     }

crates/lsp/src/lsp.rs 🔗

@@ -22,9 +22,10 @@ use serde_json::{Value, json, value::RawValue};
 use smol::{
     channel,
     io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
-    process::Child,
 };
+use util::command::{Child, Stdio};
 
+use std::path::Path;
 use std::{
     any::TypeId,
     collections::BTreeSet,
@@ -41,7 +42,6 @@ use std::{
     task::Poll,
     time::{Duration, Instant},
 };
-use std::{path::Path, process::Stdio};
 use util::{ConnectionResult, ResultExt, TryFutureExt, redact};
 
 const JSON_RPC_VERSION: &str = "2.0";
@@ -418,7 +418,7 @@ impl LanguageServer {
             working_dir,
             &binary.arguments
         );
-        let mut command = util::command::new_smol_command(&binary.path);
+        let mut command = util::command::new_command(&binary.path);
         command
             .current_dir(working_dir)
             .args(&binary.arguments)

crates/node_runtime/src/node_runtime.rs 🔗

@@ -427,7 +427,7 @@ impl ManagedNodeRuntime {
         let node_ca_certs = env::var(NODE_CA_CERTS_ENV_VAR).unwrap_or_else(|_| String::new());
 
         let valid = if fs::metadata(&node_binary).await.is_ok() {
-            let result = util::command::new_smol_command(&node_binary)
+            let result = util::command::new_command(&node_binary)
                 .env(NODE_CA_CERTS_ENV_VAR, node_ca_certs)
                 .arg(npm_file)
                 .arg("--version")
@@ -555,7 +555,7 @@ impl NodeRuntimeTrait for ManagedNodeRuntime {
     ) -> Result<Output> {
         let attempt = || async {
             let npm_command = self.npm_command(proxy, subcommand, args).await?;
-            let mut command = util::command::new_smol_command(npm_command.path);
+            let mut command = util::command::new_command(npm_command.path);
             command.args(npm_command.args);
             command.envs(npm_command.env);
             configure_npm_command(&mut command, directory);
@@ -640,7 +640,7 @@ pub struct SystemNodeRuntime {
 impl SystemNodeRuntime {
     const MIN_VERSION: semver::Version = Version::new(22, 0, 0);
     async fn new(node: PathBuf, npm: PathBuf) -> Result<Self> {
-        let output = util::command::new_smol_command(&node)
+        let output = util::command::new_command(&node)
             .arg("--version")
             .output()
             .await
@@ -723,7 +723,7 @@ impl NodeRuntimeTrait for SystemNodeRuntime {
         args: &[&str],
     ) -> anyhow::Result<Output> {
         let npm_command = self.npm_command(proxy, subcommand, args).await?;
-        let mut command = util::command::new_smol_command(npm_command.path);
+        let mut command = util::command::new_command(npm_command.path);
         command.args(npm_command.args);
         command.envs(npm_command.env);
         configure_npm_command(&mut command, directory);
@@ -841,7 +841,7 @@ impl NodeRuntimeTrait for UnavailableNodeRuntime {
     }
 }
 
-fn configure_npm_command(command: &mut smol::process::Command, directory: Option<&Path>) {
+fn configure_npm_command(command: &mut util::command::Command, directory: Option<&Path>) {
     if let Some(directory) = directory {
         command.current_dir(directory);
         command.args(["--prefix".into(), directory.to_path_buf()]);

crates/project/src/debugger/locators/cargo.rs 🔗

@@ -3,10 +3,10 @@ use async_trait::async_trait;
 use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
 use gpui::{BackgroundExecutor, SharedString};
 use serde_json::{Value, json};
-use smol::{io::AsyncReadExt, process::Stdio};
+use smol::{io::AsyncReadExt, process::Stdio as SmolStdio};
 use std::time::Duration;
 use task::{BuildTaskDefinition, DebugScenario, ShellBuilder, SpawnInTerminal, TaskTemplate};
-use util::command::new_smol_command;
+use util::command::{Stdio, new_command};
 
 pub(crate) struct CargoLocator;
 
@@ -19,7 +19,7 @@ async fn find_best_executable(
         return executables.first().cloned();
     }
     for executable in executables {
-        let Some(mut child) = new_smol_command(&executable)
+        let Some(mut child) = new_command(&executable)
             .arg("--list")
             .stdout(Stdio::piped())
             .spawn()
@@ -136,7 +136,7 @@ impl DapLocator for CargoLocator {
             )
             .envs(build_config.env.iter().map(|(k, v)| (k.clone(), v.clone())))
             .current_dir(cwd)
-            .stdout(Stdio::piped())
+            .stdout(SmolStdio::piped())
             .spawn()?;
 
         let mut output = String::new();

crates/project/src/debugger/session.rs 🔗

@@ -51,7 +51,6 @@ use std::collections::{BTreeMap, VecDeque};
 use std::net::Ipv4Addr;
 use std::ops::RangeInclusive;
 use std::path::PathBuf;
-use std::process::Stdio;
 use std::time::Duration;
 use std::u64;
 use std::{
@@ -64,7 +63,8 @@ use std::{
 use task::SharedTaskContext;
 use text::{PointUtf16, ToPointUtf16};
 use url::Url;
-use util::command::new_smol_command;
+use util::command::Stdio;
+use util::command::new_command;
 use util::{ResultExt, debug_panic, maybe};
 use worktree::Worktree;
 
@@ -2883,7 +2883,7 @@ impl Session {
 
                 let child = remote_client.update(cx, |client, _| {
                     let command = client.build_forward_ports_command(port_forwards)?;
-                    let child = new_smol_command(command.program)
+                    let child = new_command(command.program)
                         .args(command.args)
                         .envs(command.env)
                         .spawn()
@@ -3067,7 +3067,7 @@ struct KillCompanionBrowserParams {
 async fn spawn_companion(
     node_runtime: NodeRuntime,
     cx: &mut AsyncApp,
-) -> Result<(u16, smol::process::Child)> {
+) -> Result<(u16, util::command::Child)> {
     let binary_path = node_runtime
         .binary_path()
         .await
@@ -3089,7 +3089,7 @@ async fn spawn_companion(
         .to_string_lossy()
         .to_string();
 
-    let child = new_smol_command(binary_path)
+    let child = new_command(binary_path)
         .arg(path)
         .args([
             format!("--listen=127.0.0.1:{port}"),

crates/project/src/environment.rs 🔗

@@ -6,7 +6,7 @@ use rpc::proto::{self, REMOTE_SERVER_PROJECT_ID};
 use std::{collections::VecDeque, path::Path, sync::Arc};
 use task::{Shell, shell_to_proto};
 use terminal::terminal_settings::TerminalSettings;
-use util::{ResultExt, command::new_smol_command, rel_path::RelPath};
+use util::{ResultExt, command::new_command, rel_path::RelPath};
 use worktree::Worktree;
 
 use collections::HashMap;
@@ -402,7 +402,7 @@ async fn load_direnv_environment(
     };
 
     let args = &["export", "json"];
-    let direnv_output = new_smol_command(&direnv_path)
+    let direnv_output = new_command(&direnv_path)
         .args(args)
         .envs(env)
         .env("TERM", "dumb")

crates/project/src/lsp_store.rs 🔗

@@ -2373,7 +2373,8 @@ impl LocalLspStore {
             Some(worktree_path)
         });
 
-        let mut child = util::command::new_smol_command(command);
+        use util::command::Stdio;
+        let mut child = util::command::new_command(command);
 
         if let Some(buffer_env) = buffer.env.as_ref() {
             child.envs(buffer_env);
@@ -2394,9 +2395,9 @@ impl LocalLspStore {
         }
 
         let mut child = child
-            .stdin(smol::process::Stdio::piped())
-            .stdout(smol::process::Stdio::piped())
-            .stderr(smol::process::Stdio::piped())
+            .stdin(Stdio::piped())
+            .stdout(Stdio::piped())
+            .stderr(Stdio::piped())
             .spawn()?;
 
         let stdin = child.stdin.as_mut().context("failed to acquire stdin")?;
@@ -14100,7 +14101,7 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate {
         };
 
         let env = self.shell_env().await;
-        let output = util::command::new_smol_command(&npm)
+        let output = util::command::new_command(&npm)
             .args(["root", "-g"])
             .envs(env)
             .current_dir(local_package_directory)
@@ -14135,7 +14136,7 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate {
         if self.fs.is_file(&working_dir).await {
             working_dir.pop();
         }
-        let output = util::command::new_smol_command(&command.path)
+        let output = util::command::new_command(&command.path)
             .args(command.arguments)
             .envs(command.env.clone().unwrap_or_default())
             .current_dir(working_dir)

crates/recent_projects/src/remote_connections.rs 🔗

@@ -422,7 +422,7 @@ async fn path_exists(connection: &Arc<dyn RemoteConnection>, path: &Path) -> boo
     ) else {
         return false;
     };
-    let Ok(mut child) = util::command::new_smol_command(command.program)
+    let Ok(mut child) = util::command::new_command(command.program)
         .args(command.args)
         .envs(command.env)
         .spawn()

crates/remote/src/transport.rs 🔗

@@ -10,7 +10,7 @@ use futures::{
 };
 use gpui::{AppContext as _, AsyncApp, Task};
 use rpc::proto::Envelope;
-use smol::process::Child;
+use util::command::Child;
 
 pub mod docker;
 #[cfg(any(test, feature = "test-support"))]
@@ -181,10 +181,9 @@ async fn build_remote_server_from_source(
     binary_exists_on_server: bool,
     cx: &mut AsyncApp,
 ) -> Result<Option<std::path::PathBuf>> {
-    use smol::process::{Command, Stdio};
     use std::env::VarError;
     use std::path::Path;
-    use util::command::new_smol_command;
+    use util::command::{Command, Stdio, new_command};
 
     if let Ok(path) = std::env::var("ZED_COPY_REMOTE_SERVER") {
         let path = std::path::PathBuf::from(path);
@@ -267,7 +266,7 @@ async fn build_remote_server_from_source(
         delegate.set_status(Some("Building remote server binary from source"), cx);
         log::info!("building remote server binary from source");
         run_cmd(
-            new_smol_command("cargo")
+            new_command("cargo")
                 .current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
                 .args([
                     "build",
@@ -297,18 +296,12 @@ async fn build_remote_server_from_source(
             .context("rustup not found on $PATH, install rustup (see https://rustup.rs/)")?;
         delegate.set_status(Some("Adding rustup target for cross-compilation"), cx);
         log::info!("adding rustup target");
-        run_cmd(
-            new_smol_command(rustup)
-                .args(["target", "add"])
-                .arg(&triple),
-        )
-        .await?;
+        run_cmd(new_command(rustup).args(["target", "add"]).arg(&triple)).await?;
 
         if which("cargo-zigbuild", cx).await?.is_none() {
             delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx);
             log::info!("installing cargo-zigbuild");
-            run_cmd(new_smol_command("cargo").args(["install", "--locked", "cargo-zigbuild"]))
-                .await?;
+            run_cmd(new_command("cargo").args(["install", "--locked", "cargo-zigbuild"])).await?;
         }
 
         delegate.set_status(
@@ -319,7 +312,7 @@ async fn build_remote_server_from_source(
         );
         log::info!("building remote binary from source for {triple} with Zig");
         run_cmd(
-            new_smol_command("cargo")
+            new_command("cargo")
                 .args([
                     "zigbuild",
                     "--package",
@@ -347,7 +340,7 @@ async fn build_remote_server_from_source(
 
         #[cfg(not(target_os = "windows"))]
         let archive_path = {
-            run_cmd(new_smol_command("gzip").arg("-f").arg(&bin_path)).await?;
+            run_cmd(new_command("gzip").arg("-f").arg(&bin_path)).await?;
             bin_path.with_extension("gz")
         };
 
@@ -362,7 +355,7 @@ async fn build_remote_server_from_source(
                 bin_path.display(),
                 zip_path.display(),
             );
-            run_cmd(new_smol_command("powershell.exe").args([
+            run_cmd(new_command("powershell.exe").args([
                 "-NoProfile",
                 "-Command",
                 &compress_command,

crates/remote/src/transport/docker.rs 🔗

@@ -9,10 +9,10 @@ use semver::Version as SemanticVersion;
 use std::time::Instant;
 use std::{
     path::{Path, PathBuf},
-    process::Stdio,
     sync::Arc,
 };
 use util::ResultExt;
+use util::command::Stdio;
 use util::shell::ShellKind;
 use util::{
     paths::{PathStyle, RemotePathBuf},
@@ -400,7 +400,7 @@ impl DockerExecConnection {
         src_path: String,
         dst_path: String,
     ) -> Result<()> {
-        let mut command = util::command::new_smol_command(&docker_cli);
+        let mut command = util::command::new_command(&docker_cli);
         command.arg("cp");
         command.arg("-a");
         command.arg(&src_path);
@@ -419,7 +419,7 @@ impl DockerExecConnection {
             );
         }
 
-        let mut chown_command = util::command::new_smol_command(&docker_cli);
+        let mut chown_command = util::command::new_command(&docker_cli);
         chown_command.arg("exec");
         chown_command.arg(connection_options.container_id);
         chown_command.arg("chown");
@@ -469,7 +469,7 @@ impl DockerExecConnection {
         subcommand: &str,
         args: &[impl AsRef<str>],
     ) -> Result<String> {
-        let mut command = util::command::new_smol_command(self.docker_cli());
+        let mut command = util::command::new_command(self.docker_cli());
         command.arg(subcommand);
         for arg in args {
             command.arg(arg.as_ref());
@@ -589,7 +589,7 @@ impl DockerExecConnection {
 
     fn kill_inner(&self) -> Result<()> {
         if let Some(pid) = self.proxy_process.lock().take() {
-            if let Ok(_) = util::command::new_smol_command("kill")
+            if let Ok(_) = util::command::new_command("kill")
                 .arg(pid.to_string())
                 .spawn()
             {
@@ -658,7 +658,7 @@ impl RemoteConnection for DockerExecConnection {
         if reconnect {
             docker_args.push("--reconnect".to_string());
         }
-        let mut command = util::command::new_smol_command(self.docker_cli());
+        let mut command = util::command::new_command(self.docker_cli());
         command
             .kill_on_drop(true)
             .stdin(Stdio::piped())

crates/remote/src/transport/ssh.rs 🔗

@@ -18,10 +18,7 @@ use release_channel::{AppVersion, ReleaseChannel};
 use rpc::proto::Envelope;
 use semver::Version;
 pub use settings::SshPortForwardOption;
-use smol::{
-    fs,
-    process::{self, Child, Stdio},
-};
+use smol::fs;
 use std::{
     net::IpAddr,
     path::{Path, PathBuf},
@@ -29,6 +26,7 @@ use std::{
     time::Instant,
 };
 use tempfile::TempDir;
+use util::command::{Child, Stdio};
 use util::{
     paths::{PathStyle, RemotePathBuf},
     rel_path::RelPath,
@@ -157,7 +155,7 @@ impl MasterProcess {
             "-o",
         ];
 
-        let mut master_process = util::command::new_smol_command("ssh");
+        let mut master_process = util::command::new_command("ssh");
         master_process
             .kill_on_drop(true)
             .stdin(Stdio::null())
@@ -206,7 +204,7 @@ impl MasterProcess {
             &format!("echo '{}'; exec $0", Self::CONNECTION_ESTABLISHED_MAGIC),
         ];
 
-        let mut master_process = util::command::new_smol_command("ssh");
+        let mut master_process = util::command::new_command("ssh");
         master_process
             .kill_on_drop(true)
             .stdin(Stdio::null())
@@ -992,8 +990,8 @@ impl SshRemoteConnection {
         src_path: &Path,
         dest_path_str: &str,
         args: Option<&[&str]>,
-    ) -> process::Command {
-        let mut command = util::command::new_smol_command("scp");
+    ) -> util::command::Command {
+        let mut command = util::command::new_command("scp");
         self.socket.ssh_options(&mut command, false).args(
             self.socket
                 .connection_options
@@ -1012,8 +1010,8 @@ impl SshRemoteConnection {
         command
     }
 
-    fn build_sftp_command(&self) -> process::Command {
-        let mut command = util::command::new_smol_command("sftp");
+    fn build_sftp_command(&self) -> util::command::Command {
+        let mut command = util::command::new_command("sftp");
         self.socket.ssh_options(&mut command, false).args(
             self.socket
                 .connection_options
@@ -1133,8 +1131,8 @@ impl SshSocket {
         program: &str,
         args: &[impl AsRef<str>],
         allow_pseudo_tty: bool,
-    ) -> process::Command {
-        let mut command = util::command::new_smol_command("ssh");
+    ) -> util::command::Command {
+        let mut command = util::command::new_command("ssh");
         let program = shell_kind.prepend_command_prefix(program);
         let mut to_run = shell_kind
             .try_quote_prefix_aware(&program)
@@ -1185,9 +1183,9 @@ impl SshSocket {
 
     fn ssh_options<'a>(
         &self,
-        command: &'a mut process::Command,
+        command: &'a mut util::command::Command,
         include_port_forwards: bool,
-    ) -> &'a mut process::Command {
+    ) -> &'a mut util::command::Command {
         let args = if include_port_forwards {
             self.connection_options.additional_args()
         } else {

crates/remote/src/transport/wsl.rs 🔗

@@ -11,16 +11,17 @@ use gpui::{App, AppContext as _, AsyncApp, Task};
 use release_channel::{AppVersion, ReleaseChannel};
 use rpc::proto::Envelope;
 use semver::Version;
-use smol::{fs, process};
+use smol::fs;
 use std::{
     ffi::OsStr,
     fmt::Write as _,
     path::{Path, PathBuf},
-    process::Stdio,
     sync::Arc,
     time::Instant,
 };
+
 use util::{
+    command::Stdio,
     paths::{PathStyle, RemotePathBuf},
     rel_path::RelPath,
     shell::{Shell, ShellKind},
@@ -595,7 +596,9 @@ pub fn wsl_path_to_windows_path(
     }
 }
 
-fn run_wsl_command_impl(mut command: process::Command) -> impl Future<Output = Result<String>> {
+fn run_wsl_command_impl(
+    mut command: util::command::Command,
+) -> impl Future<Output = Result<String>> {
     async move {
         let output = command
             .output()
@@ -622,8 +625,8 @@ fn wsl_command_impl(
     program: &str,
     args: &[impl AsRef<OsStr>],
     exec: bool,
-) -> process::Command {
-    let mut command = util::command::new_smol_command("wsl.exe");
+) -> util::command::Command {
+    let mut command = util::command::new_command("wsl.exe");
 
     if let Some(user) = &options.user {
         command.arg("--user").arg(user);

crates/remote_server/src/server.rs 🔗

@@ -56,7 +56,7 @@ use std::{
     sync::{Arc, LazyLock},
 };
 use thiserror::Error;
-use util::{ResultExt, command::new_smol_command};
+use util::{ResultExt, command::new_command};
 
 #[derive(Subcommand)]
 pub enum Commands {
@@ -945,11 +945,11 @@ fn spawn_server_windows(binary_name: &Path, paths: &ServerPaths) -> Result<(), S
 
 #[cfg(not(windows))]
 fn spawn_server_normal(binary_name: &Path, paths: &ServerPaths) -> Result<(), SpawnServerError> {
-    let mut server_process = new_smol_command(binary_name);
+    let mut server_process = new_command(binary_name);
     server_process
-        .stdin(std::process::Stdio::null())
-        .stdout(std::process::Stdio::null())
-        .stderr(std::process::Stdio::null())
+        .stdin(util::command::Stdio::null())
+        .stdout(util::command::Stdio::null())
+        .stderr(util::command::Stdio::null())
         .arg("run")
         .arg("--log-file")
         .arg(&paths.log_file)
@@ -977,7 +977,7 @@ pub struct CheckPidError {
     pid: u32,
 }
 async fn check_server_running(pid: u32) -> std::io::Result<bool> {
-    new_smol_command("kill")
+    new_command("kill")
         .arg("-0")
         .arg(pid.to_string())
         .output()

crates/repl/src/kernels/mod.rs 🔗

@@ -190,7 +190,7 @@ pub fn python_env_kernel_specifications(
                     let python_path = toolchain.path.to_string();
                     let environment_kind = extract_environment_kind(&toolchain.as_json);
 
-                    let has_ipykernel = util::command::new_smol_command(&python_path)
+                    let has_ipykernel = util::command::new_command(&python_path)
                         .args(&["-c", "import ipykernel"])
                         .output()
                         .await

crates/repl/src/kernels/native_kernel.rs 🔗

@@ -12,7 +12,7 @@ use jupyter_protocol::{
 };
 use project::Fs;
 use runtimelib::{RuntimeError, dirs};
-use smol::{net::TcpListener, process::Command};
+use smol::net::TcpListener;
 use std::{
     env,
     fmt::Debug,
@@ -20,6 +20,7 @@ use std::{
     path::PathBuf,
     sync::Arc,
 };
+use util::command::Command;
 use uuid::Uuid;
 
 use super::{KernelSession, RunningKernel};
@@ -52,7 +53,7 @@ impl LocalKernelSpecification {
             self.name
         );
 
-        let mut cmd = util::command::new_smol_command(&argv[0]);
+        let mut cmd = util::command::new_command(&argv[0]);
 
         for arg in &argv[1..] {
             if arg == "{connection_file}" {
@@ -85,7 +86,7 @@ async fn peek_ports(ip: IpAddr) -> Result<[u16; 5]> {
 }
 
 pub struct NativeRunningKernel {
-    pub process: smol::process::Child,
+    pub process: util::command::Child,
     connection_path: PathBuf,
     _process_status_task: Option<Task<()>>,
     pub working_directory: PathBuf,
@@ -143,9 +144,9 @@ impl NativeRunningKernel {
 
             let mut process = cmd
                 .current_dir(&working_directory)
-                .stdout(std::process::Stdio::piped())
-                .stderr(std::process::Stdio::piped())
-                .stdin(std::process::Stdio::piped())
+                .stdout(util::command::Stdio::piped())
+                .stderr(util::command::Stdio::piped())
+                .stdin(util::command::Stdio::piped())
                 .kill_on_drop(true)
                 .spawn()
                 .context("failed to start the kernel process")?;
@@ -490,7 +491,7 @@ pub async fn local_kernel_specifications(fs: Arc<dyn Fs>) -> Result<Vec<LocalKer
     }
 
     // Search for kernels inside the base python environment
-    let command = util::command::new_smol_command("python")
+    let command = util::command::new_command("python")
         .arg("-c")
         .arg("import sys; print(sys.prefix)")
         .output()

crates/repl/src/repl_editor.rs 🔗

@@ -108,7 +108,7 @@ pub fn install_ipykernel_and_assign(
     let window_handle = window.window_handle();
 
     let install_task = cx.background_spawn(async move {
-        let output = util::command::new_smol_command(python_path.to_string_lossy().as_ref())
+        let output = util::command::new_command(python_path.to_string_lossy().as_ref())
             .args(&["-m", "pip", "install", "ipykernel"])
             .output()
             .await

crates/supermaven/src/supermaven.rs 🔗

@@ -17,13 +17,12 @@ use messages::*;
 use postage::watch;
 use serde::{Deserialize, Serialize};
 use settings::SettingsStore;
-use smol::{
-    io::AsyncWriteExt,
-    process::{Child, ChildStdin, ChildStdout},
-};
-use std::{path::PathBuf, process::Stdio, sync::Arc};
+use smol::io::AsyncWriteExt;
+use std::{path::PathBuf, sync::Arc};
 use ui::prelude::*;
 use util::ResultExt;
+use util::command::Child;
+use util::command::Stdio;
 
 actions!(
     supermaven,
@@ -271,7 +270,7 @@ impl SupermavenAgent {
         client: Arc<Client>,
         cx: &mut Context<Supermaven>,
     ) -> Result<Self> {
-        let mut process = util::command::new_smol_command(&binary_path)
+        let mut process = util::command::new_command(&binary_path)
             .arg("stdio")
             .stdin(Stdio::piped())
             .stdout(Stdio::piped())
@@ -308,9 +307,9 @@ impl SupermavenAgent {
         })
     }
 
-    async fn handle_outgoing_messages(
+    async fn handle_outgoing_messages<W: smol::io::AsyncWrite + Unpin>(
         mut outgoing: mpsc::UnboundedReceiver<OutboundMessage>,
-        mut stdin: ChildStdin,
+        mut stdin: W,
     ) -> Result<()> {
         while let Some(message) = outgoing.next().await {
             let bytes = serde_json::to_vec(&message)?;
@@ -320,9 +319,9 @@ impl SupermavenAgent {
         Ok(())
     }
 
-    async fn handle_incoming_messages(
+    async fn handle_incoming_messages<R: smol::io::AsyncRead + Unpin>(
         this: WeakEntity<Supermaven>,
-        stdout: ChildStdout,
+        stdout: R,
         cx: &mut AsyncApp,
     ) -> Result<()> {
         const MESSAGE_PREFIX: &str = "SM-MESSAGE ";

crates/util/src/command.rs 🔗

@@ -1,8 +1,20 @@
 use std::ffi::OsStr;
+#[cfg(not(target_os = "macos"))]
+use std::path::Path;
+
+#[cfg(target_os = "macos")]
+mod darwin;
+
+#[cfg(target_os = "macos")]
+pub use darwin::{Child, Command, Stdio};
 
 #[cfg(target_os = "windows")]
 const CREATE_NO_WINDOW: u32 = 0x0800_0000_u32;
 
+pub fn new_command(program: impl AsRef<OsStr>) -> Command {
+    Command::new(program)
+}
+
 #[cfg(target_os = "windows")]
 pub fn new_std_command(program: impl AsRef<OsStr>) -> std::process::Command {
     use std::os::windows::process::CommandExt;
@@ -17,86 +29,104 @@ pub fn new_std_command(program: impl AsRef<OsStr>) -> std::process::Command {
     std::process::Command::new(program)
 }
 
-#[cfg(target_os = "windows")]
-pub fn new_smol_command(program: impl AsRef<OsStr>) -> smol::process::Command {
-    use smol::process::windows::CommandExt;
+#[cfg(not(target_os = "macos"))]
+pub type Child = smol::process::Child;
 
-    let mut command = smol::process::Command::new(program);
-    command.creation_flags(CREATE_NO_WINDOW);
-    command
-}
+#[cfg(not(target_os = "macos"))]
+pub use std::process::Stdio;
 
-#[cfg(target_os = "macos")]
-pub fn new_smol_command(program: impl AsRef<OsStr>) -> smol::process::Command {
-    use std::os::unix::process::CommandExt;
-
-    // Create a std::process::Command first so we can use pre_exec
-    let mut std_cmd = std::process::Command::new(program);
-
-    // WORKAROUND: Reset exception ports before exec to prevent inheritance of
-    // crash handler exception ports. Due to a timing issue, child processes can
-    // inherit the parent's exception ports before they're fully stabilized,
-    // which can block child process spawning.
-    // See: https://github.com/zed-industries/zed/issues/36754
-    unsafe {
-        std_cmd.pre_exec(|| {
-            // Reset all exception ports to system defaults for this task.
-            // This prevents the child from inheriting the parent's crash handler
-            // exception ports.
-            reset_exception_ports();
-            Ok(())
-        });
+#[cfg(not(target_os = "macos"))]
+#[derive(Debug)]
+pub struct Command(smol::process::Command);
+
+#[cfg(not(target_os = "macos"))]
+impl Command {
+    #[inline]
+    pub fn new(program: impl AsRef<OsStr>) -> Self {
+        #[cfg(target_os = "windows")]
+        {
+            use smol::process::windows::CommandExt;
+            let mut cmd = smol::process::Command::new(program);
+            cmd.creation_flags(CREATE_NO_WINDOW);
+            Self(cmd)
+        }
+        #[cfg(not(target_os = "windows"))]
+        Self(smol::process::Command::new(program))
     }
 
-    // Convert to async_process::Command via From trait
-    smol::process::Command::from(std_cmd)
-}
+    pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
+        self.0.arg(arg);
+        self
+    }
 
-#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
-pub fn new_smol_command(program: impl AsRef<OsStr>) -> smol::process::Command {
-    smol::process::Command::new(program)
-}
+    pub fn args<I, S>(&mut self, args: I) -> &mut Self
+    where
+        I: IntoIterator<Item = S>,
+        S: AsRef<OsStr>,
+    {
+        self.0.args(args);
+        self
+    }
 
-#[cfg(target_os = "macos")]
-pub fn reset_exception_ports() {
-    use mach2::exception_types::{
-        EXC_MASK_ALL, EXCEPTION_DEFAULT, exception_behavior_t, exception_mask_t,
-    };
-    use mach2::kern_return::{KERN_SUCCESS, kern_return_t};
-    use mach2::mach_types::task_t;
-    use mach2::port::{MACH_PORT_NULL, mach_port_t};
-    use mach2::thread_status::{THREAD_STATE_NONE, thread_state_flavor_t};
-    use mach2::traps::mach_task_self;
-
-    // FFI binding for task_set_exception_ports (not exposed by mach2 crate)
-    unsafe extern "C" {
-        fn task_set_exception_ports(
-            task: task_t,
-            exception_mask: exception_mask_t,
-            new_port: mach_port_t,
-            behavior: exception_behavior_t,
-            new_flavor: thread_state_flavor_t,
-        ) -> kern_return_t;
+    pub fn env(&mut self, key: impl AsRef<OsStr>, val: impl AsRef<OsStr>) -> &mut Self {
+        self.0.env(key, val);
+        self
     }
 
-    unsafe {
-        let task = mach_task_self();
-        // Reset all exception ports to MACH_PORT_NULL (system default)
-        // This prevents the child process from inheriting the parent's crash handler
-        let kr = task_set_exception_ports(
-            task,
-            EXC_MASK_ALL,
-            MACH_PORT_NULL,
-            EXCEPTION_DEFAULT as exception_behavior_t,
-            THREAD_STATE_NONE,
-        );
-
-        if kr != KERN_SUCCESS {
-            // Log but don't fail - the process can still work without this workaround
-            eprintln!(
-                "Warning: failed to reset exception ports in child process (kern_return: {})",
-                kr
-            );
-        }
+    pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
+    where
+        I: IntoIterator<Item = (K, V)>,
+        K: AsRef<OsStr>,
+        V: AsRef<OsStr>,
+    {
+        self.0.envs(vars);
+        self
+    }
+
+    pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
+        self.0.env_remove(key);
+        self
+    }
+
+    pub fn env_clear(&mut self) -> &mut Self {
+        self.0.env_clear();
+        self
+    }
+
+    pub fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
+        self.0.current_dir(dir);
+        self
+    }
+
+    pub fn stdin(&mut self, cfg: impl Into<Stdio>) -> &mut Self {
+        self.0.stdin(cfg.into());
+        self
+    }
+
+    pub fn stdout(&mut self, cfg: impl Into<Stdio>) -> &mut Self {
+        self.0.stdout(cfg.into());
+        self
+    }
+
+    pub fn stderr(&mut self, cfg: impl Into<Stdio>) -> &mut Self {
+        self.0.stderr(cfg.into());
+        self
+    }
+
+    pub fn kill_on_drop(&mut self, kill_on_drop: bool) -> &mut Self {
+        self.0.kill_on_drop(kill_on_drop);
+        self
+    }
+
+    pub fn spawn(&mut self) -> std::io::Result<Child> {
+        self.0.spawn()
+    }
+
+    pub async fn output(&mut self) -> std::io::Result<std::process::Output> {
+        self.0.output().await
+    }
+
+    pub async fn status(&mut self) -> std::io::Result<std::process::ExitStatus> {
+        self.0.status().await
     }
 }

crates/util/src/command/darwin.rs 🔗

@@ -0,0 +1,825 @@
+use mach2::exception_types::{
+    EXC_MASK_ALL, EXCEPTION_DEFAULT, exception_behavior_t, exception_mask_t,
+};
+use mach2::port::{MACH_PORT_NULL, mach_port_t};
+use mach2::thread_status::{THREAD_STATE_NONE, thread_state_flavor_t};
+use smol::Unblock;
+use std::collections::BTreeMap;
+use std::ffi::{CString, OsStr, OsString};
+use std::io;
+use std::os::unix::ffi::OsStrExt;
+use std::os::unix::io::FromRawFd;
+use std::os::unix::process::ExitStatusExt;
+use std::path::{Path, PathBuf};
+use std::process::{ExitStatus, Output};
+use std::ptr;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum Stdio {
+    /// A new pipe should be arranged to connect the parent and child processes.
+    #[default]
+    Piped,
+    /// The child inherits from the corresponding parent descriptor.
+    Inherit,
+    /// This stream will be ignored (redirected to `/dev/null`).
+    Null,
+}
+
+impl Stdio {
+    pub fn piped() -> Self {
+        Self::Piped
+    }
+
+    pub fn inherit() -> Self {
+        Self::Inherit
+    }
+
+    pub fn null() -> Self {
+        Self::Null
+    }
+}
+
+unsafe extern "C" {
+    fn posix_spawnattr_setexceptionports_np(
+        attr: *mut libc::posix_spawnattr_t,
+        mask: exception_mask_t,
+        new_port: mach_port_t,
+        behavior: exception_behavior_t,
+        new_flavor: thread_state_flavor_t,
+    ) -> libc::c_int;
+
+    fn posix_spawn_file_actions_addchdir_np(
+        file_actions: *mut libc::posix_spawn_file_actions_t,
+        path: *const libc::c_char,
+    ) -> libc::c_int;
+
+    fn posix_spawn_file_actions_addinherit_np(
+        file_actions: *mut libc::posix_spawn_file_actions_t,
+        filedes: libc::c_int,
+    ) -> libc::c_int;
+
+    static environ: *const *mut libc::c_char;
+}
+
+#[derive(Debug)]
+pub struct Command {
+    program: OsString,
+    args: Vec<OsString>,
+    envs: BTreeMap<OsString, Option<OsString>>,
+    env_clear: bool,
+    current_dir: Option<PathBuf>,
+    stdin_cfg: Option<Stdio>,
+    stdout_cfg: Option<Stdio>,
+    stderr_cfg: Option<Stdio>,
+    kill_on_drop: bool,
+}
+
+impl Command {
+    pub fn new(program: impl AsRef<OsStr>) -> Self {
+        Self {
+            program: program.as_ref().to_owned(),
+            args: Vec::new(),
+            envs: BTreeMap::new(),
+            env_clear: false,
+            current_dir: None,
+            stdin_cfg: None,
+            stdout_cfg: None,
+            stderr_cfg: None,
+            kill_on_drop: false,
+        }
+    }
+
+    pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
+        self.args.push(arg.as_ref().to_owned());
+        self
+    }
+
+    pub fn args<I, S>(&mut self, args: I) -> &mut Self
+    where
+        I: IntoIterator<Item = S>,
+        S: AsRef<OsStr>,
+    {
+        self.args
+            .extend(args.into_iter().map(|a| a.as_ref().to_owned()));
+        self
+    }
+
+    pub fn env(&mut self, key: impl AsRef<OsStr>, val: impl AsRef<OsStr>) -> &mut Self {
+        self.envs
+            .insert(key.as_ref().to_owned(), Some(val.as_ref().to_owned()));
+        self
+    }
+
+    pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
+    where
+        I: IntoIterator<Item = (K, V)>,
+        K: AsRef<OsStr>,
+        V: AsRef<OsStr>,
+    {
+        for (key, val) in vars {
+            self.envs
+                .insert(key.as_ref().to_owned(), Some(val.as_ref().to_owned()));
+        }
+        self
+    }
+
+    pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
+        let key = key.as_ref().to_owned();
+        if self.env_clear {
+            self.envs.remove(&key);
+        } else {
+            self.envs.insert(key, None);
+        }
+        self
+    }
+
+    pub fn env_clear(&mut self) -> &mut Self {
+        self.env_clear = true;
+        self.envs.clear();
+        self
+    }
+
+    pub fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
+        self.current_dir = Some(dir.as_ref().to_owned());
+        self
+    }
+
+    pub fn stdin(&mut self, cfg: Stdio) -> &mut Self {
+        self.stdin_cfg = Some(cfg);
+        self
+    }
+
+    pub fn stdout(&mut self, cfg: Stdio) -> &mut Self {
+        self.stdout_cfg = Some(cfg);
+        self
+    }
+
+    pub fn stderr(&mut self, cfg: Stdio) -> &mut Self {
+        self.stderr_cfg = Some(cfg);
+        self
+    }
+
+    pub fn kill_on_drop(&mut self, kill_on_drop: bool) -> &mut Self {
+        self.kill_on_drop = kill_on_drop;
+        self
+    }
+
+    pub fn spawn(&mut self) -> io::Result<Child> {
+        let current_dir = self
+            .current_dir
+            .as_deref()
+            .unwrap_or_else(|| Path::new("."));
+
+        // Optimization: if no environment modifications were requested, pass None
+        // to spawn_posix so it uses the `environ` global directly, avoiding a
+        // full copy of the environment. This matches std::process::Command behavior.
+        let envs = if self.env_clear || !self.envs.is_empty() {
+            let mut result = BTreeMap::<OsString, OsString>::new();
+            if !self.env_clear {
+                for (key, val) in std::env::vars_os() {
+                    result.insert(key, val);
+                }
+            }
+            for (key, maybe_val) in &self.envs {
+                if let Some(val) = maybe_val {
+                    result.insert(key.clone(), val.clone());
+                } else {
+                    result.remove(key);
+                }
+            }
+            Some(result.into_iter().collect::<Vec<_>>())
+        } else {
+            None
+        };
+
+        spawn_posix_spawn(
+            &self.program,
+            &self.args,
+            current_dir,
+            envs.as_deref(),
+            self.stdin_cfg.unwrap_or_default(),
+            self.stdout_cfg.unwrap_or_default(),
+            self.stderr_cfg.unwrap_or_default(),
+            self.kill_on_drop,
+        )
+    }
+
+    pub async fn output(&mut self) -> io::Result<Output> {
+        self.stdin_cfg.get_or_insert(Stdio::null());
+        self.stdout_cfg.get_or_insert(Stdio::piped());
+        self.stderr_cfg.get_or_insert(Stdio::piped());
+
+        let child = self.spawn()?;
+        child.output().await
+    }
+
+    pub async fn status(&mut self) -> io::Result<ExitStatus> {
+        let mut child = self.spawn()?;
+        child.status().await
+    }
+}
+
+#[derive(Debug)]
+pub struct Child {
+    pid: libc::pid_t,
+    pub stdin: Option<Unblock<std::fs::File>>,
+    pub stdout: Option<Unblock<std::fs::File>>,
+    pub stderr: Option<Unblock<std::fs::File>>,
+    kill_on_drop: bool,
+    status: Option<ExitStatus>,
+}
+
+impl Drop for Child {
+    fn drop(&mut self) {
+        if self.kill_on_drop && self.status.is_none() {
+            let _ = self.kill();
+        }
+    }
+}
+
+impl Child {
+    pub fn id(&self) -> u32 {
+        self.pid as u32
+    }
+
+    pub fn kill(&mut self) -> io::Result<()> {
+        let result = unsafe { libc::kill(self.pid, libc::SIGKILL) };
+        if result == -1 {
+            Err(io::Error::last_os_error())
+        } else {
+            Ok(())
+        }
+    }
+
+    pub fn try_status(&mut self) -> io::Result<Option<ExitStatus>> {
+        if let Some(status) = self.status {
+            return Ok(Some(status));
+        }
+
+        let mut status: libc::c_int = 0;
+        let result = unsafe { libc::waitpid(self.pid, &mut status, libc::WNOHANG) };
+
+        if result == -1 {
+            Err(io::Error::last_os_error())
+        } else if result == 0 {
+            Ok(None)
+        } else {
+            let exit_status = ExitStatus::from_raw(status);
+            self.status = Some(exit_status);
+            Ok(Some(exit_status))
+        }
+    }
+
+    pub fn status(
+        &mut self,
+    ) -> impl std::future::Future<Output = io::Result<ExitStatus>> + Send + 'static {
+        self.stdin.take();
+
+        let pid = self.pid;
+        let cached_status = self.status;
+
+        async move {
+            if let Some(status) = cached_status {
+                return Ok(status);
+            }
+
+            smol::unblock(move || {
+                let mut status: libc::c_int = 0;
+                let result = unsafe { libc::waitpid(pid, &mut status, 0) };
+                if result == -1 {
+                    Err(io::Error::last_os_error())
+                } else {
+                    Ok(ExitStatus::from_raw(status))
+                }
+            })
+            .await
+        }
+    }
+
+    pub async fn output(mut self) -> io::Result<Output> {
+        use futures_lite::AsyncReadExt;
+
+        let status = self.status();
+
+        let stdout = self.stdout.take();
+        let stdout_future = async move {
+            let mut data = Vec::new();
+            if let Some(mut stdout) = stdout {
+                stdout.read_to_end(&mut data).await?;
+            }
+            io::Result::Ok(data)
+        };
+
+        let stderr = self.stderr.take();
+        let stderr_future = async move {
+            let mut data = Vec::new();
+            if let Some(mut stderr) = stderr {
+                stderr.read_to_end(&mut data).await?;
+            }
+            io::Result::Ok(data)
+        };
+
+        let (stdout_data, stderr_data) =
+            futures_lite::future::try_zip(stdout_future, stderr_future).await?;
+        let status = status.await?;
+
+        Ok(Output {
+            status,
+            stdout: stdout_data,
+            stderr: stderr_data,
+        })
+    }
+}
+
+fn spawn_posix_spawn(
+    program: &OsStr,
+    args: &[OsString],
+    current_dir: &Path,
+    envs: Option<&[(OsString, OsString)]>,
+    stdin_cfg: Stdio,
+    stdout_cfg: Stdio,
+    stderr_cfg: Stdio,
+    kill_on_drop: bool,
+) -> io::Result<Child> {
+    let program_cstr = CString::new(program.as_bytes()).map_err(|_| invalid_input_error())?;
+
+    let current_dir_cstr =
+        CString::new(current_dir.as_os_str().as_bytes()).map_err(|_| invalid_input_error())?;
+
+    let mut argv_cstrs = vec![program_cstr.clone()];
+    for arg in args {
+        let cstr = CString::new(arg.as_bytes()).map_err(|_| invalid_input_error())?;
+        argv_cstrs.push(cstr);
+    }
+    let mut argv_ptrs: Vec<*mut libc::c_char> = argv_cstrs
+        .iter()
+        .map(|s| s.as_ptr() as *mut libc::c_char)
+        .collect();
+    argv_ptrs.push(ptr::null_mut());
+
+    let envp: Vec<CString> = if let Some(envs) = envs {
+        envs.iter()
+            .map(|(key, value)| {
+                let mut env_str = key.as_bytes().to_vec();
+                env_str.push(b'=');
+                env_str.extend_from_slice(value.as_bytes());
+                CString::new(env_str)
+            })
+            .collect::<Result<Vec<_>, _>>()
+            .map_err(|_| invalid_input_error())?
+    } else {
+        Vec::new()
+    };
+    let mut envp_ptrs: Vec<*mut libc::c_char> = envp
+        .iter()
+        .map(|s| s.as_ptr() as *mut libc::c_char)
+        .collect();
+    envp_ptrs.push(ptr::null_mut());
+
+    let (stdin_read, stdin_write) = match stdin_cfg {
+        Stdio::Piped => {
+            let (r, w) = create_pipe()?;
+            (Some(r), Some(w))
+        }
+        Stdio::Null => {
+            let fd = open_dev_null(libc::O_RDONLY)?;
+            (Some(fd), None)
+        }
+        Stdio::Inherit => (None, None),
+    };
+
+    let (stdout_read, stdout_write) = match stdout_cfg {
+        Stdio::Piped => {
+            let (r, w) = create_pipe()?;
+            (Some(r), Some(w))
+        }
+        Stdio::Null => {
+            let fd = open_dev_null(libc::O_WRONLY)?;
+            (None, Some(fd))
+        }
+        Stdio::Inherit => (None, None),
+    };
+
+    let (stderr_read, stderr_write) = match stderr_cfg {
+        Stdio::Piped => {
+            let (r, w) = create_pipe()?;
+            (Some(r), Some(w))
+        }
+        Stdio::Null => {
+            let fd = open_dev_null(libc::O_WRONLY)?;
+            (None, Some(fd))
+        }
+        Stdio::Inherit => (None, None),
+    };
+
+    let mut attr: libc::posix_spawnattr_t = ptr::null_mut();
+    let mut file_actions: libc::posix_spawn_file_actions_t = ptr::null_mut();
+
+    unsafe {
+        cvt_nz(libc::posix_spawnattr_init(&mut attr))?;
+        cvt_nz(libc::posix_spawn_file_actions_init(&mut file_actions))?;
+
+        cvt_nz(libc::posix_spawnattr_setflags(
+            &mut attr,
+            libc::POSIX_SPAWN_CLOEXEC_DEFAULT as libc::c_short,
+        ))?;
+
+        cvt_nz(posix_spawnattr_setexceptionports_np(
+            &mut attr,
+            EXC_MASK_ALL,
+            MACH_PORT_NULL,
+            EXCEPTION_DEFAULT as exception_behavior_t,
+            THREAD_STATE_NONE,
+        ))?;
+
+        cvt_nz(posix_spawn_file_actions_addchdir_np(
+            &mut file_actions,
+            current_dir_cstr.as_ptr(),
+        ))?;
+
+        if let Some(fd) = stdin_read {
+            cvt_nz(libc::posix_spawn_file_actions_adddup2(
+                &mut file_actions,
+                fd,
+                libc::STDIN_FILENO,
+            ))?;
+            cvt_nz(posix_spawn_file_actions_addinherit_np(
+                &mut file_actions,
+                libc::STDIN_FILENO,
+            ))?;
+        }
+
+        if let Some(fd) = stdout_write {
+            cvt_nz(libc::posix_spawn_file_actions_adddup2(
+                &mut file_actions,
+                fd,
+                libc::STDOUT_FILENO,
+            ))?;
+            cvt_nz(posix_spawn_file_actions_addinherit_np(
+                &mut file_actions,
+                libc::STDOUT_FILENO,
+            ))?;
+        }
+
+        if let Some(fd) = stderr_write {
+            cvt_nz(libc::posix_spawn_file_actions_adddup2(
+                &mut file_actions,
+                fd,
+                libc::STDERR_FILENO,
+            ))?;
+            cvt_nz(posix_spawn_file_actions_addinherit_np(
+                &mut file_actions,
+                libc::STDERR_FILENO,
+            ))?;
+        }
+
+        let mut pid: libc::pid_t = 0;
+
+        let spawn_result = libc::posix_spawnp(
+            &mut pid,
+            program_cstr.as_ptr(),
+            &file_actions,
+            &attr,
+            argv_ptrs.as_ptr(),
+            if envs.is_some() {
+                envp_ptrs.as_ptr()
+            } else {
+                environ
+            },
+        );
+
+        libc::posix_spawnattr_destroy(&mut attr);
+        libc::posix_spawn_file_actions_destroy(&mut file_actions);
+
+        if let Some(fd) = stdin_read {
+            libc::close(fd);
+        }
+        if let Some(fd) = stdout_write {
+            libc::close(fd);
+        }
+        if let Some(fd) = stderr_write {
+            libc::close(fd);
+        }
+
+        cvt_nz(spawn_result)?;
+
+        Ok(Child {
+            pid,
+            stdin: stdin_write.map(|fd| Unblock::new(std::fs::File::from_raw_fd(fd))),
+            stdout: stdout_read.map(|fd| Unblock::new(std::fs::File::from_raw_fd(fd))),
+            stderr: stderr_read.map(|fd| Unblock::new(std::fs::File::from_raw_fd(fd))),
+            kill_on_drop,
+            status: None,
+        })
+    }
+}
+
+fn create_pipe() -> io::Result<(libc::c_int, libc::c_int)> {
+    let mut fds: [libc::c_int; 2] = [0; 2];
+    let result = unsafe { libc::pipe(fds.as_mut_ptr()) };
+    if result == -1 {
+        return Err(io::Error::last_os_error());
+    }
+    Ok((fds[0], fds[1]))
+}
+
+fn open_dev_null(flags: libc::c_int) -> io::Result<libc::c_int> {
+    let fd = unsafe { libc::open(c"/dev/null".as_ptr() as *const libc::c_char, flags) };
+    if fd == -1 {
+        return Err(io::Error::last_os_error());
+    }
+    Ok(fd)
+}
+
+/// Zero means `Ok()`, all other values are treated as raw OS errors. Does not look at `errno`.
+/// Mirrored after Rust's std `cvt_nz` function.
+fn cvt_nz(error: libc::c_int) -> io::Result<()> {
+    if error == 0 {
+        Ok(())
+    } else {
+        Err(io::Error::from_raw_os_error(error))
+    }
+}
+
+fn invalid_input_error() -> io::Error {
+    io::Error::new(
+        io::ErrorKind::InvalidInput,
+        "invalid argument: path or argument contains null byte",
+    )
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use futures_lite::AsyncWriteExt;
+
+    #[test]
+    fn test_spawn_echo() {
+        smol::block_on(async {
+            let output = Command::new("/bin/echo")
+                .args(["-n", "hello world"])
+                .output()
+                .await
+                .expect("failed to run command");
+
+            assert!(output.status.success());
+            assert_eq!(output.stdout, b"hello world");
+        });
+    }
+
+    #[test]
+    fn test_spawn_cat_stdin() {
+        smol::block_on(async {
+            let mut child = Command::new("/bin/cat")
+                .stdin(Stdio::piped())
+                .stdout(Stdio::piped())
+                .spawn()
+                .expect("failed to spawn");
+
+            if let Some(ref mut stdin) = child.stdin {
+                stdin
+                    .write_all(b"hello from stdin")
+                    .await
+                    .expect("failed to write");
+                stdin.close().await.expect("failed to close");
+            }
+            drop(child.stdin.take());
+
+            let output = child.output().await.expect("failed to get output");
+            assert!(output.status.success());
+            assert_eq!(output.stdout, b"hello from stdin");
+        });
+    }
+
+    #[test]
+    fn test_spawn_stderr() {
+        smol::block_on(async {
+            let output = Command::new("/bin/sh")
+                .args(["-c", "echo error >&2"])
+                .output()
+                .await
+                .expect("failed to run command");
+
+            assert!(output.status.success());
+            assert_eq!(output.stderr, b"error\n");
+        });
+    }
+
+    #[test]
+    fn test_spawn_exit_code() {
+        smol::block_on(async {
+            let output = Command::new("/bin/sh")
+                .args(["-c", "exit 42"])
+                .output()
+                .await
+                .expect("failed to run command");
+
+            assert!(!output.status.success());
+            assert_eq!(output.status.code(), Some(42));
+        });
+    }
+
+    #[test]
+    fn test_spawn_current_dir() {
+        smol::block_on(async {
+            let output = Command::new("/bin/pwd")
+                .current_dir("/tmp")
+                .output()
+                .await
+                .expect("failed to run command");
+
+            assert!(output.status.success());
+            let pwd = String::from_utf8_lossy(&output.stdout);
+            assert!(pwd.trim() == "/tmp" || pwd.trim() == "/private/tmp");
+        });
+    }
+
+    #[test]
+    fn test_spawn_env() {
+        smol::block_on(async {
+            let output = Command::new("/bin/sh")
+                .args(["-c", "echo $MY_TEST_VAR"])
+                .env("MY_TEST_VAR", "test_value")
+                .output()
+                .await
+                .expect("failed to run command");
+
+            assert!(output.status.success());
+            assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "test_value");
+        });
+    }
+
+    #[test]
+    fn test_spawn_status() {
+        smol::block_on(async {
+            let status = Command::new("/usr/bin/true")
+                .status()
+                .await
+                .expect("failed to run command");
+
+            assert!(status.success());
+
+            let status = Command::new("/usr/bin/false")
+                .status()
+                .await
+                .expect("failed to run command");
+
+            assert!(!status.success());
+        });
+    }
+
+    #[test]
+    fn test_env_remove_removes_set_env() {
+        smol::block_on(async {
+            let output = Command::new("/bin/sh")
+                .args(["-c", "echo ${MY_VAR:-unset}"])
+                .env("MY_VAR", "set_value")
+                .env_remove("MY_VAR")
+                .output()
+                .await
+                .expect("failed to run command");
+
+            assert!(output.status.success());
+            assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unset");
+        });
+    }
+
+    #[test]
+    fn test_env_remove_removes_inherited_env() {
+        smol::block_on(async {
+            // SAFETY: This test is single-threaded and we clean up the var at the end
+            unsafe { std::env::set_var("TEST_INHERITED_VAR", "inherited_value") };
+
+            let output = Command::new("/bin/sh")
+                .args(["-c", "echo ${TEST_INHERITED_VAR:-unset}"])
+                .env_remove("TEST_INHERITED_VAR")
+                .output()
+                .await
+                .expect("failed to run command");
+
+            assert!(output.status.success());
+            assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unset");
+
+            // SAFETY: Cleaning up test env var
+            unsafe { std::env::remove_var("TEST_INHERITED_VAR") };
+        });
+    }
+
+    #[test]
+    fn test_env_after_env_remove() {
+        smol::block_on(async {
+            let output = Command::new("/bin/sh")
+                .args(["-c", "echo ${MY_VAR:-unset}"])
+                .env_remove("MY_VAR")
+                .env("MY_VAR", "new_value")
+                .output()
+                .await
+                .expect("failed to run command");
+
+            assert!(output.status.success());
+            assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "new_value");
+        });
+    }
+
+    #[test]
+    fn test_env_remove_after_env_clear() {
+        smol::block_on(async {
+            let output = Command::new("/bin/sh")
+                .args(["-c", "echo ${MY_VAR:-unset}"])
+                .env_clear()
+                .env("MY_VAR", "set_value")
+                .env_remove("MY_VAR")
+                .output()
+                .await
+                .expect("failed to run command");
+
+            assert!(output.status.success());
+            assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unset");
+        });
+    }
+
+    #[test]
+    fn test_stdio_null_stdin() {
+        smol::block_on(async {
+            let child = Command::new("/bin/cat")
+                .stdin(Stdio::null())
+                .stdout(Stdio::piped())
+                .spawn()
+                .expect("failed to spawn");
+
+            let output = child.output().await.expect("failed to get output");
+            assert!(output.status.success());
+            assert!(
+                output.stdout.is_empty(),
+                "stdin from /dev/null should produce no output from cat"
+            );
+        });
+    }
+
+    #[test]
+    fn test_stdio_null_stdout() {
+        smol::block_on(async {
+            let mut child = Command::new("/bin/echo")
+                .args(["hello"])
+                .stdout(Stdio::null())
+                .spawn()
+                .expect("failed to spawn");
+
+            assert!(
+                child.stdout.is_none(),
+                "stdout should be None when Stdio::null() is used"
+            );
+
+            let status = child.status().await.expect("failed to get status");
+            assert!(status.success());
+        });
+    }
+
+    #[test]
+    fn test_stdio_null_stderr() {
+        smol::block_on(async {
+            let mut child = Command::new("/bin/sh")
+                .args(["-c", "echo error >&2"])
+                .stderr(Stdio::null())
+                .spawn()
+                .expect("failed to spawn");
+
+            assert!(
+                child.stderr.is_none(),
+                "stderr should be None when Stdio::null() is used"
+            );
+
+            let status = child.status().await.expect("failed to get status");
+            assert!(status.success());
+        });
+    }
+
+    #[test]
+    fn test_stdio_piped_stdin() {
+        smol::block_on(async {
+            let mut child = Command::new("/bin/cat")
+                .stdin(Stdio::piped())
+                .stdout(Stdio::piped())
+                .spawn()
+                .expect("failed to spawn");
+
+            assert!(
+                child.stdin.is_some(),
+                "stdin should be Some when Stdio::piped() is used"
+            );
+
+            if let Some(ref mut stdin) = child.stdin {
+                stdin
+                    .write_all(b"piped input")
+                    .await
+                    .expect("failed to write");
+                stdin.close().await.expect("failed to close");
+            }
+            drop(child.stdin.take());
+
+            let output = child.output().await.expect("failed to get output");
+            assert!(output.status.success());
+            assert_eq!(output.stdout, b"piped input");
+        });
+    }
+}

crates/util/src/shell_env.rs 🔗

@@ -141,7 +141,7 @@ async fn capture_windows(
         std::env::current_exe().context("Failed to determine current zed executable path.")?;
 
     let shell_kind = ShellKind::new(shell_path, true);
-    let mut cmd = crate::command::new_smol_command(shell_path);
+    let mut cmd = crate::command::new_command(shell_path);
     cmd.args(args);
     let cmd = match shell_kind {
         ShellKind::Csh

crates/util/src/util.rs 🔗

@@ -409,8 +409,6 @@ pub fn set_pre_exec_to_start_new_session(
         use std::os::unix::process::CommandExt;
         command.pre_exec(|| {
             libc::setsid();
-            #[cfg(target_os = "macos")]
-            crate::command::reset_exception_ports();
             Ok(())
         });
     };