main.rs

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