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 std::{
  7    env, fs, io,
  8    path::{Path, PathBuf},
  9    process::ExitStatus,
 10    thread::{self, JoinHandle},
 11};
 12use util::paths::PathLikeWithPosition;
 13
 14struct Detect;
 15
 16trait InstalledApp {
 17    fn zed_version_string(&self) -> String;
 18    fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
 19    fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus>;
 20}
 21
 22#[derive(Parser, Debug)]
 23#[command(name = "zed", disable_version_flag = true)]
 24struct Args {
 25    /// Wait for all of the given paths to be opened/closed before exiting.
 26    #[arg(short, long)]
 27    wait: bool,
 28    /// Add files to the currently open workspace
 29    #[arg(short, long, overrides_with = "new")]
 30    add: bool,
 31    /// Create a new workspace
 32    #[arg(short, long, overrides_with = "add")]
 33    new: bool,
 34    /// A sequence of space-separated paths that you want to open.
 35    ///
 36    /// Use `path:line:row` syntax to open a file at a specific location.
 37    /// Non-existing paths and directories will ignore `:line:row` suffix.
 38    #[arg(value_parser = parse_path_with_position)]
 39    paths_with_position: Vec<PathLikeWithPosition<PathBuf>>,
 40    /// Print Zed's version and the app path.
 41    #[arg(short, long)]
 42    version: bool,
 43    /// Run zed in the foreground (useful for debugging)
 44    #[arg(long)]
 45    foreground: bool,
 46    /// Custom path to Zed.app or the zed binary
 47    #[arg(long)]
 48    zed: Option<PathBuf>,
 49    /// Run zed in dev-server mode
 50    #[arg(long)]
 51    dev_server_token: Option<String>,
 52}
 53
 54fn parse_path_with_position(
 55    argument_str: &str,
 56) -> Result<PathLikeWithPosition<PathBuf>, std::convert::Infallible> {
 57    PathLikeWithPosition::parse_str(argument_str, |path_str| {
 58        Ok(Path::new(path_str).to_path_buf())
 59    })
 60}
 61
 62fn main() -> Result<()> {
 63    // Intercept version designators
 64    #[cfg(target_os = "macos")]
 65    if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {
 66        // 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.
 67        use std::str::FromStr as _;
 68
 69        if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {
 70            return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());
 71        }
 72    }
 73    let args = Args::parse();
 74
 75    let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
 76
 77    if args.version {
 78        println!("{}", app.zed_version_string());
 79        return Ok(());
 80    }
 81
 82    let curdir = env::current_dir()?;
 83    let mut paths = vec![];
 84    for path in args.paths_with_position {
 85        let canonicalized = path.map_path_like(|path| match fs::canonicalize(&path) {
 86            Ok(path) => Ok(path),
 87            Err(e) => {
 88                if let Some(mut parent) = path.parent() {
 89                    if parent == Path::new("") {
 90                        parent = &curdir;
 91                    }
 92                    match fs::canonicalize(parent) {
 93                        Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
 94                        Err(_) => Err(e),
 95                    }
 96                } else {
 97                    Err(e)
 98                }
 99            }
100        })?;
101        paths.push(canonicalized.to_string(|path| path.display().to_string()))
102    }
103
104    let (server, server_name) =
105        IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
106    let url = format!("zed-cli://{server_name}");
107
108    let open_new_workspace = if args.new {
109        Some(true)
110    } else if args.add {
111        Some(false)
112    } else {
113        None
114    };
115
116    let sender: JoinHandle<anyhow::Result<()>> = thread::spawn(move || {
117        let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
118        let (tx, rx) = (handshake.requests, handshake.responses);
119        tx.send(CliRequest::Open {
120            paths,
121            wait: args.wait,
122            open_new_workspace,
123            dev_server_token: args.dev_server_token,
124        })?;
125
126        while let Ok(response) = rx.recv() {
127            match response {
128                CliResponse::Ping => {}
129                CliResponse::Stdout { message } => println!("{message}"),
130                CliResponse::Stderr { message } => eprintln!("{message}"),
131                CliResponse::Exit { status } => std::process::exit(status),
132            }
133        }
134
135        Ok(())
136    });
137
138    if args.foreground {
139        app.run_foreground(url)?;
140    } else {
141        app.launch(url)?;
142        sender.join().unwrap()?;
143    }
144
145    Ok(())
146}
147
148#[cfg(target_os = "linux")]
149mod linux {
150    use std::{
151        env,
152        ffi::OsString,
153        io,
154        os::{
155            linux::net::SocketAddrExt,
156            unix::net::{SocketAddr, UnixDatagram},
157        },
158        path::{Path, PathBuf},
159        process::{self, ExitStatus},
160        thread,
161        time::Duration,
162    };
163
164    use anyhow::anyhow;
165    use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
166    use fork::Fork;
167    use once_cell::sync::Lazy;
168
169    use crate::{Detect, InstalledApp};
170
171    static RELEASE_CHANNEL: Lazy<String> =
172        Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
173
174    struct App(PathBuf);
175
176    impl Detect {
177        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
178            let path = if let Some(path) = path {
179                path.to_path_buf().canonicalize()
180            } else {
181                let cli = env::current_exe()?;
182                let dir = cli
183                    .parent()
184                    .ok_or_else(|| anyhow!("no parent path for cli"))?;
185
186                match dir.join("zed").canonicalize() {
187                    Ok(path) => Ok(path),
188                    // development builds have Zed capitalized
189                    Err(e) => match dir.join("Zed").canonicalize() {
190                        Ok(path) => Ok(path),
191                        Err(_) => Err(e),
192                    },
193                }
194            }?;
195
196            Ok(App(path))
197        }
198    }
199
200    impl InstalledApp for App {
201        fn zed_version_string(&self) -> String {
202            format!(
203                "Zed {}{}{}",
204                if *RELEASE_CHANNEL == "stable" {
205                    "".to_string()
206                } else {
207                    format!(" {} ", *RELEASE_CHANNEL)
208                },
209                option_env!("RELEASE_VERSION").unwrap_or_default(),
210                self.0.display(),
211            )
212        }
213
214        fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
215            let uid: u32 = unsafe { libc::getuid() };
216            let sock_addr =
217                SocketAddr::from_abstract_name(format!("zed-{}-{}", *RELEASE_CHANNEL, uid))?;
218
219            let sock = UnixDatagram::unbound()?;
220            if sock.connect_addr(&sock_addr).is_err() {
221                self.boot_background(ipc_url)?;
222            } else {
223                sock.send(ipc_url.as_bytes())?;
224            }
225            Ok(())
226        }
227
228        fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
229            std::process::Command::new(self.0.clone())
230                .arg(ipc_url)
231                .status()
232        }
233    }
234
235    impl App {
236        fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {
237            let path = &self.0;
238
239            match fork::fork() {
240                Ok(Fork::Parent(_)) => Ok(()),
241                Ok(Fork::Child) => {
242                    std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "");
243                    if let Err(_) = fork::setsid() {
244                        eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
245                        process::exit(1);
246                    }
247                    if std::env::var("ZED_KEEP_FD").is_err() {
248                        if let Err(_) = fork::close_fd() {
249                            eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
250                        }
251                    }
252                    let error =
253                        exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
254                    // if exec succeeded, we never get here.
255                    eprintln!("failed to exec {:?}: {}", path, error);
256                    process::exit(1)
257                }
258                Err(_) => Err(anyhow!(io::Error::last_os_error())),
259            }
260        }
261
262        fn wait_for_socket(
263            &self,
264            sock_addr: &SocketAddr,
265            sock: &mut UnixDatagram,
266        ) -> Result<(), std::io::Error> {
267            for _ in 0..100 {
268                thread::sleep(Duration::from_millis(10));
269                if sock.connect_addr(&sock_addr).is_ok() {
270                    return Ok(());
271                }
272            }
273            sock.connect_addr(&sock_addr)
274        }
275    }
276}
277
278// todo("windows")
279#[cfg(target_os = "windows")]
280mod windows {
281    use crate::{Detect, InstalledApp};
282    use std::io;
283    use std::path::Path;
284    use std::process::ExitStatus;
285
286    struct App;
287    impl InstalledApp for App {
288        fn zed_version_string(&self) -> String {
289            unimplemented!()
290        }
291        fn launch(&self, _ipc_url: String) -> anyhow::Result<()> {
292            unimplemented!()
293        }
294        fn run_foreground(&self, _ipc_url: String) -> io::Result<ExitStatus> {
295            unimplemented!()
296        }
297    }
298
299    impl Detect {
300        pub fn detect(_path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
301            Ok(App)
302        }
303    }
304}
305
306#[cfg(target_os = "macos")]
307mod mac_os {
308    use anyhow::{anyhow, Context, Result};
309    use core_foundation::{
310        array::{CFArray, CFIndex},
311        string::kCFStringEncodingUTF8,
312        url::{CFURLCreateWithBytes, CFURL},
313    };
314    use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
315    use serde::Deserialize;
316    use std::{
317        ffi::OsStr,
318        fs, io,
319        path::{Path, PathBuf},
320        process::{Command, ExitStatus},
321        ptr,
322    };
323
324    use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
325
326    use crate::{Detect, InstalledApp};
327
328    #[derive(Debug, Deserialize)]
329    struct InfoPlist {
330        #[serde(rename = "CFBundleShortVersionString")]
331        bundle_short_version_string: String,
332    }
333
334    enum Bundle {
335        App {
336            app_bundle: PathBuf,
337            plist: InfoPlist,
338        },
339        LocalPath {
340            executable: PathBuf,
341            plist: InfoPlist,
342        },
343    }
344
345    fn locate_bundle() -> Result<PathBuf> {
346        let cli_path = std::env::current_exe()?.canonicalize()?;
347        let mut app_path = cli_path.clone();
348        while app_path.extension() != Some(OsStr::new("app")) {
349            if !app_path.pop() {
350                return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
351            }
352        }
353        Ok(app_path)
354    }
355
356    impl Detect {
357        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
358            let bundle_path = if let Some(bundle_path) = path {
359                bundle_path
360                    .canonicalize()
361                    .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
362            } else {
363                locate_bundle().context("bundle autodiscovery")?
364            };
365
366            match bundle_path.extension().and_then(|ext| ext.to_str()) {
367                Some("app") => {
368                    let plist_path = bundle_path.join("Contents/Info.plist");
369                    let plist =
370                        plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
371                            format!("Reading *.app bundle plist file at {plist_path:?}")
372                        })?;
373                    Ok(Bundle::App {
374                        app_bundle: bundle_path,
375                        plist,
376                    })
377                }
378                _ => {
379                    println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
380                    let plist_path = bundle_path
381                        .parent()
382                        .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
383                        .join("WebRTC.framework/Resources/Info.plist");
384                    let plist =
385                        plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
386                            format!("Reading dev bundle plist file at {plist_path:?}")
387                        })?;
388                    Ok(Bundle::LocalPath {
389                        executable: bundle_path,
390                        plist,
391                    })
392                }
393            }
394        }
395    }
396
397    impl InstalledApp for Bundle {
398        fn zed_version_string(&self) -> String {
399            let is_dev = matches!(self, Self::LocalPath { .. });
400            format!(
401                "Zed {}{}{}",
402                self.plist().bundle_short_version_string,
403                if is_dev { " (dev)" } else { "" },
404                self.path().display(),
405            )
406        }
407
408        fn launch(&self, url: String) -> anyhow::Result<()> {
409            match self {
410                Self::App { app_bundle, .. } => {
411                    let app_path = app_bundle;
412
413                    let status = unsafe {
414                        let app_url = CFURL::from_path(app_path, true)
415                            .with_context(|| format!("invalid app path {app_path:?}"))?;
416                        let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
417                            ptr::null(),
418                            url.as_ptr(),
419                            url.len() as CFIndex,
420                            kCFStringEncodingUTF8,
421                            ptr::null(),
422                        ));
423                        // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
424                        let urls_to_open =
425                            CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
426                        LSOpenFromURLSpec(
427                            &LSLaunchURLSpec {
428                                appURL: app_url.as_concrete_TypeRef(),
429                                itemURLs: urls_to_open.as_concrete_TypeRef(),
430                                passThruParams: ptr::null(),
431                                launchFlags: kLSLaunchDefaults,
432                                asyncRefCon: ptr::null_mut(),
433                            },
434                            ptr::null_mut(),
435                        )
436                    };
437
438                    anyhow::ensure!(
439                        status == 0,
440                        "cannot start app bundle {}",
441                        self.zed_version_string()
442                    );
443                }
444
445                Self::LocalPath { executable, .. } => {
446                    let executable_parent = executable
447                        .parent()
448                        .with_context(|| format!("Executable {executable:?} path has no parent"))?;
449                    let subprocess_stdout_file = fs::File::create(
450                        executable_parent.join("zed_dev.log"),
451                    )
452                    .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
453                    let subprocess_stdin_file =
454                        subprocess_stdout_file.try_clone().with_context(|| {
455                            format!("Cloning descriptor for file {subprocess_stdout_file:?}")
456                        })?;
457                    let mut command = std::process::Command::new(executable);
458                    let command = command
459                        .env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
460                        .stderr(subprocess_stdout_file)
461                        .stdout(subprocess_stdin_file)
462                        .arg(url);
463
464                    command
465                        .spawn()
466                        .with_context(|| format!("Spawning {command:?}"))?;
467                }
468            }
469
470            Ok(())
471        }
472
473        fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
474            let path = match self {
475                Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
476                Bundle::LocalPath { executable, .. } => executable.clone(),
477            };
478
479            std::process::Command::new(path).arg(ipc_url).status()
480        }
481    }
482
483    impl Bundle {
484        fn plist(&self) -> &InfoPlist {
485            match self {
486                Self::App { plist, .. } => plist,
487                Self::LocalPath { plist, .. } => plist,
488            }
489        }
490
491        fn path(&self) -> &Path {
492            match self {
493                Self::App { app_bundle, .. } => app_bundle,
494                Self::LocalPath { executable, .. } => executable,
495            }
496        }
497    }
498
499    pub(super) fn spawn_channel_cli(
500        channel: release_channel::ReleaseChannel,
501        leftover_args: Vec<String>,
502    ) -> Result<()> {
503        use anyhow::bail;
504
505        let app_id_prompt = format!("id of app \"{}\"", channel.display_name());
506        let app_id_output = Command::new("osascript")
507            .arg("-e")
508            .arg(&app_id_prompt)
509            .output()?;
510        if !app_id_output.status.success() {
511            bail!("Could not determine app id for {}", channel.display_name());
512        }
513        let app_name = String::from_utf8(app_id_output.stdout)?.trim().to_owned();
514        let app_path_prompt = format!("kMDItemCFBundleIdentifier == '{app_name}'");
515        let app_path_output = Command::new("mdfind").arg(app_path_prompt).output()?;
516        if !app_path_output.status.success() {
517            bail!(
518                "Could not determine app path for {}",
519                channel.display_name()
520            );
521        }
522        let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned();
523        let cli_path = format!("{app_path}/Contents/MacOS/cli");
524        Command::new(cli_path).args(leftover_args).spawn()?;
525        Ok(())
526    }
527}