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