Remove netcat dependency (#28920)

Ben Kunkle created

Closes #28813
Closes #27749

Release Notes:

- Removed the need to have openbsd `netcat` (`nc`) installed on your
system in order to enter passwords for `git` or `ssh` (remote
development). If you previously installed `netcat` specifically for Zed,
you may uninstall it.

Change summary

Cargo.lock                       |  3 +
crates/askpass/Cargo.toml        |  1 
crates/askpass/src/askpass.rs    | 62 ++++++++++++++++++++++++++-------
crates/remote_server/Cargo.toml  |  1 
crates/remote_server/src/main.rs |  9 ++++
crates/zed/Cargo.toml            |  1 
crates/zed/src/main.rs           | 10 +++++
script/linux                     |  7 ---
8 files changed, 72 insertions(+), 22 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -448,7 +448,6 @@ dependencies = [
  "smol",
  "tempfile",
  "util",
- "which 6.0.3",
  "workspace-hack",
 ]
 
@@ -11752,6 +11751,7 @@ name = "remote_server"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "askpass",
  "async-watch",
  "backtrace",
  "cargo_toml",
@@ -18204,6 +18204,7 @@ dependencies = [
  "agent",
  "anyhow",
  "ashpd",
+ "askpass",
  "assets",
  "assistant",
  "assistant_context_editor",

crates/askpass/Cargo.toml 🔗

@@ -18,5 +18,4 @@ gpui.workspace = true
 smol.workspace = true
 tempfile.workspace = true
 util.workspace = true
-which.workspace = true
 workspace-hack.workspace = true

crates/askpass/src/askpass.rs 🔗

@@ -72,6 +72,8 @@ impl AskPassSession {
         let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
         let listener =
             UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
+        let zed_path = std::env::current_exe()
+            .context("Failed to figure out current executable path for use in askpass")?;
 
         let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
         let mut kill_tx = Some(askpass_kill_master_tx);
@@ -110,21 +112,10 @@ impl AskPassSession {
             drop(temp_dir)
         });
 
-        anyhow::ensure!(
-            which::which("nc").is_ok(),
-            "Cannot find `nc` command (netcat), which is required to connect over SSH."
-        );
-
         // Create an askpass script that communicates back to this process.
         let askpass_script = format!(
-            "{shebang}\n{print_args} | {nc} -U {askpass_socket} 2> /dev/null \n",
-            // on macOS `brew install netcat` provides the GNU netcat implementation
-            // which does not support -U.
-            nc = if cfg!(target_os = "macos") {
-                "/usr/bin/nc"
-            } else {
-                "nc"
-            },
+            "{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
+            zed_exe = zed_path.display(),
             askpass_socket = askpass_socket.display(),
             print_args = "printf '%s\\0' \"$@\"",
             shebang = "#!/bin/sh",
@@ -170,6 +161,51 @@ impl AskPassSession {
     }
 }
 
+/// The main function for when Zed is running in netcat mode for use in askpass.
+/// Called from both the remote server binary and the zed binary in their respective main functions.
+#[cfg(unix)]
+pub fn main(socket: &str) {
+    use std::io::{self, Read, Write};
+    use std::os::unix::net::UnixStream;
+    use std::process::exit;
+
+    let mut stream = match UnixStream::connect(socket) {
+        Ok(stream) => stream,
+        Err(err) => {
+            eprintln!("Error connecting to socket {}: {}", socket, err);
+            exit(1);
+        }
+    };
+
+    let mut buffer = Vec::new();
+    if let Err(err) = io::stdin().read_to_end(&mut buffer) {
+        eprintln!("Error reading from stdin: {}", err);
+        exit(1);
+    }
+
+    if buffer.last() != Some(&b'\0') {
+        buffer.push(b'\0');
+    }
+
+    if let Err(err) = stream.write_all(&buffer) {
+        eprintln!("Error writing to socket: {}", err);
+        exit(1);
+    }
+
+    let mut response = Vec::new();
+    if let Err(err) = stream.read_to_end(&mut response) {
+        eprintln!("Error reading from socket: {}", err);
+        exit(1);
+    }
+
+    if let Err(err) = io::stdout().write_all(&response) {
+        eprintln!("Error writing to stdout: {}", err);
+        exit(1);
+    }
+}
+#[cfg(not(unix))]
+pub fn main(_socket: &str) {}
+
 #[cfg(not(unix))]
 pub struct AskPassSession {
     path: PathBuf,

crates/remote_server/Cargo.toml 🔗

@@ -23,6 +23,7 @@ test-support = ["fs/test-support"]
 
 [dependencies]
 anyhow.workspace = true
+askpass.workspace = true
 async-watch.workspace = true
 backtrace = "0.3"
 chrono.workspace = true

crates/remote_server/src/main.rs 🔗

@@ -8,6 +8,10 @@ use std::path::PathBuf;
 struct Cli {
     #[command(subcommand)]
     command: Option<Commands>,
+    /// Used for SSH/Git password authentication, to remove the need for netcat as a dependency,
+    /// by having Zed act like netcat communicating over a Unix socket.
+    #[arg(long, hide = true)]
+    askpass: Option<String>,
 }
 
 #[derive(Subcommand)]
@@ -46,6 +50,11 @@ fn main() {
 
     let cli = Cli::parse();
 
+    if let Some(socket_path) = &cli.askpass {
+        askpass::main(socket_path);
+        return;
+    }
+
     let result = match cli.command {
         Some(Commands::Run {
             log_file,

crates/zed/Cargo.toml 🔗

@@ -18,6 +18,7 @@ path = "src/main.rs"
 activity_indicator.workspace = true
 agent.workspace = true
 anyhow.workspace = true
+askpass.workspace = true
 assets.workspace = true
 assistant.workspace = true
 assistant_context_editor.workspace = true

crates/zed/src/main.rs 🔗

@@ -180,6 +180,11 @@ fn main() {
 
     let args = Args::parse();
 
+    if let Some(socket) = &args.askpass {
+        askpass::main(socket);
+        return;
+    }
+
     // Set custom data directory.
     if let Some(dir) = &args.user_data_dir {
         paths::set_custom_data_dir(dir);
@@ -1002,6 +1007,11 @@ struct Args {
     #[arg(long)]
     system_specs: bool,
 
+    /// Used for SSH/Git password authentication, to remove the need for netcat as a dependency,
+    /// by having Zed act like netcat communicating over a Unix socket.
+    #[arg(long, hide = true)]
+    askpass: Option<String>,
+
     /// Run zed in the foreground, only used on Windows, to match the behavior of the behavior on macOS.
     #[arg(long)]
     #[cfg(target_os = "windows")]

script/linux 🔗

@@ -38,7 +38,6 @@ if [[ -n $apt ]]; then
     cmake
     clang
     jq
-    netcat-openbsd
     git
     curl
     gettext-base
@@ -88,14 +87,12 @@ if [[ -n $dnf ]] || [[ -n $yum ]]; then
     tar
   )
   # perl used for building openssl-sys crate. See: https://docs.rs/openssl/latest/openssl/
-  # openbsd-netcat is unavailable in RHEL8/9 (and nmap-ncat doesn't support sockets)
   if grep -qP '^ID="?(fedora)' /etc/os-release; then
     deps+=(
       perl-FindBin
       perl-IPC-Cmd
       perl-File-Compare
       perl-File-Copy
-      netcat
       mold
     )
   elif grep -qP '^ID="?(rhel|rocky|alma|centos|ol)' /etc/os-release; then
@@ -152,7 +149,6 @@ if [[ -n $zyp ]]; then
     libzstd-devel
     make
     mold
-    netcat-openbsd
     openssl-devel
     sqlite3-devel
     tar
@@ -179,7 +175,6 @@ if [[ -n $pacman ]]; then
     libgit2
     libxcb
     libxkbcommon-x11
-    openbsd-netcat
     openssl
     zstd
     pkgconf
@@ -209,7 +204,6 @@ if [[ -n $xbps ]]; then
     libxcb-devel
     libxkbcommon-devel
     libzstd-devel
-    openbsd-netcat
     openssl-devel
     wayland-devel
     vulkan-loader
@@ -234,7 +228,6 @@ if [[ -n $emerge ]]; then
     media-libs/alsa-lib
     media-libs/fontconfig
     media-libs/vulkan-loader
-    net-analyzer/openbsd-netcat
     x11-libs/libxcb
     x11-libs/libxkbcommon
     sys-devel/mold