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) -> 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)?;
 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) -> anyhow::Result<()> {
 713            let sock_path = paths::data_dir().join(format!(
 714                "zed-{}.sock",
 715                *release_channel::RELEASE_CHANNEL_NAME
 716            ));
 717            let sock = UnixDatagram::unbound()?;
 718            if sock.connect(&sock_path).is_err() {
 719                self.boot_background(ipc_url)?;
 720            } else {
 721                sock.send(ipc_url.as_bytes())?;
 722            }
 723            Ok(())
 724        }
 725
 726        fn run_foreground(
 727            &self,
 728            ipc_url: String,
 729            user_data_dir: Option<&str>,
 730        ) -> io::Result<ExitStatus> {
 731            let mut cmd = std::process::Command::new(self.0.clone());
 732            cmd.arg(ipc_url);
 733            if let Some(dir) = user_data_dir {
 734                cmd.arg("--user-data-dir").arg(dir);
 735            }
 736            cmd.status()
 737        }
 738
 739        fn path(&self) -> PathBuf {
 740            self.0.clone()
 741        }
 742    }
 743
 744    impl App {
 745        fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {
 746            let path = &self.0;
 747
 748            match fork::fork() {
 749                Ok(Fork::Parent(_)) => Ok(()),
 750                Ok(Fork::Child) => {
 751                    unsafe { std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "") };
 752                    if fork::setsid().is_err() {
 753                        eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
 754                        process::exit(1);
 755                    }
 756                    if fork::close_fd().is_err() {
 757                        eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
 758                    }
 759                    let error =
 760                        exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
 761                    // if exec succeeded, we never get here.
 762                    eprintln!("failed to exec {:?}: {}", path, error);
 763                    process::exit(1)
 764                }
 765                Err(_) => Err(anyhow!(io::Error::last_os_error())),
 766            }
 767        }
 768
 769        fn wait_for_socket(
 770            &self,
 771            sock_addr: &SocketAddr,
 772            sock: &mut UnixDatagram,
 773        ) -> Result<(), std::io::Error> {
 774            for _ in 0..100 {
 775                thread::sleep(Duration::from_millis(10));
 776                if sock.connect_addr(sock_addr).is_ok() {
 777                    return Ok(());
 778                }
 779            }
 780            sock.connect_addr(sock_addr)
 781        }
 782    }
 783}
 784
 785#[cfg(target_os = "linux")]
 786mod flatpak {
 787    use std::ffi::OsString;
 788    use std::path::PathBuf;
 789    use std::process::Command;
 790    use std::{env, process};
 791
 792    const EXTRA_LIB_ENV_NAME: &str = "ZED_FLATPAK_LIB_PATH";
 793    const NO_ESCAPE_ENV_NAME: &str = "ZED_FLATPAK_NO_ESCAPE";
 794
 795    /// Adds bundled libraries to LD_LIBRARY_PATH if running under flatpak
 796    pub fn ld_extra_libs() {
 797        let mut paths = if let Ok(paths) = env::var("LD_LIBRARY_PATH") {
 798            env::split_paths(&paths).collect()
 799        } else {
 800            Vec::new()
 801        };
 802
 803        if let Ok(extra_path) = env::var(EXTRA_LIB_ENV_NAME) {
 804            paths.push(extra_path.into());
 805        }
 806
 807        unsafe { env::set_var("LD_LIBRARY_PATH", env::join_paths(paths).unwrap()) };
 808    }
 809
 810    /// Restarts outside of the sandbox if currently running within it
 811    pub fn try_restart_to_host() {
 812        if let Some(flatpak_dir) = get_flatpak_dir() {
 813            let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
 814            args.append(&mut get_xdg_env_args());
 815            args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update zed".into());
 816            args.push(
 817                format!(
 818                    "--env={EXTRA_LIB_ENV_NAME}={}",
 819                    flatpak_dir.join("lib").to_str().unwrap()
 820                )
 821                .into(),
 822            );
 823            args.push(flatpak_dir.join("bin").join("zed").into());
 824
 825            let mut is_app_location_set = false;
 826            for arg in &env::args_os().collect::<Vec<_>>()[1..] {
 827                args.push(arg.clone());
 828                is_app_location_set |= arg == "--zed";
 829            }
 830
 831            if !is_app_location_set {
 832                args.push("--zed".into());
 833                args.push(flatpak_dir.join("libexec").join("zed-editor").into());
 834            }
 835
 836            let error = exec::execvp("/usr/bin/flatpak-spawn", args);
 837            eprintln!("failed restart cli on host: {:?}", error);
 838            process::exit(1);
 839        }
 840    }
 841
 842    pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args {
 843        if env::var(NO_ESCAPE_ENV_NAME).is_ok()
 844            && env::var("FLATPAK_ID").is_ok_and(|id| id.starts_with("dev.zed.Zed"))
 845            && args.zed.is_none()
 846        {
 847            args.zed = Some("/app/libexec/zed-editor".into());
 848            unsafe { env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed") };
 849        }
 850        args
 851    }
 852
 853    fn get_flatpak_dir() -> Option<PathBuf> {
 854        if env::var(NO_ESCAPE_ENV_NAME).is_ok() {
 855            return None;
 856        }
 857
 858        if let Ok(flatpak_id) = env::var("FLATPAK_ID") {
 859            if !flatpak_id.starts_with("dev.zed.Zed") {
 860                return None;
 861            }
 862
 863            let install_dir = Command::new("/usr/bin/flatpak-spawn")
 864                .arg("--host")
 865                .arg("flatpak")
 866                .arg("info")
 867                .arg("--show-location")
 868                .arg(flatpak_id)
 869                .output()
 870                .unwrap();
 871            let install_dir = PathBuf::from(String::from_utf8(install_dir.stdout).unwrap().trim());
 872            Some(install_dir.join("files"))
 873        } else {
 874            None
 875        }
 876    }
 877
 878    fn get_xdg_env_args() -> Vec<OsString> {
 879        let xdg_keys = [
 880            "XDG_DATA_HOME",
 881            "XDG_CONFIG_HOME",
 882            "XDG_CACHE_HOME",
 883            "XDG_STATE_HOME",
 884        ];
 885        env::vars()
 886            .filter(|(key, _)| xdg_keys.contains(&key.as_str()))
 887            .map(|(key, val)| format!("--env=FLATPAK_{}={}", key, val).into())
 888            .collect()
 889    }
 890}
 891
 892#[cfg(target_os = "windows")]
 893mod windows {
 894    use anyhow::Context;
 895    use release_channel::app_identifier;
 896    use windows::{
 897        Win32::{
 898            Foundation::{CloseHandle, ERROR_ALREADY_EXISTS, GENERIC_WRITE, GetLastError},
 899            Storage::FileSystem::{
 900                CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING, WriteFile,
 901            },
 902            System::Threading::CreateMutexW,
 903        },
 904        core::HSTRING,
 905    };
 906
 907    use crate::{Detect, InstalledApp};
 908    use std::io;
 909    use std::path::{Path, PathBuf};
 910    use std::process::ExitStatus;
 911
 912    fn check_single_instance() -> bool {
 913        let mutex = unsafe {
 914            CreateMutexW(
 915                None,
 916                false,
 917                &HSTRING::from(format!("{}-Instance-Mutex", app_identifier())),
 918            )
 919            .expect("Unable to create instance sync event")
 920        };
 921        let last_err = unsafe { GetLastError() };
 922        let _ = unsafe { CloseHandle(mutex) };
 923        last_err != ERROR_ALREADY_EXISTS
 924    }
 925
 926    struct App(PathBuf);
 927
 928    impl InstalledApp for App {
 929        fn zed_version_string(&self) -> String {
 930            format!(
 931                "Zed {}{}{}{}",
 932                if *release_channel::RELEASE_CHANNEL_NAME == "stable" {
 933                    "".to_string()
 934                } else {
 935                    format!("{} ", *release_channel::RELEASE_CHANNEL_NAME)
 936                },
 937                option_env!("RELEASE_VERSION").unwrap_or_default(),
 938                match option_env!("ZED_COMMIT_SHA") {
 939                    Some(commit_sha) => format!(" {commit_sha} "),
 940                    None => "".to_string(),
 941                },
 942                self.0.display(),
 943            )
 944        }
 945
 946        fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
 947            if check_single_instance() {
 948                std::process::Command::new(self.0.clone())
 949                    .arg(ipc_url)
 950                    .spawn()?;
 951            } else {
 952                unsafe {
 953                    let pipe = CreateFileW(
 954                        &HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", app_identifier())),
 955                        GENERIC_WRITE.0,
 956                        FILE_SHARE_MODE::default(),
 957                        None,
 958                        OPEN_EXISTING,
 959                        FILE_FLAGS_AND_ATTRIBUTES::default(),
 960                        None,
 961                    )?;
 962                    let message = ipc_url.as_bytes();
 963                    let mut bytes_written = 0;
 964                    WriteFile(pipe, Some(message), Some(&mut bytes_written), None)?;
 965                    CloseHandle(pipe)?;
 966                }
 967            }
 968            Ok(())
 969        }
 970
 971        fn run_foreground(
 972            &self,
 973            ipc_url: String,
 974            user_data_dir: Option<&str>,
 975        ) -> io::Result<ExitStatus> {
 976            let mut cmd = std::process::Command::new(self.0.clone());
 977            cmd.arg(ipc_url).arg("--foreground");
 978            if let Some(dir) = user_data_dir {
 979                cmd.arg("--user-data-dir").arg(dir);
 980            }
 981            cmd.spawn()?.wait()
 982        }
 983
 984        fn path(&self) -> PathBuf {
 985            self.0.clone()
 986        }
 987    }
 988
 989    impl Detect {
 990        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
 991            let path = if let Some(path) = path {
 992                path.to_path_buf().canonicalize()?
 993            } else {
 994                let cli = std::env::current_exe()?;
 995                let dir = cli.parent().context("no parent path for cli")?;
 996
 997                // ../Zed.exe is the standard, lib/zed is for MSYS2, ./zed.exe is for the target
 998                // directory in development builds.
 999                let possible_locations = ["../Zed.exe", "../lib/zed/zed-editor.exe", "./zed.exe"];
1000                possible_locations
1001                    .iter()
1002                    .find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
1003                    .context(format!(
1004                        "could not find any of: {}",
1005                        possible_locations.join(", ")
1006                    ))?
1007            };
1008
1009            Ok(App(path))
1010        }
1011    }
1012}
1013
1014#[cfg(target_os = "macos")]
1015mod mac_os {
1016    use anyhow::{Context as _, Result};
1017    use core_foundation::{
1018        array::{CFArray, CFIndex},
1019        base::TCFType as _,
1020        string::kCFStringEncodingUTF8,
1021        url::{CFURL, CFURLCreateWithBytes},
1022    };
1023    use core_services::{LSLaunchURLSpec, LSOpenFromURLSpec, kLSLaunchDefaults};
1024    use serde::Deserialize;
1025    use std::{
1026        ffi::OsStr,
1027        fs, io,
1028        path::{Path, PathBuf},
1029        process::{Command, ExitStatus},
1030        ptr,
1031    };
1032
1033    use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
1034
1035    use crate::{Detect, InstalledApp};
1036
1037    #[derive(Debug, Deserialize)]
1038    struct InfoPlist {
1039        #[serde(rename = "CFBundleShortVersionString")]
1040        bundle_short_version_string: String,
1041    }
1042
1043    enum Bundle {
1044        App {
1045            app_bundle: PathBuf,
1046            plist: InfoPlist,
1047        },
1048        LocalPath {
1049            executable: PathBuf,
1050        },
1051    }
1052
1053    fn locate_bundle() -> Result<PathBuf> {
1054        let cli_path = std::env::current_exe()?.canonicalize()?;
1055        let mut app_path = cli_path.clone();
1056        while app_path.extension() != Some(OsStr::new("app")) {
1057            anyhow::ensure!(
1058                app_path.pop(),
1059                "cannot find app bundle containing {cli_path:?}"
1060            );
1061        }
1062        Ok(app_path)
1063    }
1064
1065    impl Detect {
1066        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
1067            let bundle_path = if let Some(bundle_path) = path {
1068                bundle_path
1069                    .canonicalize()
1070                    .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
1071            } else {
1072                locate_bundle().context("bundle autodiscovery")?
1073            };
1074
1075            match bundle_path.extension().and_then(|ext| ext.to_str()) {
1076                Some("app") => {
1077                    let plist_path = bundle_path.join("Contents/Info.plist");
1078                    let plist =
1079                        plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
1080                            format!("Reading *.app bundle plist file at {plist_path:?}")
1081                        })?;
1082                    Ok(Bundle::App {
1083                        app_bundle: bundle_path,
1084                        plist,
1085                    })
1086                }
1087                _ => Ok(Bundle::LocalPath {
1088                    executable: bundle_path,
1089                }),
1090            }
1091        }
1092    }
1093
1094    impl InstalledApp for Bundle {
1095        fn zed_version_string(&self) -> String {
1096            format!("Zed {}{}", self.version(), self.path().display(),)
1097        }
1098
1099        fn launch(&self, url: String) -> anyhow::Result<()> {
1100            match self {
1101                Self::App { app_bundle, .. } => {
1102                    let app_path = app_bundle;
1103
1104                    let status = unsafe {
1105                        let app_url = CFURL::from_path(app_path, true)
1106                            .with_context(|| format!("invalid app path {app_path:?}"))?;
1107                        let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
1108                            ptr::null(),
1109                            url.as_ptr(),
1110                            url.len() as CFIndex,
1111                            kCFStringEncodingUTF8,
1112                            ptr::null(),
1113                        ));
1114                        // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
1115                        let urls_to_open =
1116                            CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
1117                        LSOpenFromURLSpec(
1118                            &LSLaunchURLSpec {
1119                                appURL: app_url.as_concrete_TypeRef(),
1120                                itemURLs: urls_to_open.as_concrete_TypeRef(),
1121                                passThruParams: ptr::null(),
1122                                launchFlags: kLSLaunchDefaults,
1123                                asyncRefCon: ptr::null_mut(),
1124                            },
1125                            ptr::null_mut(),
1126                        )
1127                    };
1128
1129                    anyhow::ensure!(
1130                        status == 0,
1131                        "cannot start app bundle {}",
1132                        self.zed_version_string()
1133                    );
1134                }
1135
1136                Self::LocalPath { executable, .. } => {
1137                    let executable_parent = executable
1138                        .parent()
1139                        .with_context(|| format!("Executable {executable:?} path has no parent"))?;
1140                    let subprocess_stdout_file = fs::File::create(
1141                        executable_parent.join("zed_dev.log"),
1142                    )
1143                    .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
1144                    let subprocess_stdin_file =
1145                        subprocess_stdout_file.try_clone().with_context(|| {
1146                            format!("Cloning descriptor for file {subprocess_stdout_file:?}")
1147                        })?;
1148                    let mut command = std::process::Command::new(executable);
1149                    let command = command
1150                        .env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
1151                        .stderr(subprocess_stdout_file)
1152                        .stdout(subprocess_stdin_file)
1153                        .arg(url);
1154
1155                    command
1156                        .spawn()
1157                        .with_context(|| format!("Spawning {command:?}"))?;
1158                }
1159            }
1160
1161            Ok(())
1162        }
1163
1164        fn run_foreground(
1165            &self,
1166            ipc_url: String,
1167            user_data_dir: Option<&str>,
1168        ) -> io::Result<ExitStatus> {
1169            let path = match self {
1170                Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
1171                Bundle::LocalPath { executable, .. } => executable.clone(),
1172            };
1173
1174            let mut cmd = std::process::Command::new(path);
1175            cmd.arg(ipc_url);
1176            if let Some(dir) = user_data_dir {
1177                cmd.arg("--user-data-dir").arg(dir);
1178            }
1179            cmd.status()
1180        }
1181
1182        fn path(&self) -> PathBuf {
1183            match self {
1184                Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
1185                Bundle::LocalPath { executable, .. } => executable.clone(),
1186            }
1187        }
1188    }
1189
1190    impl Bundle {
1191        fn version(&self) -> String {
1192            match self {
1193                Self::App { plist, .. } => plist.bundle_short_version_string.clone(),
1194                Self::LocalPath { .. } => "<development>".to_string(),
1195            }
1196        }
1197
1198        fn path(&self) -> &Path {
1199            match self {
1200                Self::App { app_bundle, .. } => app_bundle,
1201                Self::LocalPath { executable, .. } => executable,
1202            }
1203        }
1204    }
1205
1206    pub(super) fn spawn_channel_cli(
1207        channel: release_channel::ReleaseChannel,
1208        leftover_args: Vec<String>,
1209    ) -> Result<()> {
1210        use anyhow::bail;
1211
1212        let app_path_prompt = format!(
1213            "POSIX path of (path to application \"{}\")",
1214            channel.display_name()
1215        );
1216        let app_path_output = Command::new("osascript")
1217            .arg("-e")
1218            .arg(&app_path_prompt)
1219            .output()?;
1220        if !app_path_output.status.success() {
1221            bail!(
1222                "Could not determine app path for {}",
1223                channel.display_name()
1224            );
1225        }
1226        let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned();
1227        let cli_path = format!("{app_path}/Contents/MacOS/cli");
1228        Command::new(cli_path).args(leftover_args).spawn()?;
1229        Ok(())
1230    }
1231}