977 lines · 33.2 KB · Rust
RawDownload
#![cfg_attr(    any(target_os = "linux", target_os = "freebsd", target_os = "windows"),    allow(dead_code))] use anyhow::{Context as _, Result};use clap::Parser;use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};use collections::HashMap;use parking_lot::Mutex;use std::{    env, fs, io,    path::{Path, PathBuf},    process::ExitStatus,    sync::Arc,    thread::{self, JoinHandle},};use tempfile::NamedTempFile;use util::paths::PathWithPosition; #[cfg(any(target_os = "linux", target_os = "freebsd"))]use std::io::IsTerminal; struct Detect; trait InstalledApp {    fn zed_version_string(&self) -> String;    fn launch(&self, ipc_url: String) -> anyhow::Result<()>;    fn run_foreground(        &self,        ipc_url: String,        user_data_dir: Option<&str>,    ) -> io::Result<ExitStatus>;    fn path(&self) -> PathBuf;} #[derive(Parser, Debug)]#[command(    name = "zed",    disable_version_flag = true,    before_help = "The Zed CLI binary.This CLI is a separate binary that invokes Zed. Examples:    `zed`          Simply opens Zed    `zed --foreground`          Runs in foreground (shows all logs)    `zed path-to-your-project`          Open your project in Zed    `zed -n path-to-file `          Open file/folder in a new window",    after_help = "To read from stdin, append '-', e.g. 'ps axf | zed -'")]struct Args {    /// Wait for all of the given paths to be opened/closed before exiting.    #[arg(short, long)]    wait: bool,    /// Add files to the currently open workspace    #[arg(short, long, overrides_with = "new")]    add: bool,    /// Create a new workspace    #[arg(short, long, overrides_with = "add")]    new: bool,    /// Sets a custom directory for all user data (e.g., database, extensions, logs).    /// This overrides the default platform-specific data directory location.    /// On macOS, the default is `~/Library/Application Support/Zed`.    /// On Linux/FreeBSD, the default is `$XDG_DATA_HOME/zed`.    /// On Windows, the default is `%LOCALAPPDATA%\Zed`.    #[arg(long, value_name = "DIR")]    user_data_dir: Option<String>,    /// The paths to open in Zed (space-separated).    ///    /// Use `path:line:column` syntax to open a file at the given line and column.    paths_with_position: Vec<String>,    /// Print Zed's version and the app path.    #[arg(short, long)]    version: bool,    /// Run zed in the foreground (useful for debugging)    #[arg(long)]    foreground: bool,    /// Custom path to Zed.app or the zed binary    #[arg(long)]    zed: Option<PathBuf>,    /// Run zed in dev-server mode    #[arg(long)]    dev_server_token: Option<String>,    /// Not supported in Zed CLI, only supported on Zed binary    /// Will attempt to give the correct command to run    #[arg(long)]    system_specs: bool,    /// Uninstall Zed from user system    #[cfg(all(        any(target_os = "linux", target_os = "macos"),        not(feature = "no-bundled-uninstall")    ))]    #[arg(long)]    uninstall: bool,} fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {    let canonicalized = match Path::new(argument_str).canonicalize() {        Ok(existing_path) => PathWithPosition::from_path(existing_path),        Err(_) => {            let path = PathWithPosition::parse_str(argument_str);            let curdir = env::current_dir().context("retrieving current directory")?;            path.map_path(|path| match fs::canonicalize(&path) {                Ok(path) => Ok(path),                Err(e) => {                    if let Some(mut parent) = path.parent() {                        if parent == Path::new("") {                            parent = &curdir                        }                        match fs::canonicalize(parent) {                            Ok(parent) => Ok(parent.join(path.file_name().unwrap())),                            Err(_) => Err(e),                        }                    } else {                        Err(e)                    }                }            })        }        .with_context(|| format!("parsing as path with position {argument_str}"))?,    };    Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string()))} fn main() -> Result<()> {    // Exit flatpak sandbox if needed    #[cfg(any(target_os = "linux", target_os = "freebsd"))]    {        flatpak::try_restart_to_host();        flatpak::ld_extra_libs();    }     // Intercept version designators    #[cfg(target_os = "macos")]    if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {        // When the first argument is a name of a release channel, we're going to spawn off the CLI of that version, with trailing args passed along.        use std::str::FromStr as _;         if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {            return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());        }    }    let args = Args::parse();     // Set custom data directory before any path operations    let user_data_dir = args.user_data_dir.clone();    if let Some(dir) = &user_data_dir {        paths::set_custom_data_dir(dir);    }     #[cfg(any(target_os = "linux", target_os = "freebsd"))]    let args = flatpak::set_bin_if_no_escape(args);     let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;     if args.version {        println!("{}", app.zed_version_string());        return Ok(());    }     if args.system_specs {        let path = app.path();        let msg = [            "The `--system-specs` argument is not supported in the Zed CLI, only on Zed binary.",            "To retrieve the system specs on the command line, run the following command:",            &format!("{} --system-specs", path.display()),        ];        anyhow::bail!(msg.join("\n"));    }     #[cfg(all(        any(target_os = "linux", target_os = "macos"),        not(feature = "no-bundled-uninstall")    ))]    if args.uninstall {        static UNINSTALL_SCRIPT: &[u8] = include_bytes!("../../../script/uninstall.sh");         let tmp_dir = tempfile::tempdir()?;        let script_path = tmp_dir.path().join("uninstall.sh");        fs::write(&script_path, UNINSTALL_SCRIPT)?;         use std::os::unix::fs::PermissionsExt as _;        fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?;         let status = std::process::Command::new("sh")            .arg(&script_path)            .env("ZED_CHANNEL", &*release_channel::RELEASE_CHANNEL_NAME)            .status()            .context("Failed to execute uninstall script")?;         std::process::exit(status.code().unwrap_or(1));    }     let (server, server_name) =        IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;    let url = format!("zed-cli://{server_name}");     let open_new_workspace = if args.new {        Some(true)    } else if args.add {        Some(false)    } else {        None    };     let env = {        #[cfg(any(target_os = "linux", target_os = "freebsd"))]        {            // On Linux, the desktop entry uses `cli` to spawn `zed`.            // We need to handle env vars correctly since std::env::vars() may not contain            // project-specific vars (e.g. those set by direnv).            // By setting env to None here, the LSP will use worktree env vars instead,            // which is what we want.            if !std::io::stdout().is_terminal() {                None            } else {                Some(std::env::vars().collect::<HashMap<_, _>>())            }        }         #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]        Some(std::env::vars().collect::<HashMap<_, _>>())    };     let exit_status = Arc::new(Mutex::new(None));    let mut paths = vec![];    let mut urls = vec![];    let mut stdin_tmp_file: Option<fs::File> = None;    let mut anonymous_fd_tmp_files = vec![];     for path in args.paths_with_position.iter() {        if path.starts_with("zed://")            || path.starts_with("http://")            || path.starts_with("https://")            || path.starts_with("file://")            || path.starts_with("ssh://")        {            urls.push(path.to_string());        } else if path == "-" && args.paths_with_position.len() == 1 {            let file = NamedTempFile::new()?;            paths.push(file.path().to_string_lossy().to_string());            let (file, _) = file.keep()?;            stdin_tmp_file = Some(file);        } else if let Some(file) = anonymous_fd(path) {            let tmp_file = NamedTempFile::new()?;            paths.push(tmp_file.path().to_string_lossy().to_string());            let (tmp_file, _) = tmp_file.keep()?;            anonymous_fd_tmp_files.push((file, tmp_file));        } else {            paths.push(parse_path_with_position(path)?)        }    }     anyhow::ensure!(        args.dev_server_token.is_none(),        "Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development"    );     let sender: JoinHandle<anyhow::Result<()>> = thread::spawn({        let exit_status = exit_status.clone();        let user_data_dir_for_thread = user_data_dir.clone();        move || {            let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;            let (tx, rx) = (handshake.requests, handshake.responses);             tx.send(CliRequest::Open {                paths,                urls,                wait: args.wait,                open_new_workspace,                env,                user_data_dir: user_data_dir_for_thread,            })?;             while let Ok(response) = rx.recv() {                match response {                    CliResponse::Ping => {}                    CliResponse::Stdout { message } => println!("{message}"),                    CliResponse::Stderr { message } => eprintln!("{message}"),                    CliResponse::Exit { status } => {                        exit_status.lock().replace(status);                        return Ok(());                    }                }            }             Ok(())        }    });     let stdin_pipe_handle: Option<JoinHandle<anyhow::Result<()>>> =        stdin_tmp_file.map(|tmp_file| {            thread::spawn(move || {                let stdin = std::io::stdin().lock();                if io::IsTerminal::is_terminal(&stdin) {                    return Ok(());                }                return pipe_to_tmp(stdin, tmp_file);            })        });     let anonymous_fd_pipe_handles: Vec<JoinHandle<anyhow::Result<()>>> = anonymous_fd_tmp_files        .into_iter()        .map(|(file, tmp_file)| thread::spawn(move || pipe_to_tmp(file, tmp_file)))        .collect();     if args.foreground {        app.run_foreground(url, user_data_dir.as_deref())?;    } else {        app.launch(url)?;        sender.join().unwrap()?;        if let Some(handle) = stdin_pipe_handle {            handle.join().unwrap()?;        }        for handle in anonymous_fd_pipe_handles {            handle.join().unwrap()?;        }    }     if let Some(exit_status) = exit_status.lock().take() {        std::process::exit(exit_status);    }    Ok(())} fn pipe_to_tmp(mut src: impl io::Read, mut dest: fs::File) -> Result<()> {    let mut buffer = [0; 8 * 1024];    loop {        let bytes_read = match src.read(&mut buffer) {            Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,            res => res?,        };        if bytes_read == 0 {            break;        }        io::Write::write_all(&mut dest, &buffer[..bytes_read])?;    }    io::Write::flush(&mut dest)?;    Ok(())} fn anonymous_fd(path: &str) -> Option<fs::File> {    #[cfg(target_os = "linux")]    {        use std::os::fd::{self, FromRawFd};         let fd_str = path.strip_prefix("/proc/self/fd/")?;         let link = fs::read_link(path).ok()?;        if !link.starts_with("memfd:") {            return None;        }         let fd: fd::RawFd = fd_str.parse().ok()?;        let file = unsafe { fs::File::from_raw_fd(fd) };        return Some(file);    }    #[cfg(target_os = "macos")]    {        use std::os::{            fd::{self, FromRawFd},            unix::fs::FileTypeExt,        };         let fd_str = path.strip_prefix("/dev/fd/")?;         let metadata = fs::metadata(path).ok()?;        let file_type = metadata.file_type();        if !file_type.is_fifo() && !file_type.is_socket() {            return None;        }        let fd: fd::RawFd = fd_str.parse().ok()?;        let file = unsafe { fs::File::from_raw_fd(fd) };        return Some(file);    }    #[cfg(not(any(target_os = "linux", target_os = "macos")))]    {        _ = path;        // not implemented for bsd, windows. Could be, but isn't yet        return None;    }} #[cfg(any(target_os = "linux", target_os = "freebsd"))]mod linux {    use std::{        env,        ffi::OsString,        io,        os::unix::net::{SocketAddr, UnixDatagram},        path::{Path, PathBuf},        process::{self, ExitStatus},        sync::LazyLock,        thread,        time::Duration,    };     use anyhow::{Context as _, anyhow};    use cli::FORCE_CLI_MODE_ENV_VAR_NAME;    use fork::Fork;     use crate::{Detect, InstalledApp};     static RELEASE_CHANNEL: LazyLock<String> =        LazyLock::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());     struct App(PathBuf);     impl Detect {        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {            let path = if let Some(path) = path {                path.to_path_buf().canonicalize()?            } else {                let cli = env::current_exe()?;                let dir = cli.parent().context("no parent path for cli")?;                 // libexec is the standard, lib/zed is for Arch (and other non-libexec distros),                // ./zed is for the target directory in development builds.                let possible_locations =                    ["../libexec/zed-editor", "../lib/zed/zed-editor", "./zed"];                possible_locations                    .iter()                    .find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))                    .with_context(|| {                        format!("could not find any of: {}", possible_locations.join(", "))                    })?            };             Ok(App(path))        }    }     impl InstalledApp for App {        fn zed_version_string(&self) -> String {            format!(                "Zed {}{}{} – {}",                if *RELEASE_CHANNEL == "stable" {                    "".to_string()                } else {                    format!("{} ", *RELEASE_CHANNEL)                },                option_env!("RELEASE_VERSION").unwrap_or_default(),                match option_env!("ZED_COMMIT_SHA") {                    Some(commit_sha) => format!(" {commit_sha} "),                    None => "".to_string(),                },                self.0.display(),            )        }         fn launch(&self, ipc_url: String) -> anyhow::Result<()> {            let sock_path = paths::data_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL));            let sock = UnixDatagram::unbound()?;            if sock.connect(&sock_path).is_err() {                self.boot_background(ipc_url)?;            } else {                sock.send(ipc_url.as_bytes())?;            }            Ok(())        }         fn run_foreground(            &self,            ipc_url: String,            user_data_dir: Option<&str>,        ) -> io::Result<ExitStatus> {            let mut cmd = std::process::Command::new(self.0.clone());            cmd.arg(ipc_url);            if let Some(dir) = user_data_dir {                cmd.arg("--user-data-dir").arg(dir);            }            cmd.status()        }         fn path(&self) -> PathBuf {            self.0.clone()        }    }     impl App {        fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {            let path = &self.0;             match fork::fork() {                Ok(Fork::Parent(_)) => Ok(()),                Ok(Fork::Child) => {                    unsafe { std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "") };                    if let Err(_) = fork::setsid() {                        eprintln!("failed to setsid: {}", std::io::Error::last_os_error());                        process::exit(1);                    }                    if let Err(_) = fork::close_fd() {                        eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());                    }                    let error =                        exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);

Lines 1–500 of 977.