main.rs

   1#![allow(
   2    clippy::disallowed_methods,
   3    reason = "We are not in an async environment, so std::process::Command is fine"
   4)]
   5#![cfg_attr(
   6    any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
   7    allow(dead_code)
   8)]
   9
  10use anyhow::{Context as _, Result};
  11use clap::Parser;
  12use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};
  13use parking_lot::Mutex;
  14use std::{
  15    env,
  16    ffi::OsStr,
  17    fs, io,
  18    path::{Path, PathBuf},
  19    process::ExitStatus,
  20    sync::Arc,
  21    thread::{self, JoinHandle},
  22};
  23use tempfile::NamedTempFile;
  24use util::paths::PathWithPosition;
  25
  26#[cfg(any(target_os = "linux", target_os = "freebsd"))]
  27use std::io::IsTerminal;
  28
  29const URL_PREFIX: [&'static str; 5] = ["zed://", "http://", "https://", "file://", "ssh://"];
  30
  31struct Detect;
  32
  33trait InstalledApp {
  34    fn zed_version_string(&self) -> String;
  35    fn launch(&self, ipc_url: String, user_data_dir: Option<&str>) -> anyhow::Result<()>;
  36    fn run_foreground(
  37        &self,
  38        ipc_url: String,
  39        user_data_dir: Option<&str>,
  40    ) -> io::Result<ExitStatus>;
  41    fn path(&self) -> PathBuf;
  42}
  43
  44#[derive(Parser, Debug)]
  45#[command(
  46    name = "zed",
  47    disable_version_flag = true,
  48    before_help = "The Zed CLI binary.
  49This CLI is a separate binary that invokes Zed.
  50
  51Examples:
  52    `zed`
  53          Simply opens Zed
  54    `zed --foreground`
  55          Runs in foreground (shows all logs)
  56    `zed path-to-your-project`
  57          Open your project in Zed
  58    `zed -n path-to-file `
  59          Open file/folder in a new window",
  60    after_help = "To read from stdin, append '-', e.g. 'ps axf | zed -'"
  61)]
  62struct Args {
  63    /// Wait for all of the given paths to be opened/closed before exiting.
  64    #[arg(short, long)]
  65    wait: bool,
  66    /// Add files to the currently open workspace
  67    #[arg(short, long, overrides_with_all = ["new", "reuse"])]
  68    add: bool,
  69    /// Create a new workspace
  70    #[arg(short, long, overrides_with_all = ["add", "reuse"])]
  71    new: bool,
  72    /// Reuse an existing window, replacing its workspace
  73    #[arg(short, long, overrides_with_all = ["add", "new"])]
  74    reuse: bool,
  75    /// Sets a custom directory for all user data (e.g., database, extensions, logs).
  76    /// This overrides the default platform-specific data directory location:
  77    #[cfg_attr(target_os = "macos", doc = "`~/Library/Application Support/Zed`.")]
  78    #[cfg_attr(target_os = "windows", doc = "`%LOCALAPPDATA%\\Zed`.")]
  79    #[cfg_attr(
  80        not(any(target_os = "windows", target_os = "macos")),
  81        doc = "`$XDG_DATA_HOME/zed`."
  82    )]
  83    #[arg(long, value_name = "DIR")]
  84    user_data_dir: Option<String>,
  85    /// The paths to open in Zed (space-separated).
  86    ///
  87    /// Use `path:line:column` syntax to open a file at the given line and column.
  88    paths_with_position: Vec<String>,
  89    /// Print Zed's version and the app path.
  90    #[arg(short, long)]
  91    version: bool,
  92    /// Run zed in the foreground (useful for debugging)
  93    #[arg(long)]
  94    foreground: bool,
  95    /// Custom path to Zed.app or the zed binary
  96    #[arg(long)]
  97    zed: Option<PathBuf>,
  98    /// Run zed in dev-server mode
  99    #[arg(long)]
 100    dev_server_token: Option<String>,
 101    /// The username and WSL distribution to use when opening paths. If not specified,
 102    /// Zed will attempt to open the paths directly.
 103    ///
 104    /// The username is optional, and if not specified, the default user for the distribution
 105    /// will be used.
 106    ///
 107    /// Example: `me@Ubuntu` or `Ubuntu`.
 108    ///
 109    /// WARN: You should not fill in this field by hand.
 110    #[cfg(target_os = "windows")]
 111    #[arg(long, value_name = "USER@DISTRO")]
 112    wsl: Option<String>,
 113    /// Not supported in Zed CLI, only supported on Zed binary
 114    /// Will attempt to give the correct command to run
 115    #[arg(long)]
 116    system_specs: bool,
 117    /// Pairs of file paths to diff. Can be specified multiple times.
 118    #[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])]
 119    diff: Vec<String>,
 120    /// Uninstall Zed from user system
 121    #[cfg(all(
 122        any(target_os = "linux", target_os = "macos"),
 123        not(feature = "no-bundled-uninstall")
 124    ))]
 125    #[arg(long)]
 126    uninstall: bool,
 127
 128    /// Used for SSH/Git password authentication, to remove the need for netcat as a dependency,
 129    /// by having Zed act like netcat communicating over a Unix socket.
 130    #[arg(long, hide = true)]
 131    askpass: Option<String>,
 132}
 133
 134/// Parses a path containing a position (e.g. `path:line:column`)
 135/// and returns its canonicalized string representation.
 136///
 137/// If a part of path doesn't exist, it will canonicalize the
 138/// existing part and append the non-existing part.
 139///
 140/// This method must return an absolute path, as many zed
 141/// crates assume absolute paths.
 142fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
 143    match Path::new(argument_str).canonicalize() {
 144        Ok(existing_path) => Ok(PathWithPosition::from_path(existing_path)),
 145        Err(_) => PathWithPosition::parse_str(argument_str).map_path(|mut path| {
 146            let curdir = env::current_dir().context("retrieving current directory")?;
 147            let mut children = Vec::new();
 148            let root;
 149            loop {
 150                // canonicalize handles './', and '/'.
 151                if let Ok(canonicalized) = fs::canonicalize(&path) {
 152                    root = canonicalized;
 153                    break;
 154                }
 155                // The comparison to `curdir` is just a shortcut
 156                // since we know it is canonical. The other one
 157                // is if `argument_str` is a string that starts
 158                // with a name (e.g. "foo/bar").
 159                if path == curdir || path == Path::new("") {
 160                    root = curdir;
 161                    break;
 162                }
 163                children.push(
 164                    path.file_name()
 165                        .with_context(|| format!("parsing as path with position {argument_str}"))?
 166                        .to_owned(),
 167                );
 168                if !path.pop() {
 169                    unreachable!("parsing as path with position {argument_str}");
 170                }
 171            }
 172            Ok(children.iter().rev().fold(root, |mut path, child| {
 173                path.push(child);
 174                path
 175            }))
 176        }),
 177    }
 178    .map(|path_with_pos| path_with_pos.to_string(|path| path.to_string_lossy().into_owned()))
 179}
 180
 181#[cfg(test)]
 182mod tests {
 183    use super::*;
 184    use serde_json::json;
 185    use util::path;
 186    use util::paths::SanitizedPath;
 187    use util::test::TempTree;
 188
 189    macro_rules! assert_path_eq {
 190        ($left:expr, $right:expr) => {
 191            assert_eq!(
 192                SanitizedPath::new(Path::new(&$left)),
 193                SanitizedPath::new(Path::new(&$right))
 194            )
 195        };
 196    }
 197
 198    fn cwd() -> PathBuf {
 199        env::current_dir().unwrap()
 200    }
 201
 202    static CWD_LOCK: Mutex<()> = Mutex::new(());
 203
 204    fn with_cwd<T>(path: &Path, f: impl FnOnce() -> anyhow::Result<T>) -> anyhow::Result<T> {
 205        let _lock = CWD_LOCK.lock();
 206        let old_cwd = cwd();
 207        env::set_current_dir(path)?;
 208        let result = f();
 209        env::set_current_dir(old_cwd)?;
 210        result
 211    }
 212
 213    #[test]
 214    fn test_parse_non_existing_path() {
 215        // Absolute path
 216        let result = parse_path_with_position(path!("/non/existing/path.txt")).unwrap();
 217        assert_path_eq!(result, path!("/non/existing/path.txt"));
 218
 219        // Absolute path in cwd
 220        let path = cwd().join(path!("non/existing/path.txt"));
 221        let expected = path.to_string_lossy().to_string();
 222        let result = parse_path_with_position(&expected).unwrap();
 223        assert_path_eq!(result, expected);
 224
 225        // Relative path
 226        let result = parse_path_with_position(path!("non/existing/path.txt")).unwrap();
 227        assert_path_eq!(result, expected)
 228    }
 229
 230    #[test]
 231    fn test_parse_existing_path() {
 232        let temp_tree = TempTree::new(json!({
 233            "file.txt": "",
 234        }));
 235        let file_path = temp_tree.path().join("file.txt");
 236        let expected = file_path.to_string_lossy().to_string();
 237
 238        // Absolute path
 239        let result = parse_path_with_position(file_path.to_str().unwrap()).unwrap();
 240        assert_path_eq!(result, expected);
 241
 242        // Relative path
 243        let result = with_cwd(temp_tree.path(), || parse_path_with_position("file.txt")).unwrap();
 244        assert_path_eq!(result, expected);
 245    }
 246
 247    // NOTE:
 248    // While POSIX symbolic links are somewhat supported on Windows, they are an opt in by the user, and thus
 249    // we assume that they are not supported out of the box.
 250    #[cfg(not(windows))]
 251    #[test]
 252    fn test_parse_symlink_file() {
 253        let temp_tree = TempTree::new(json!({
 254            "target.txt": "",
 255        }));
 256        let target_path = temp_tree.path().join("target.txt");
 257        let symlink_path = temp_tree.path().join("symlink.txt");
 258        std::os::unix::fs::symlink(&target_path, &symlink_path).unwrap();
 259
 260        // Absolute path
 261        let result = parse_path_with_position(symlink_path.to_str().unwrap()).unwrap();
 262        assert_eq!(result, target_path.to_string_lossy());
 263
 264        // Relative path
 265        let result =
 266            with_cwd(temp_tree.path(), || parse_path_with_position("symlink.txt")).unwrap();
 267        assert_eq!(result, target_path.to_string_lossy());
 268    }
 269
 270    #[cfg(not(windows))]
 271    #[test]
 272    fn test_parse_symlink_dir() {
 273        let temp_tree = TempTree::new(json!({
 274            "some": {
 275                "dir": { // symlink target
 276                    "ec": {
 277                        "tory": {
 278                            "file.txt": "",
 279        }}}}}));
 280
 281        let target_file_path = temp_tree.path().join("some/dir/ec/tory/file.txt");
 282        let expected = target_file_path.to_string_lossy();
 283
 284        let dir_path = temp_tree.path().join("some/dir");
 285        let symlink_path = temp_tree.path().join("symlink");
 286        std::os::unix::fs::symlink(&dir_path, &symlink_path).unwrap();
 287
 288        // Absolute path
 289        let result =
 290            parse_path_with_position(symlink_path.join("ec/tory/file.txt").to_str().unwrap())
 291                .unwrap();
 292        assert_eq!(result, expected);
 293
 294        // Relative path
 295        let result = with_cwd(temp_tree.path(), || {
 296            parse_path_with_position("symlink/ec/tory/file.txt")
 297        })
 298        .unwrap();
 299        assert_eq!(result, expected);
 300    }
 301}
 302
 303fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
 304    let mut source = PathWithPosition::parse_str(source);
 305
 306    let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
 307        if user.is_empty() {
 308            anyhow::bail!("user is empty in wsl argument");
 309        }
 310        (Some(user), distro)
 311    } else {
 312        (None, wsl)
 313    };
 314
 315    let mut args = vec!["--distribution", distro_name];
 316    if let Some(user) = user {
 317        args.push("--user");
 318        args.push(user);
 319    }
 320
 321    let command = [
 322        OsStr::new("realpath"),
 323        OsStr::new("-s"),
 324        source.path.as_ref(),
 325    ];
 326
 327    let output = util::command::new_std_command("wsl.exe")
 328        .args(&args)
 329        .arg("--exec")
 330        .args(&command)
 331        .output()?;
 332    let result = if output.status.success() {
 333        String::from_utf8_lossy(&output.stdout).to_string()
 334    } else {
 335        let fallback = util::command::new_std_command("wsl.exe")
 336            .args(&args)
 337            .arg("--")
 338            .args(&command)
 339            .output()?;
 340        String::from_utf8_lossy(&fallback.stdout).to_string()
 341    };
 342
 343    source.path = Path::new(result.trim()).to_owned();
 344
 345    Ok(source.to_string(|path| path.to_string_lossy().into_owned()))
 346}
 347
 348fn main() -> Result<()> {
 349    #[cfg(unix)]
 350    util::prevent_root_execution();
 351
 352    // Exit flatpak sandbox if needed
 353    #[cfg(target_os = "linux")]
 354    {
 355        flatpak::try_restart_to_host();
 356        flatpak::ld_extra_libs();
 357    }
 358
 359    // Intercept version designators
 360    #[cfg(target_os = "macos")]
 361    if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {
 362        // 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.
 363        use std::str::FromStr as _;
 364
 365        if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {
 366            return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());
 367        }
 368    }
 369    let args = Args::parse();
 370
 371    // `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass
 372    if let Some(socket) = &args.askpass {
 373        askpass::main(socket);
 374        return Ok(());
 375    }
 376
 377    // Set custom data directory before any path operations
 378    let user_data_dir = args.user_data_dir.clone();
 379    if let Some(dir) = &user_data_dir {
 380        paths::set_custom_data_dir(dir);
 381    }
 382
 383    #[cfg(target_os = "linux")]
 384    let args = flatpak::set_bin_if_no_escape(args);
 385
 386    let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
 387
 388    if args.version {
 389        println!("{}", app.zed_version_string());
 390        return Ok(());
 391    }
 392
 393    if args.system_specs {
 394        let path = app.path();
 395        let msg = [
 396            "The `--system-specs` argument is not supported in the Zed CLI, only on Zed binary.",
 397            "To retrieve the system specs on the command line, run the following command:",
 398            &format!("{} --system-specs", path.display()),
 399        ];
 400        anyhow::bail!(msg.join("\n"));
 401    }
 402
 403    #[cfg(all(
 404        any(target_os = "linux", target_os = "macos"),
 405        not(feature = "no-bundled-uninstall")
 406    ))]
 407    if args.uninstall {
 408        static UNINSTALL_SCRIPT: &[u8] = include_bytes!("../../../script/uninstall.sh");
 409
 410        let tmp_dir = tempfile::tempdir()?;
 411        let script_path = tmp_dir.path().join("uninstall.sh");
 412        fs::write(&script_path, UNINSTALL_SCRIPT)?;
 413
 414        use std::os::unix::fs::PermissionsExt as _;
 415        fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?;
 416
 417        let status = std::process::Command::new("sh")
 418            .arg(&script_path)
 419            .env("ZED_CHANNEL", &*release_channel::RELEASE_CHANNEL_NAME)
 420            .status()
 421            .context("Failed to execute uninstall script")?;
 422
 423        std::process::exit(status.code().unwrap_or(1));
 424    }
 425
 426    let (server, server_name) =
 427        IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
 428    let url = format!("zed-cli://{server_name}");
 429
 430    let open_new_workspace = if args.new {
 431        Some(true)
 432    } else if args.add {
 433        Some(false)
 434    } else {
 435        None
 436    };
 437
 438    let env = {
 439        #[cfg(any(target_os = "linux", target_os = "freebsd"))]
 440        {
 441            use collections::HashMap;
 442
 443            // On Linux, the desktop entry uses `cli` to spawn `zed`.
 444            // We need to handle env vars correctly since std::env::vars() may not contain
 445            // project-specific vars (e.g. those set by direnv).
 446            // By setting env to None here, the LSP will use worktree env vars instead,
 447            // which is what we want.
 448            if !std::io::stdout().is_terminal() {
 449                None
 450            } else {
 451                Some(std::env::vars().collect::<HashMap<_, _>>())
 452            }
 453        }
 454
 455        #[cfg(target_os = "windows")]
 456        {
 457            // On Windows, by default, a child process inherits a copy of the environment block of the parent process.
 458            // So we don't need to pass env vars explicitly.
 459            None
 460        }
 461
 462        #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
 463        {
 464            use collections::HashMap;
 465
 466            Some(std::env::vars().collect::<HashMap<_, _>>())
 467        }
 468    };
 469
 470    let exit_status = Arc::new(Mutex::new(None));
 471    let mut paths = vec![];
 472    let mut urls = vec![];
 473    let mut diff_paths = vec![];
 474    let mut stdin_tmp_file: Option<fs::File> = None;
 475    let mut anonymous_fd_tmp_files = vec![];
 476
 477    for path in args.diff.chunks(2) {
 478        diff_paths.push([
 479            parse_path_with_position(&path[0])?,
 480            parse_path_with_position(&path[1])?,
 481        ]);
 482    }
 483
 484    #[cfg(target_os = "windows")]
 485    let wsl = args.wsl.as_ref();
 486    #[cfg(not(target_os = "windows"))]
 487    let wsl = None;
 488
 489    for path in args.paths_with_position.iter() {
 490        if URL_PREFIX.iter().any(|&prefix| path.starts_with(prefix)) {
 491            urls.push(path.to_string());
 492        } else if path == "-" && args.paths_with_position.len() == 1 {
 493            let file = NamedTempFile::new()?;
 494            paths.push(file.path().to_string_lossy().into_owned());
 495            let (file, _) = file.keep()?;
 496            stdin_tmp_file = Some(file);
 497        } else if let Some(file) = anonymous_fd(path) {
 498            let tmp_file = NamedTempFile::new()?;
 499            paths.push(tmp_file.path().to_string_lossy().into_owned());
 500            let (tmp_file, _) = tmp_file.keep()?;
 501            anonymous_fd_tmp_files.push((file, tmp_file));
 502        } else if let Some(wsl) = wsl {
 503            urls.push(format!("file://{}", parse_path_in_wsl(path, wsl)?));
 504        } else {
 505            paths.push(parse_path_with_position(path)?);
 506        }
 507    }
 508
 509    anyhow::ensure!(
 510        args.dev_server_token.is_none(),
 511        "Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development"
 512    );
 513
 514    rayon::ThreadPoolBuilder::new()
 515        .num_threads(4)
 516        .stack_size(10 * 1024 * 1024)
 517        .thread_name(|ix| format!("RayonWorker{}", ix))
 518        .build_global()
 519        .unwrap();
 520
 521    let sender: JoinHandle<anyhow::Result<()>> = thread::Builder::new()
 522        .name("CliReceiver".to_string())
 523        .spawn({
 524            let exit_status = exit_status.clone();
 525            let user_data_dir_for_thread = user_data_dir.clone();
 526            move || {
 527                let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
 528                let (tx, rx) = (handshake.requests, handshake.responses);
 529
 530                #[cfg(target_os = "windows")]
 531                let wsl = args.wsl;
 532                #[cfg(not(target_os = "windows"))]
 533                let wsl = None;
 534
 535                tx.send(CliRequest::Open {
 536                    paths,
 537                    urls,
 538                    diff_paths,
 539                    wsl,
 540                    wait: args.wait,
 541                    open_new_workspace,
 542                    reuse: args.reuse,
 543                    env,
 544                    user_data_dir: user_data_dir_for_thread,
 545                })?;
 546
 547                while let Ok(response) = rx.recv() {
 548                    match response {
 549                        CliResponse::Ping => {}
 550                        CliResponse::Stdout { message } => println!("{message}"),
 551                        CliResponse::Stderr { message } => eprintln!("{message}"),
 552                        CliResponse::Exit { status } => {
 553                            exit_status.lock().replace(status);
 554                            return Ok(());
 555                        }
 556                    }
 557                }
 558
 559                Ok(())
 560            }
 561        })
 562        .unwrap();
 563
 564    let stdin_pipe_handle: Option<JoinHandle<anyhow::Result<()>>> =
 565        stdin_tmp_file.map(|mut tmp_file| {
 566            thread::Builder::new()
 567                .name("CliStdin".to_string())
 568                .spawn(move || {
 569                    let mut stdin = std::io::stdin().lock();
 570                    if !io::IsTerminal::is_terminal(&stdin) {
 571                        io::copy(&mut stdin, &mut tmp_file)?;
 572                    }
 573                    Ok(())
 574                })
 575                .unwrap()
 576        });
 577
 578    let anonymous_fd_pipe_handles: Vec<_> = anonymous_fd_tmp_files
 579        .into_iter()
 580        .map(|(mut file, mut tmp_file)| {
 581            thread::Builder::new()
 582                .name("CliAnonymousFd".to_string())
 583                .spawn(move || io::copy(&mut file, &mut tmp_file))
 584                .unwrap()
 585        })
 586        .collect();
 587
 588    if args.foreground {
 589        app.run_foreground(url, user_data_dir.as_deref())?;
 590    } else {
 591        app.launch(url, user_data_dir.as_deref())?;
 592        sender.join().unwrap()?;
 593        if let Some(handle) = stdin_pipe_handle {
 594            handle.join().unwrap()?;
 595        }
 596        for handle in anonymous_fd_pipe_handles {
 597            handle.join().unwrap()?;
 598        }
 599    }
 600
 601    if let Some(exit_status) = exit_status.lock().take() {
 602        std::process::exit(exit_status);
 603    }
 604    Ok(())
 605}
 606
 607fn anonymous_fd(path: &str) -> Option<fs::File> {
 608    #[cfg(target_os = "linux")]
 609    {
 610        use std::os::fd::{self, FromRawFd};
 611
 612        let fd_str = path.strip_prefix("/proc/self/fd/")?;
 613
 614        let link = fs::read_link(path).ok()?;
 615        if !link.starts_with("memfd:") {
 616            return None;
 617        }
 618
 619        let fd: fd::RawFd = fd_str.parse().ok()?;
 620        let file = unsafe { fs::File::from_raw_fd(fd) };
 621        Some(file)
 622    }
 623    #[cfg(any(target_os = "macos", target_os = "freebsd"))]
 624    {
 625        use std::os::{
 626            fd::{self, FromRawFd},
 627            unix::fs::FileTypeExt,
 628        };
 629
 630        let fd_str = path.strip_prefix("/dev/fd/")?;
 631
 632        let metadata = fs::metadata(path).ok()?;
 633        let file_type = metadata.file_type();
 634        if !file_type.is_fifo() && !file_type.is_socket() {
 635            return None;
 636        }
 637        let fd: fd::RawFd = fd_str.parse().ok()?;
 638        let file = unsafe { fs::File::from_raw_fd(fd) };
 639        Some(file)
 640    }
 641    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
 642    {
 643        _ = path;
 644        // not implemented for bsd, windows. Could be, but isn't yet
 645        None
 646    }
 647}
 648
 649#[cfg(any(target_os = "linux", target_os = "freebsd"))]
 650mod linux {
 651    use std::{
 652        env,
 653        ffi::OsString,
 654        io,
 655        os::unix::net::{SocketAddr, UnixDatagram},
 656        path::{Path, PathBuf},
 657        process::{self, ExitStatus},
 658        thread,
 659        time::Duration,
 660    };
 661
 662    use anyhow::{Context as _, anyhow};
 663    use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
 664    use fork::Fork;
 665
 666    use crate::{Detect, InstalledApp};
 667
 668    struct App(PathBuf);
 669
 670    impl Detect {
 671        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
 672            let path = if let Some(path) = path {
 673                path.to_path_buf().canonicalize()?
 674            } else {
 675                let cli = env::current_exe()?;
 676                let dir = cli.parent().context("no parent path for cli")?;
 677
 678                // libexec is the standard, lib/zed is for Arch (and other non-libexec distros),
 679                // ./zed is for the target directory in development builds.
 680                let possible_locations =
 681                    ["../libexec/zed-editor", "../lib/zed/zed-editor", "./zed"];
 682                possible_locations
 683                    .iter()
 684                    .find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
 685                    .with_context(|| {
 686                        format!("could not find any of: {}", possible_locations.join(", "))
 687                    })?
 688            };
 689
 690            Ok(App(path))
 691        }
 692    }
 693
 694    impl InstalledApp for App {
 695        fn zed_version_string(&self) -> String {
 696            format!(
 697                "Zed {}{}{}{}",
 698                if *release_channel::RELEASE_CHANNEL_NAME == "stable" {
 699                    "".to_string()
 700                } else {
 701                    format!("{} ", *release_channel::RELEASE_CHANNEL_NAME)
 702                },
 703                option_env!("RELEASE_VERSION").unwrap_or_default(),
 704                match option_env!("ZED_COMMIT_SHA") {
 705                    Some(commit_sha) => format!(" {commit_sha} "),
 706                    None => "".to_string(),
 707                },
 708                self.0.display(),
 709            )
 710        }
 711
 712        fn launch(&self, ipc_url: String, user_data_dir: Option<&str>) -> anyhow::Result<()> {
 713            let data_dir = user_data_dir
 714                .map(PathBuf::from)
 715                .unwrap_or_else(|| paths::data_dir().clone());
 716
 717            let sock_path = data_dir.join(format!(
 718                "zed-{}.sock",
 719                *release_channel::RELEASE_CHANNEL_NAME
 720            ));
 721            let sock = UnixDatagram::unbound()?;
 722            if sock.connect(&sock_path).is_err() {
 723                self.boot_background(ipc_url, user_data_dir)?;
 724            } else {
 725                sock.send(ipc_url.as_bytes())?;
 726            }
 727            Ok(())
 728        }
 729
 730        fn run_foreground(
 731            &self,
 732            ipc_url: String,
 733            user_data_dir: Option<&str>,
 734        ) -> io::Result<ExitStatus> {
 735            let mut cmd = std::process::Command::new(self.0.clone());
 736            cmd.arg(ipc_url);
 737            if let Some(dir) = user_data_dir {
 738                cmd.arg("--user-data-dir").arg(dir);
 739            }
 740            cmd.status()
 741        }
 742
 743        fn path(&self) -> PathBuf {
 744            self.0.clone()
 745        }
 746    }
 747
 748    impl App {
 749        fn boot_background(
 750            &self,
 751            ipc_url: String,
 752            user_data_dir: Option<&str>,
 753        ) -> anyhow::Result<()> {
 754            let path = &self.0;
 755
 756            match fork::fork() {
 757                Ok(Fork::Parent(_)) => Ok(()),
 758                Ok(Fork::Child) => {
 759                    unsafe { std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "") };
 760                    if fork::setsid().is_err() {
 761                        eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
 762                        process::exit(1);
 763                    }
 764                    if fork::close_fd().is_err() {
 765                        eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
 766                    }
 767                    let mut args: Vec<OsString> =
 768                        vec![path.as_os_str().to_owned(), OsString::from(ipc_url)];
 769                    if let Some(dir) = user_data_dir {
 770                        args.push(OsString::from("--user-data-dir"));
 771                        args.push(OsString::from(dir));
 772                    }
 773                    let error = exec::execvp(path.clone(), &args);
 774                    // if exec succeeded, we never get here.
 775                    eprintln!("failed to exec {:?}: {}", path, error);
 776                    process::exit(1)
 777                }
 778                Err(_) => Err(anyhow!(io::Error::last_os_error())),
 779            }
 780        }
 781
 782        fn wait_for_socket(
 783            &self,
 784            sock_addr: &SocketAddr,
 785            sock: &mut UnixDatagram,
 786        ) -> Result<(), std::io::Error> {
 787            for _ in 0..100 {
 788                thread::sleep(Duration::from_millis(10));
 789                if sock.connect_addr(sock_addr).is_ok() {
 790                    return Ok(());
 791                }
 792            }
 793            sock.connect_addr(sock_addr)
 794        }
 795    }
 796}
 797
 798#[cfg(target_os = "linux")]
 799mod flatpak {
 800    use std::ffi::OsString;
 801    use std::path::PathBuf;
 802    use std::process::Command;
 803    use std::{env, process};
 804
 805    const EXTRA_LIB_ENV_NAME: &str = "ZED_FLATPAK_LIB_PATH";
 806    const NO_ESCAPE_ENV_NAME: &str = "ZED_FLATPAK_NO_ESCAPE";
 807
 808    /// Adds bundled libraries to LD_LIBRARY_PATH if running under flatpak
 809    pub fn ld_extra_libs() {
 810        let mut paths = if let Ok(paths) = env::var("LD_LIBRARY_PATH") {
 811            env::split_paths(&paths).collect()
 812        } else {
 813            Vec::new()
 814        };
 815
 816        if let Ok(extra_path) = env::var(EXTRA_LIB_ENV_NAME) {
 817            paths.push(extra_path.into());
 818        }
 819
 820        unsafe { env::set_var("LD_LIBRARY_PATH", env::join_paths(paths).unwrap()) };
 821    }
 822
 823    /// Restarts outside of the sandbox if currently running within it
 824    pub fn try_restart_to_host() {
 825        if let Some(flatpak_dir) = get_flatpak_dir() {
 826            let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
 827            args.append(&mut get_xdg_env_args());
 828            args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update zed".into());
 829            args.push(
 830                format!(
 831                    "--env={EXTRA_LIB_ENV_NAME}={}",
 832                    flatpak_dir.join("lib").to_str().unwrap()
 833                )
 834                .into(),
 835            );
 836            args.push(flatpak_dir.join("bin").join("zed").into());
 837
 838            let mut is_app_location_set = false;
 839            for arg in &env::args_os().collect::<Vec<_>>()[1..] {
 840                args.push(arg.clone());
 841                is_app_location_set |= arg == "--zed";
 842            }
 843
 844            if !is_app_location_set {
 845                args.push("--zed".into());
 846                args.push(flatpak_dir.join("libexec").join("zed-editor").into());
 847            }
 848
 849            let error = exec::execvp("/usr/bin/flatpak-spawn", args);
 850            eprintln!("failed restart cli on host: {:?}", error);
 851            process::exit(1);
 852        }
 853    }
 854
 855    pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args {
 856        if env::var(NO_ESCAPE_ENV_NAME).is_ok()
 857            && env::var("FLATPAK_ID").is_ok_and(|id| id.starts_with("dev.zed.Zed"))
 858            && args.zed.is_none()
 859        {
 860            args.zed = Some("/app/libexec/zed-editor".into());
 861            unsafe { env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed") };
 862        }
 863        args
 864    }
 865
 866    fn get_flatpak_dir() -> Option<PathBuf> {
 867        if env::var(NO_ESCAPE_ENV_NAME).is_ok() {
 868            return None;
 869        }
 870
 871        if let Ok(flatpak_id) = env::var("FLATPAK_ID") {
 872            if !flatpak_id.starts_with("dev.zed.Zed") {
 873                return None;
 874            }
 875
 876            let install_dir = Command::new("/usr/bin/flatpak-spawn")
 877                .arg("--host")
 878                .arg("flatpak")
 879                .arg("info")
 880                .arg("--show-location")
 881                .arg(flatpak_id)
 882                .output()
 883                .unwrap();
 884            let install_dir = PathBuf::from(String::from_utf8(install_dir.stdout).unwrap().trim());
 885            Some(install_dir.join("files"))
 886        } else {
 887            None
 888        }
 889    }
 890
 891    fn get_xdg_env_args() -> Vec<OsString> {
 892        let xdg_keys = [
 893            "XDG_DATA_HOME",
 894            "XDG_CONFIG_HOME",
 895            "XDG_CACHE_HOME",
 896            "XDG_STATE_HOME",
 897        ];
 898        env::vars()
 899            .filter(|(key, _)| xdg_keys.contains(&key.as_str()))
 900            .map(|(key, val)| format!("--env=FLATPAK_{}={}", key, val).into())
 901            .collect()
 902    }
 903}
 904
 905#[cfg(target_os = "windows")]
 906mod windows {
 907    use anyhow::Context;
 908    use release_channel::app_identifier;
 909    use windows::{
 910        Win32::{
 911            Foundation::{CloseHandle, ERROR_ALREADY_EXISTS, GENERIC_WRITE, GetLastError},
 912            Storage::FileSystem::{
 913                CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING, WriteFile,
 914            },
 915            System::Threading::CreateMutexW,
 916        },
 917        core::HSTRING,
 918    };
 919
 920    use crate::{Detect, InstalledApp};
 921    use std::io;
 922    use std::path::{Path, PathBuf};
 923    use std::process::ExitStatus;
 924
 925    fn check_single_instance() -> bool {
 926        let mutex = unsafe {
 927            CreateMutexW(
 928                None,
 929                false,
 930                &HSTRING::from(format!("{}-Instance-Mutex", app_identifier())),
 931            )
 932            .expect("Unable to create instance sync event")
 933        };
 934        let last_err = unsafe { GetLastError() };
 935        let _ = unsafe { CloseHandle(mutex) };
 936        last_err != ERROR_ALREADY_EXISTS
 937    }
 938
 939    struct App(PathBuf);
 940
 941    impl InstalledApp for App {
 942        fn zed_version_string(&self) -> String {
 943            format!(
 944                "Zed {}{}{}{}",
 945                if *release_channel::RELEASE_CHANNEL_NAME == "stable" {
 946                    "".to_string()
 947                } else {
 948                    format!("{} ", *release_channel::RELEASE_CHANNEL_NAME)
 949                },
 950                option_env!("RELEASE_VERSION").unwrap_or_default(),
 951                match option_env!("ZED_COMMIT_SHA") {
 952                    Some(commit_sha) => format!(" {commit_sha} "),
 953                    None => "".to_string(),
 954                },
 955                self.0.display(),
 956            )
 957        }
 958
 959        fn launch(&self, ipc_url: String, user_data_dir: Option<&str>) -> anyhow::Result<()> {
 960            if check_single_instance() {
 961                let mut cmd = std::process::Command::new(self.0.clone());
 962                cmd.arg(ipc_url);
 963                if let Some(dir) = user_data_dir {
 964                    cmd.arg("--user-data-dir").arg(dir);
 965                }
 966                cmd.spawn()?;
 967            } else {
 968                unsafe {
 969                    let pipe = CreateFileW(
 970                        &HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", app_identifier())),
 971                        GENERIC_WRITE.0,
 972                        FILE_SHARE_MODE::default(),
 973                        None,
 974                        OPEN_EXISTING,
 975                        FILE_FLAGS_AND_ATTRIBUTES::default(),
 976                        None,
 977                    )?;
 978                    let message = ipc_url.as_bytes();
 979                    let mut bytes_written = 0;
 980                    WriteFile(pipe, Some(message), Some(&mut bytes_written), None)?;
 981                    CloseHandle(pipe)?;
 982                }
 983            }
 984            Ok(())
 985        }
 986
 987        fn run_foreground(
 988            &self,
 989            ipc_url: String,
 990            user_data_dir: Option<&str>,
 991        ) -> io::Result<ExitStatus> {
 992            let mut cmd = std::process::Command::new(self.0.clone());
 993            cmd.arg(ipc_url).arg("--foreground");
 994            if let Some(dir) = user_data_dir {
 995                cmd.arg("--user-data-dir").arg(dir);
 996            }
 997            cmd.spawn()?.wait()
 998        }
 999
1000        fn path(&self) -> PathBuf {
1001            self.0.clone()
1002        }
1003    }
1004
1005    impl Detect {
1006        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
1007            let path = if let Some(path) = path {
1008                path.to_path_buf().canonicalize()?
1009            } else {
1010                let cli = std::env::current_exe()?;
1011                let dir = cli.parent().context("no parent path for cli")?;
1012
1013                // ../Zed.exe is the standard, lib/zed is for MSYS2, ./zed.exe is for the target
1014                // directory in development builds.
1015                let possible_locations = ["../Zed.exe", "../lib/zed/zed-editor.exe", "./zed.exe"];
1016                possible_locations
1017                    .iter()
1018                    .find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
1019                    .context(format!(
1020                        "could not find any of: {}",
1021                        possible_locations.join(", ")
1022                    ))?
1023            };
1024
1025            Ok(App(path))
1026        }
1027    }
1028}
1029
1030#[cfg(target_os = "macos")]
1031mod mac_os {
1032    use anyhow::{Context as _, Result};
1033    use core_foundation::{
1034        array::{CFArray, CFIndex},
1035        base::TCFType as _,
1036        string::kCFStringEncodingUTF8,
1037        url::{CFURL, CFURLCreateWithBytes},
1038    };
1039    use core_services::{LSLaunchURLSpec, LSOpenFromURLSpec, kLSLaunchDefaults};
1040    use serde::Deserialize;
1041    use std::{
1042        ffi::OsStr,
1043        fs, io,
1044        path::{Path, PathBuf},
1045        process::{Command, ExitStatus},
1046        ptr,
1047    };
1048
1049    use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
1050
1051    use crate::{Detect, InstalledApp};
1052
1053    #[derive(Debug, Deserialize)]
1054    struct InfoPlist {
1055        #[serde(rename = "CFBundleShortVersionString")]
1056        bundle_short_version_string: String,
1057    }
1058
1059    enum Bundle {
1060        App {
1061            app_bundle: PathBuf,
1062            plist: InfoPlist,
1063        },
1064        LocalPath {
1065            executable: PathBuf,
1066        },
1067    }
1068
1069    fn locate_bundle() -> Result<PathBuf> {
1070        let cli_path = std::env::current_exe()?.canonicalize()?;
1071        let mut app_path = cli_path.clone();
1072        while app_path.extension() != Some(OsStr::new("app")) {
1073            anyhow::ensure!(
1074                app_path.pop(),
1075                "cannot find app bundle containing {cli_path:?}"
1076            );
1077        }
1078        Ok(app_path)
1079    }
1080
1081    impl Detect {
1082        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
1083            let bundle_path = if let Some(bundle_path) = path {
1084                bundle_path
1085                    .canonicalize()
1086                    .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
1087            } else {
1088                locate_bundle().context("bundle autodiscovery")?
1089            };
1090
1091            match bundle_path.extension().and_then(|ext| ext.to_str()) {
1092                Some("app") => {
1093                    let plist_path = bundle_path.join("Contents/Info.plist");
1094                    let plist =
1095                        plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
1096                            format!("Reading *.app bundle plist file at {plist_path:?}")
1097                        })?;
1098                    Ok(Bundle::App {
1099                        app_bundle: bundle_path,
1100                        plist,
1101                    })
1102                }
1103                _ => Ok(Bundle::LocalPath {
1104                    executable: bundle_path,
1105                }),
1106            }
1107        }
1108    }
1109
1110    impl InstalledApp for Bundle {
1111        fn zed_version_string(&self) -> String {
1112            format!("Zed {}{}", self.version(), self.path().display(),)
1113        }
1114
1115        fn launch(&self, url: String, user_data_dir: Option<&str>) -> anyhow::Result<()> {
1116            match self {
1117                Self::App { app_bundle, .. } => {
1118                    let app_path = app_bundle;
1119
1120                    let status = unsafe {
1121                        let app_url = CFURL::from_path(app_path, true)
1122                            .with_context(|| format!("invalid app path {app_path:?}"))?;
1123                        let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
1124                            ptr::null(),
1125                            url.as_ptr(),
1126                            url.len() as CFIndex,
1127                            kCFStringEncodingUTF8,
1128                            ptr::null(),
1129                        ));
1130                        // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
1131                        let urls_to_open =
1132                            CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
1133                        LSOpenFromURLSpec(
1134                            &LSLaunchURLSpec {
1135                                appURL: app_url.as_concrete_TypeRef(),
1136                                itemURLs: urls_to_open.as_concrete_TypeRef(),
1137                                passThruParams: ptr::null(),
1138                                launchFlags: kLSLaunchDefaults,
1139                                asyncRefCon: ptr::null_mut(),
1140                            },
1141                            ptr::null_mut(),
1142                        )
1143                    };
1144
1145                    anyhow::ensure!(
1146                        status == 0,
1147                        "cannot start app bundle {}",
1148                        self.zed_version_string()
1149                    );
1150                }
1151
1152                Self::LocalPath { executable, .. } => {
1153                    let executable_parent = executable
1154                        .parent()
1155                        .with_context(|| format!("Executable {executable:?} path has no parent"))?;
1156                    let subprocess_stdout_file = fs::File::create(
1157                        executable_parent.join("zed_dev.log"),
1158                    )
1159                    .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
1160                    let subprocess_stdin_file =
1161                        subprocess_stdout_file.try_clone().with_context(|| {
1162                            format!("Cloning descriptor for file {subprocess_stdout_file:?}")
1163                        })?;
1164                    let mut command = std::process::Command::new(executable);
1165                    command.env(FORCE_CLI_MODE_ENV_VAR_NAME, "");
1166                    if let Some(dir) = user_data_dir {
1167                        command.arg("--user-data-dir").arg(dir);
1168                    }
1169                    command
1170                        .stderr(subprocess_stdout_file)
1171                        .stdout(subprocess_stdin_file)
1172                        .arg(url);
1173
1174                    command
1175                        .spawn()
1176                        .with_context(|| format!("Spawning {command:?}"))?;
1177                }
1178            }
1179
1180            Ok(())
1181        }
1182
1183        fn run_foreground(
1184            &self,
1185            ipc_url: String,
1186            user_data_dir: Option<&str>,
1187        ) -> io::Result<ExitStatus> {
1188            let path = match self {
1189                Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
1190                Bundle::LocalPath { executable, .. } => executable.clone(),
1191            };
1192
1193            let mut cmd = std::process::Command::new(path);
1194            cmd.arg(ipc_url);
1195            if let Some(dir) = user_data_dir {
1196                cmd.arg("--user-data-dir").arg(dir);
1197            }
1198            cmd.status()
1199        }
1200
1201        fn path(&self) -> PathBuf {
1202            match self {
1203                Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
1204                Bundle::LocalPath { executable, .. } => executable.clone(),
1205            }
1206        }
1207    }
1208
1209    impl Bundle {
1210        fn version(&self) -> String {
1211            match self {
1212                Self::App { plist, .. } => plist.bundle_short_version_string.clone(),
1213                Self::LocalPath { .. } => "<development>".to_string(),
1214            }
1215        }
1216
1217        fn path(&self) -> &Path {
1218            match self {
1219                Self::App { app_bundle, .. } => app_bundle,
1220                Self::LocalPath { executable, .. } => executable,
1221            }
1222        }
1223    }
1224
1225    pub(super) fn spawn_channel_cli(
1226        channel: release_channel::ReleaseChannel,
1227        leftover_args: Vec<String>,
1228    ) -> Result<()> {
1229        use anyhow::bail;
1230
1231        let app_path_prompt = format!(
1232            "POSIX path of (path to application \"{}\")",
1233            channel.display_name()
1234        );
1235        let app_path_output = Command::new("osascript")
1236            .arg("-e")
1237            .arg(&app_path_prompt)
1238            .output()?;
1239        if !app_path_output.status.success() {
1240            bail!(
1241                "Could not determine app path for {}",
1242                channel.display_name()
1243            );
1244        }
1245        let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned();
1246        let cli_path = format!("{app_path}/Contents/MacOS/cli");
1247        Command::new(cli_path).args(leftover_args).spawn()?;
1248        Ok(())
1249    }
1250}