diff --git a/crates/http_client/src/http_client.rs b/crates/http_client/src/http_client.rs index 1182ef74ca..2e814ccdc3 100644 --- a/crates/http_client/src/http_client.rs +++ b/crates/http_client/src/http_client.rs @@ -10,7 +10,7 @@ pub use http::{self, Method, Request, Response, StatusCode, Uri, request::Builde use futures::future::BoxFuture; use parking_lot::Mutex; -use serde::Serialize; + use std::sync::Arc; #[cfg(feature = "test-support")] use std::{any::type_name, fmt}; @@ -208,63 +208,6 @@ impl HttpClientWithUrl { format!("{}{}", self.base_url(), path) } - /// Builds a Zed API URL using the given path. - pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result { - let base_url = self.base_url(); - let base_api_url = match base_url.as_ref() { - "https://zed.dev" => "https://api.zed.dev", - "https://staging.zed.dev" => "https://api-staging.zed.dev", - "http://localhost:3000" => "http://localhost:8080", - other => other, - }; - - Ok(Url::parse_with_params( - &format!("{}{}", base_api_url, path), - query, - )?) - } - - /// Builds a Zed Cloud URL using the given path. - pub fn build_zed_cloud_url(&self, path: &str) -> Result { - let base_url = self.base_url(); - let base_api_url = match base_url.as_ref() { - "https://zed.dev" => "https://cloud.zed.dev", - "https://staging.zed.dev" => "https://cloud.zed.dev", - "http://localhost:3000" => "http://localhost:8787", - other => other, - }; - - Ok(Url::parse(&format!("{}{}", base_api_url, path))?) - } - - /// Builds a Zed Cloud URL using the given path and query params. - pub fn build_zed_cloud_url_with_query(&self, path: &str, query: impl Serialize) -> Result { - let base_url = self.base_url(); - let base_api_url = match base_url.as_ref() { - "https://zed.dev" => "https://cloud.zed.dev", - "https://staging.zed.dev" => "https://cloud.zed.dev", - "http://localhost:3000" => "http://localhost:8787", - other => other, - }; - let query = serde_urlencoded::to_string(&query)?; - Ok(Url::parse(&format!("{}{}?{}", base_api_url, path, query))?) - } - - /// Builds a Zed LLM URL using the given path. - pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result { - let base_url = self.base_url(); - let base_api_url = match base_url.as_ref() { - "https://zed.dev" => "https://cloud.zed.dev", - "https://staging.zed.dev" => "https://llm-staging.zed.dev", - "http://localhost:3000" => "http://localhost:8787", - other => other, - }; - - Ok(Url::parse_with_params( - &format!("{}{}", base_api_url, path), - query, - )?) - } } impl HttpClient for HttpClientWithUrl { diff --git a/crates/util/src/shell_env.rs b/crates/util/src/shell_env.rs deleted file mode 100644 index c41a28b469..0000000000 --- a/crates/util/src/shell_env.rs +++ /dev/null @@ -1,221 +0,0 @@ -use std::path::Path; - -use anyhow::{Context as _, Result}; -use collections::HashMap; - -use crate::shell::ShellKind; - -pub fn print_env() { - let env_vars: HashMap = std::env::vars().collect(); - let json = serde_json::to_string_pretty(&env_vars).unwrap_or_else(|err| { - eprintln!("Error serializing environment variables: {}", err); - std::process::exit(1); - }); - println!("{}", json); -} - -/// Capture all environment variables from the login shell in the given directory. -pub async fn capture( - shell_path: impl AsRef, - args: &[String], - directory: impl AsRef, -) -> Result> { - #[cfg(windows)] - return capture_windows(shell_path.as_ref(), args, directory.as_ref()).await; - #[cfg(unix)] - return capture_unix(shell_path.as_ref(), args, directory.as_ref()).await; -} - -#[cfg(unix)] -async fn capture_unix( - shell_path: &Path, - args: &[String], - directory: &Path, -) -> Result> { - use std::os::unix::process::CommandExt; - - use crate::command::new_std_command; - - let shell_kind = ShellKind::new(shell_path, false); - let zed_path = super::get_shell_safe_zed_path(shell_kind)?; - - let mut command_string = String::new(); - let mut command = new_std_command(shell_path); - command.args(args); - // In some shells, file descriptors greater than 2 cannot be used in interactive mode, - // so file descriptor 0 (stdin) is used instead. This impacts zsh, old bash; perhaps others. - // See: https://github.com/zed-industries/zed/pull/32136#issuecomment-2999645482 - const FD_STDIN: std::os::fd::RawFd = 0; - const FD_STDOUT: std::os::fd::RawFd = 1; - const FD_STDERR: std::os::fd::RawFd = 2; - - let (fd_num, redir) = match shell_kind { - ShellKind::Rc => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]` - ShellKind::Nushell | ShellKind::Tcsh => (FD_STDOUT, "".to_string()), - // xonsh doesn't support redirecting to stdin, and control sequences are printed to - // stdout on startup - ShellKind::Xonsh => (FD_STDERR, "o>e".to_string()), - ShellKind::PowerShell => (FD_STDIN, format!(">{}", FD_STDIN)), - _ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0` - }; - - match shell_kind { - ShellKind::Csh | ShellKind::Tcsh => { - // For csh/tcsh, login shell requires passing `-` as 0th argument (instead of `-l`) - command.arg0("-"); - } - ShellKind::Fish => { - // in fish, asdf, direnv attach to the `fish_prompt` event - command_string.push_str("emit fish_prompt;"); - command.arg("-l"); - } - _ => { - command.arg("-l"); - } - } - // cd into the directory, triggering directory specific side-effects (asdf, direnv, etc) - command_string.push_str(&format!("cd '{}';", directory.display())); - if let Some(prefix) = shell_kind.command_prefix() { - command_string.push(prefix); - } - command_string.push_str(&format!("{} --printenv {}", zed_path, redir)); - command.args(["-i", "-c", &command_string]); - - super::set_pre_exec_to_start_new_session(&mut command); - - let (env_output, process_output) = spawn_and_read_fd(command, fd_num).await?; - let env_output = String::from_utf8_lossy(&env_output); - - anyhow::ensure!( - process_output.status.success(), - "login shell exited with {}. stdout: {:?}, stderr: {:?}", - process_output.status, - String::from_utf8_lossy(&process_output.stdout), - String::from_utf8_lossy(&process_output.stderr), - ); - - // Parse the JSON output from zed --printenv - let env_map: collections::HashMap = serde_json::from_str(&env_output) - .with_context(|| { - format!("Failed to deserialize environment variables from json: {env_output}") - })?; - Ok(env_map) -} - -#[cfg(unix)] -async fn spawn_and_read_fd( - mut command: std::process::Command, - child_fd: std::os::fd::RawFd, -) -> anyhow::Result<(Vec, std::process::Output)> { - use command_fds::{CommandFdExt, FdMapping}; - use std::{io::Read, process::Stdio}; - - let (mut reader, writer) = std::io::pipe()?; - - command.fd_mappings(vec![FdMapping { - parent_fd: writer.into(), - child_fd, - }])?; - - let process = smol::process::Command::from(command) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; - - let mut buffer = Vec::new(); - reader.read_to_end(&mut buffer)?; - - Ok((buffer, process.output().await?)) -} - -#[cfg(windows)] -async fn capture_windows( - shell_path: &Path, - args: &[String], - directory: &Path, -) -> Result> { - use std::process::Stdio; - - let zed_path = - 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); - cmd.args(args); - let cmd = match shell_kind { - ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::Xonsh - | ShellKind::Posix => cmd.args([ - "-l", - "-i", - "-c", - &format!( - "cd '{}'; '{}' --printenv", - directory.display(), - zed_path.display() - ), - ]), - ShellKind::PowerShell | ShellKind::Pwsh => cmd.args([ - "-NonInteractive", - "-NoProfile", - "-Command", - &format!( - "Set-Location '{}'; & '{}' --printenv", - directory.display(), - zed_path.display() - ), - ]), - ShellKind::Elvish => cmd.args([ - "-c", - &format!( - "cd '{}'; '{}' --printenv", - directory.display(), - zed_path.display() - ), - ]), - ShellKind::Nushell => cmd.args([ - "-c", - &format!( - "cd '{}'; {}'{}' --printenv", - directory.display(), - shell_kind - .command_prefix() - .map(|prefix| prefix.to_string()) - .unwrap_or_default(), - zed_path.display() - ), - ]), - ShellKind::Cmd => cmd.args([ - "/c", - "cd", - &directory.display().to_string(), - "&&", - &zed_path.display().to_string(), - "--printenv", - ]), - } - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - let output = cmd - .output() - .await - .with_context(|| format!("command {cmd:?}"))?; - anyhow::ensure!( - output.status.success(), - "Command {cmd:?} failed with {}. stdout: {:?}, stderr: {:?}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - let env_output = String::from_utf8_lossy(&output.stdout); - - // Parse the JSON output from zed --printenv - serde_json::from_str(&env_output).with_context(|| { - format!("Failed to deserialize environment variables from json: {env_output}") - }) -} diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 4ea3590196..d7383a4975 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -10,18 +10,17 @@ pub mod schemars; pub mod serde; pub mod shell; pub mod shell_builder; -pub mod shell_env; pub mod size; #[cfg(any(test, feature = "test-support"))] pub mod test; pub mod time; -use anyhow::{Context as _, Result}; +use anyhow::Result; use futures::Future; use itertools::Either; -use paths::PathExt; + use regex::Regex; -use std::path::PathBuf; + use std::sync::{LazyLock, OnceLock}; use std::{ borrow::Cow, @@ -224,157 +223,7 @@ where items.sort_by(compare); } -/// Prevents execution of the application with root privileges on Unix systems. -/// -/// This function checks if the current process is running with root privileges -/// and terminates the program with an error message unless explicitly allowed via the -/// `ZED_ALLOW_ROOT` environment variable. -#[cfg(unix)] -pub fn prevent_root_execution() { - let is_root = nix::unistd::geteuid().is_root(); - let allow_root = std::env::var("ZED_ALLOW_ROOT").is_ok_and(|val| val == "true"); - if is_root && !allow_root { - eprintln!( - "\ -Error: Running Zed as root or via sudo is unsupported. - Doing so (even once) may subtly break things for all subsequent non-root usage of Zed. - It is untested and not recommended, don't complain when things break. - If you wish to proceed anyways, set `ZED_ALLOW_ROOT=true` in your environment." - ); - std::process::exit(1); - } -} - -#[cfg(unix)] -fn load_shell_from_passwd() -> Result<()> { - let buflen = match unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) } { - n if n < 0 => 1024, - n => n as usize, - }; - let mut buffer = Vec::with_capacity(buflen); - - let mut pwd: std::mem::MaybeUninit = std::mem::MaybeUninit::uninit(); - let mut result: *mut libc::passwd = std::ptr::null_mut(); - - let uid = unsafe { libc::getuid() }; - let status = unsafe { - libc::getpwuid_r( - uid, - pwd.as_mut_ptr(), - buffer.as_mut_ptr() as *mut libc::c_char, - buflen, - &mut result, - ) - }; - anyhow::ensure!(!result.is_null(), "passwd entry for uid {} not found", uid); - - // SAFETY: If `getpwuid_r` doesn't error, we have the entry here. - let entry = unsafe { pwd.assume_init() }; - - anyhow::ensure!( - status == 0, - "call to getpwuid_r failed. uid: {}, status: {}", - uid, - status - ); - anyhow::ensure!( - entry.pw_uid == uid, - "passwd entry has different uid ({}) than getuid ({}) returned", - entry.pw_uid, - uid, - ); - - let shell = unsafe { std::ffi::CStr::from_ptr(entry.pw_shell).to_str().unwrap() }; - let should_set_shell = env::var("SHELL").map_or(true, |shell_env| { - shell_env != shell && !std::path::Path::new(&shell_env).exists() - }); - - if should_set_shell { - log::info!( - "updating SHELL environment variable to value from passwd entry: {:?}", - shell, - ); - unsafe { env::set_var("SHELL", shell) }; - } - - Ok(()) -} - -/// Returns a shell escaped path for the current zed executable -pub fn get_shell_safe_zed_path(shell_kind: shell::ShellKind) -> anyhow::Result { - let zed_path = - std::env::current_exe().context("Failed to determine current zed executable path.")?; - - zed_path - .try_shell_safe(shell_kind) - .context("Failed to shell-escape Zed executable path.") -} - -/// Returns a path for the zed cli executable, this function -/// should be called from the zed executable, not zed-cli. -pub fn get_zed_cli_path() -> Result { - let zed_path = - std::env::current_exe().context("Failed to determine current zed executable path.")?; - let parent = zed_path - .parent() - .context("Failed to determine parent directory of zed executable path.")?; - - let possible_locations: &[&str] = if cfg!(target_os = "macos") { - // On macOS, the zed executable and zed-cli are inside the app bundle, - // so here ./cli is for both installed and development builds. - &["./cli"] - } else if cfg!(target_os = "windows") { - // bin/zed.exe is for installed builds, ./cli.exe is for development builds. - &["bin/zed.exe", "./cli.exe"] - } else if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") { - // bin is the standard, ./cli is for the target directory in development builds. - &["../bin/zed", "./cli"] - } else { - anyhow::bail!("unsupported platform for determining zed-cli path"); - }; - - possible_locations - .iter() - .find_map(|p| { - parent - .join(p) - .canonicalize() - .ok() - .filter(|p| p != &zed_path) - }) - .with_context(|| { - format!( - "could not find zed-cli from any of: {}", - possible_locations.join(", ") - ) - }) -} - -#[cfg(unix)] -pub async fn load_login_shell_environment() -> Result<()> { - load_shell_from_passwd().log_err(); - - // If possible, we want to `cd` in the user's `$HOME` to trigger programs - // such as direnv, asdf, mise, ... to adjust the PATH. These tools often hook - // into shell's `cd` command (and hooks) to manipulate env. - // We do this so that we get the env a user would have when spawning a shell - // in home directory. - for (name, value) in shell_env::capture(get_system_shell(), &[], paths::home_dir()) - .await - .with_context(|| format!("capturing environment with {:?}", get_system_shell()))? - { - unsafe { env::set_var(&name, &value) }; - } - - log::info!( - "set environment variables from shell:{}, path:{}", - std::env::var("SHELL").unwrap_or_default(), - std::env::var("PATH").unwrap_or_default(), - ); - - Ok(()) -} /// Configures the process to start a new session, to prevent interactive shells from taking control /// of the terminal. diff --git a/script/install-linux b/script/install-linux deleted file mode 100755 index a642ed2e0e..0000000000 --- a/script/install-linux +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail - -if [[ $# -gt 0 ]]; then - echo " - Usage: ${0##*/} - Builds and installs zed onto your system into ~/.local, making it available as ~/.local/bin/zed. - - Before running this you should ensure you have all the build dependencies installed with `./script/linux`. - " - exit 1 -fi -export ZED_CHANNEL=$(