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