main.rs

  1#![cfg_attr(any(target_os = "linux", target_os = "windows"), allow(dead_code))]
  2
  3use anyhow::{Context, Result};
  4use clap::Parser;
  5use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
  6use parking_lot::Mutex;
  7use std::{
  8    env, fs, io,
  9    path::{Path, PathBuf},
 10    process::ExitStatus,
 11    sync::Arc,
 12    thread::{self, JoinHandle},
 13};
 14use util::paths::PathLikeWithPosition;
 15
 16struct Detect;
 17
 18trait InstalledApp {
 19    fn zed_version_string(&self) -> String;
 20    fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
 21    fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus>;
 22}
 23
 24#[derive(Parser, Debug)]
 25#[command(name = "zed", disable_version_flag = true)]
 26struct Args {
 27    /// Wait for all of the given paths to be opened/closed before exiting.
 28    #[arg(short, long)]
 29    wait: bool,
 30    /// Add files to the currently open workspace
 31    #[arg(short, long, overrides_with = "new")]
 32    add: bool,
 33    /// Create a new workspace
 34    #[arg(short, long, overrides_with = "add")]
 35    new: bool,
 36    /// A sequence of space-separated paths that you want to open.
 37    ///
 38    /// Use `path:line:row` syntax to open a file at a specific location.
 39    /// Non-existing paths and directories will ignore `:line:row` suffix.
 40    #[arg(value_parser = parse_path_with_position)]
 41    paths_with_position: Vec<PathLikeWithPosition<PathBuf>>,
 42    /// Print Zed's version and the app path.
 43    #[arg(short, long)]
 44    version: bool,
 45    /// Run zed in the foreground (useful for debugging)
 46    #[arg(long)]
 47    foreground: bool,
 48    /// Custom path to Zed.app or the zed binary
 49    #[arg(long)]
 50    zed: Option<PathBuf>,
 51    /// Run zed in dev-server mode
 52    #[arg(long)]
 53    dev_server_token: Option<String>,
 54}
 55
 56fn parse_path_with_position(
 57    argument_str: &str,
 58) -> Result<PathLikeWithPosition<PathBuf>, std::convert::Infallible> {
 59    PathLikeWithPosition::parse_str(argument_str, |_, path_str| {
 60        Ok(Path::new(path_str).to_path_buf())
 61    })
 62}
 63
 64fn main() -> Result<()> {
 65    // Exit flatpak sandbox if needed
 66    #[cfg(target_os = "linux")]
 67    {
 68        flatpak::try_restart_to_host();
 69        flatpak::ld_extra_libs();
 70    }
 71
 72    // Intercept version designators
 73    #[cfg(target_os = "macos")]
 74    if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {
 75        // When the first argument is a name of a release channel, we're gonna spawn off a cli of that version, with trailing args passed along.
 76        use std::str::FromStr as _;
 77
 78        if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {
 79            return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());
 80        }
 81    }
 82    let args = Args::parse();
 83
 84    #[cfg(target_os = "linux")]
 85    let args = flatpak::set_bin_if_no_escape(args);
 86
 87    let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
 88
 89    if args.version {
 90        println!("{}", app.zed_version_string());
 91        return Ok(());
 92    }
 93
 94    let curdir = env::current_dir()?;
 95    let mut paths = vec![];
 96    for path in args.paths_with_position {
 97        let canonicalized = path.map_path_like(|path| match fs::canonicalize(&path) {
 98            Ok(path) => Ok(path),
 99            Err(e) => {
100                if let Some(mut parent) = path.parent() {
101                    if parent == Path::new("") {
102                        parent = &curdir;
103                    }
104                    match fs::canonicalize(parent) {
105                        Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
106                        Err(_) => Err(e),
107                    }
108                } else {
109                    Err(e)
110                }
111            }
112        })?;
113        paths.push(canonicalized.to_string(|path| path.display().to_string()))
114    }
115
116    let (server, server_name) =
117        IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
118    let url = format!("zed-cli://{server_name}");
119
120    let open_new_workspace = if args.new {
121        Some(true)
122    } else if args.add {
123        Some(false)
124    } else {
125        None
126    };
127
128    let exit_status = Arc::new(Mutex::new(None));
129
130    let sender: JoinHandle<anyhow::Result<()>> = thread::spawn({
131        let exit_status = exit_status.clone();
132        move || {
133            let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
134            let (tx, rx) = (handshake.requests, handshake.responses);
135            tx.send(CliRequest::Open {
136                paths,
137                wait: args.wait,
138                open_new_workspace,
139                dev_server_token: args.dev_server_token,
140            })?;
141
142            while let Ok(response) = rx.recv() {
143                match response {
144                    CliResponse::Ping => {}
145                    CliResponse::Stdout { message } => println!("{message}"),
146                    CliResponse::Stderr { message } => eprintln!("{message}"),
147                    CliResponse::Exit { status } => {
148                        exit_status.lock().replace(status);
149                        return Ok(());
150                    }
151                }
152            }
153
154            Ok(())
155        }
156    });
157
158    if args.foreground {
159        app.run_foreground(url)?;
160    } else {
161        app.launch(url)?;
162        sender.join().unwrap()?;
163    }
164
165    if let Some(exit_status) = exit_status.lock().take() {
166        std::process::exit(exit_status);
167    }
168    Ok(())
169}
170
171#[cfg(target_os = "linux")]
172mod linux {
173    use std::{
174        env,
175        ffi::OsString,
176        io,
177        os::unix::net::{SocketAddr, UnixDatagram},
178        path::{Path, PathBuf},
179        process::{self, ExitStatus},
180        thread,
181        time::Duration,
182    };
183
184    use anyhow::anyhow;
185    use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
186    use fork::Fork;
187    use once_cell::sync::Lazy;
188
189    use crate::{Detect, InstalledApp};
190
191    static RELEASE_CHANNEL: Lazy<String> =
192        Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
193
194    struct App(PathBuf);
195
196    impl Detect {
197        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
198            let path = if let Some(path) = path {
199                path.to_path_buf().canonicalize()
200            } else {
201                let cli = env::current_exe()?;
202                let dir = cli
203                    .parent()
204                    .and_then(Path::parent)
205                    .ok_or_else(|| anyhow!("no parent path for cli"))?;
206
207                match dir.join("libexec").join("zed-editor").canonicalize() {
208                    Ok(path) => Ok(path),
209                    // In development cli and zed are in the ./target/ directory together
210                    Err(e) => match cli.parent().unwrap().join("zed").canonicalize() {
211                        Ok(path) if path != cli => Ok(path),
212                        _ => Err(e),
213                    },
214                }
215            }?;
216
217            Ok(App(path))
218        }
219    }
220
221    impl InstalledApp for App {
222        fn zed_version_string(&self) -> String {
223            format!(
224                "Zed {}{}{}",
225                if *RELEASE_CHANNEL == "stable" {
226                    "".to_string()
227                } else {
228                    format!(" {} ", *RELEASE_CHANNEL)
229                },
230                option_env!("RELEASE_VERSION").unwrap_or_default(),
231                self.0.display(),
232            )
233        }
234
235        fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
236            let sock_path = paths::support_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL));
237            let sock = UnixDatagram::unbound()?;
238            if sock.connect(&sock_path).is_err() {
239                self.boot_background(ipc_url)?;
240            } else {
241                sock.send(ipc_url.as_bytes())?;
242            }
243            Ok(())
244        }
245
246        fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
247            std::process::Command::new(self.0.clone())
248                .arg(ipc_url)
249                .status()
250        }
251    }
252
253    impl App {
254        fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {
255            let path = &self.0;
256
257            match fork::fork() {
258                Ok(Fork::Parent(_)) => Ok(()),
259                Ok(Fork::Child) => {
260                    std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "");
261                    if let Err(_) = fork::setsid() {
262                        eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
263                        process::exit(1);
264                    }
265                    if let Err(_) = fork::close_fd() {
266                        eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
267                    }
268                    let error =
269                        exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
270                    // if exec succeeded, we never get here.
271                    eprintln!("failed to exec {:?}: {}", path, error);
272                    process::exit(1)
273                }
274                Err(_) => Err(anyhow!(io::Error::last_os_error())),
275            }
276        }
277
278        fn wait_for_socket(
279            &self,
280            sock_addr: &SocketAddr,
281            sock: &mut UnixDatagram,
282        ) -> Result<(), std::io::Error> {
283            for _ in 0..100 {
284                thread::sleep(Duration::from_millis(10));
285                if sock.connect_addr(&sock_addr).is_ok() {
286                    return Ok(());
287                }
288            }
289            sock.connect_addr(&sock_addr)
290        }
291    }
292}
293
294#[cfg(target_os = "linux")]
295mod flatpak {
296    use std::ffi::OsString;
297    use std::path::PathBuf;
298    use std::process::Command;
299    use std::{env, process};
300
301    const EXTRA_LIB_ENV_NAME: &'static str = "ZED_FLATPAK_LIB_PATH";
302    const NO_ESCAPE_ENV_NAME: &'static str = "ZED_FLATPAK_NO_ESCAPE";
303
304    /// Adds bundled libraries to LD_LIBRARY_PATH if running under flatpak
305    pub fn ld_extra_libs() {
306        let mut paths = if let Ok(paths) = env::var("LD_LIBRARY_PATH") {
307            env::split_paths(&paths).collect()
308        } else {
309            Vec::new()
310        };
311
312        if let Ok(extra_path) = env::var(EXTRA_LIB_ENV_NAME) {
313            paths.push(extra_path.into());
314        }
315
316        env::set_var("LD_LIBRARY_PATH", env::join_paths(paths).unwrap());
317    }
318
319    /// Restarts outside of the sandbox if currently running within it
320    pub fn try_restart_to_host() {
321        if let Some(flatpak_dir) = get_flatpak_dir() {
322            let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
323            args.append(&mut get_xdg_env_args());
324            args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update zed".into());
325            args.push(
326                format!(
327                    "--env={EXTRA_LIB_ENV_NAME}={}",
328                    flatpak_dir.join("lib").to_str().unwrap()
329                )
330                .into(),
331            );
332            args.push(flatpak_dir.join("bin").join("zed").into());
333
334            let mut is_app_location_set = false;
335            for arg in &env::args_os().collect::<Vec<_>>()[1..] {
336                args.push(arg.clone());
337                is_app_location_set |= arg == "--zed";
338            }
339
340            if !is_app_location_set {
341                args.push("--zed".into());
342                args.push(flatpak_dir.join("libexec").join("zed-editor").into());
343            }
344
345            let error = exec::execvp("/usr/bin/flatpak-spawn", args);
346            eprintln!("failed restart cli on host: {:?}", error);
347            process::exit(1);
348        }
349    }
350
351    pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args {
352        if env::var(NO_ESCAPE_ENV_NAME).is_ok()
353            && env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed"))
354        {
355            if args.zed.is_none() {
356                args.zed = Some("/app/libexec/zed-editor".into());
357                env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed");
358            }
359        }
360        args
361    }
362
363    fn get_flatpak_dir() -> Option<PathBuf> {
364        if env::var(NO_ESCAPE_ENV_NAME).is_ok() {
365            return None;
366        }
367
368        if let Ok(flatpak_id) = env::var("FLATPAK_ID") {
369            if !flatpak_id.starts_with("dev.zed.Zed") {
370                return None;
371            }
372
373            let install_dir = Command::new("/usr/bin/flatpak-spawn")
374                .arg("--host")
375                .arg("flatpak")
376                .arg("info")
377                .arg("--show-location")
378                .arg(flatpak_id)
379                .output()
380                .unwrap();
381            let install_dir = PathBuf::from(String::from_utf8(install_dir.stdout).unwrap().trim());
382            Some(install_dir.join("files"))
383        } else {
384            None
385        }
386    }
387
388    fn get_xdg_env_args() -> Vec<OsString> {
389        let xdg_keys = [
390            "XDG_DATA_HOME",
391            "XDG_CONFIG_HOME",
392            "XDG_CACHE_HOME",
393            "XDG_STATE_HOME",
394        ];
395        env::vars()
396            .filter(|(key, _)| xdg_keys.contains(&key.as_str()))
397            .map(|(key, val)| format!("--env=FLATPAK_{}={}", key, val).into())
398            .collect()
399    }
400}
401
402// todo("windows")
403#[cfg(target_os = "windows")]
404mod windows {
405    use crate::{Detect, InstalledApp};
406    use std::io;
407    use std::path::Path;
408    use std::process::ExitStatus;
409
410    struct App;
411    impl InstalledApp for App {
412        fn zed_version_string(&self) -> String {
413            unimplemented!()
414        }
415        fn launch(&self, _ipc_url: String) -> anyhow::Result<()> {
416            unimplemented!()
417        }
418        fn run_foreground(&self, _ipc_url: String) -> io::Result<ExitStatus> {
419            unimplemented!()
420        }
421    }
422
423    impl Detect {
424        pub fn detect(_path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
425            Ok(App)
426        }
427    }
428}
429
430#[cfg(target_os = "macos")]
431mod mac_os {
432    use anyhow::{anyhow, Context, Result};
433    use core_foundation::{
434        array::{CFArray, CFIndex},
435        string::kCFStringEncodingUTF8,
436        url::{CFURLCreateWithBytes, CFURL},
437    };
438    use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
439    use serde::Deserialize;
440    use std::{
441        ffi::OsStr,
442        fs, io,
443        path::{Path, PathBuf},
444        process::{Command, ExitStatus},
445        ptr,
446    };
447
448    use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
449
450    use crate::{Detect, InstalledApp};
451
452    #[derive(Debug, Deserialize)]
453    struct InfoPlist {
454        #[serde(rename = "CFBundleShortVersionString")]
455        bundle_short_version_string: String,
456    }
457
458    enum Bundle {
459        App {
460            app_bundle: PathBuf,
461            plist: InfoPlist,
462        },
463        LocalPath {
464            executable: PathBuf,
465            plist: InfoPlist,
466        },
467    }
468
469    fn locate_bundle() -> Result<PathBuf> {
470        let cli_path = std::env::current_exe()?.canonicalize()?;
471        let mut app_path = cli_path.clone();
472        while app_path.extension() != Some(OsStr::new("app")) {
473            if !app_path.pop() {
474                return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
475            }
476        }
477        Ok(app_path)
478    }
479
480    impl Detect {
481        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
482            let bundle_path = if let Some(bundle_path) = path {
483                bundle_path
484                    .canonicalize()
485                    .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
486            } else {
487                locate_bundle().context("bundle autodiscovery")?
488            };
489
490            match bundle_path.extension().and_then(|ext| ext.to_str()) {
491                Some("app") => {
492                    let plist_path = bundle_path.join("Contents/Info.plist");
493                    let plist =
494                        plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
495                            format!("Reading *.app bundle plist file at {plist_path:?}")
496                        })?;
497                    Ok(Bundle::App {
498                        app_bundle: bundle_path,
499                        plist,
500                    })
501                }
502                _ => {
503                    println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
504                    let plist_path = bundle_path
505                        .parent()
506                        .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
507                        .join("WebRTC.framework/Resources/Info.plist");
508                    let plist =
509                        plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
510                            format!("Reading dev bundle plist file at {plist_path:?}")
511                        })?;
512                    Ok(Bundle::LocalPath {
513                        executable: bundle_path,
514                        plist,
515                    })
516                }
517            }
518        }
519    }
520
521    impl InstalledApp for Bundle {
522        fn zed_version_string(&self) -> String {
523            let is_dev = matches!(self, Self::LocalPath { .. });
524            format!(
525                "Zed {}{}{}",
526                self.plist().bundle_short_version_string,
527                if is_dev { " (dev)" } else { "" },
528                self.path().display(),
529            )
530        }
531
532        fn launch(&self, url: String) -> anyhow::Result<()> {
533            match self {
534                Self::App { app_bundle, .. } => {
535                    let app_path = app_bundle;
536
537                    let status = unsafe {
538                        let app_url = CFURL::from_path(app_path, true)
539                            .with_context(|| format!("invalid app path {app_path:?}"))?;
540                        let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
541                            ptr::null(),
542                            url.as_ptr(),
543                            url.len() as CFIndex,
544                            kCFStringEncodingUTF8,
545                            ptr::null(),
546                        ));
547                        // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
548                        let urls_to_open =
549                            CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
550                        LSOpenFromURLSpec(
551                            &LSLaunchURLSpec {
552                                appURL: app_url.as_concrete_TypeRef(),
553                                itemURLs: urls_to_open.as_concrete_TypeRef(),
554                                passThruParams: ptr::null(),
555                                launchFlags: kLSLaunchDefaults,
556                                asyncRefCon: ptr::null_mut(),
557                            },
558                            ptr::null_mut(),
559                        )
560                    };
561
562                    anyhow::ensure!(
563                        status == 0,
564                        "cannot start app bundle {}",
565                        self.zed_version_string()
566                    );
567                }
568
569                Self::LocalPath { executable, .. } => {
570                    let executable_parent = executable
571                        .parent()
572                        .with_context(|| format!("Executable {executable:?} path has no parent"))?;
573                    let subprocess_stdout_file = fs::File::create(
574                        executable_parent.join("zed_dev.log"),
575                    )
576                    .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
577                    let subprocess_stdin_file =
578                        subprocess_stdout_file.try_clone().with_context(|| {
579                            format!("Cloning descriptor for file {subprocess_stdout_file:?}")
580                        })?;
581                    let mut command = std::process::Command::new(executable);
582                    let command = command
583                        .env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
584                        .stderr(subprocess_stdout_file)
585                        .stdout(subprocess_stdin_file)
586                        .arg(url);
587
588                    command
589                        .spawn()
590                        .with_context(|| format!("Spawning {command:?}"))?;
591                }
592            }
593
594            Ok(())
595        }
596
597        fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
598            let path = match self {
599                Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
600                Bundle::LocalPath { executable, .. } => executable.clone(),
601            };
602
603            std::process::Command::new(path).arg(ipc_url).status()
604        }
605    }
606
607    impl Bundle {
608        fn plist(&self) -> &InfoPlist {
609            match self {
610                Self::App { plist, .. } => plist,
611                Self::LocalPath { plist, .. } => plist,
612            }
613        }
614
615        fn path(&self) -> &Path {
616            match self {
617                Self::App { app_bundle, .. } => app_bundle,
618                Self::LocalPath { executable, .. } => executable,
619            }
620        }
621    }
622
623    pub(super) fn spawn_channel_cli(
624        channel: release_channel::ReleaseChannel,
625        leftover_args: Vec<String>,
626    ) -> Result<()> {
627        use anyhow::bail;
628
629        let app_id_prompt = format!("id of app \"{}\"", channel.display_name());
630        let app_id_output = Command::new("osascript")
631            .arg("-e")
632            .arg(&app_id_prompt)
633            .output()?;
634        if !app_id_output.status.success() {
635            bail!("Could not determine app id for {}", channel.display_name());
636        }
637        let app_name = String::from_utf8(app_id_output.stdout)?.trim().to_owned();
638        let app_path_prompt = format!("kMDItemCFBundleIdentifier == '{app_name}'");
639        let app_path_output = Command::new("mdfind").arg(app_path_prompt).output()?;
640        if !app_path_output.status.success() {
641            bail!(
642                "Could not determine app path for {}",
643                channel.display_name()
644            );
645        }
646        let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned();
647        let cli_path = format!("{app_path}/Contents/MacOS/cli");
648        Command::new(cli_path).args(leftover_args).spawn()?;
649        Ok(())
650    }
651}