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