windows: Implement `cli` and handle `open_urls` (#25412)

张小白 created

Closes #ISSUE

Release Notes:

- N/A

Change summary

Cargo.lock                                  |   1 
Cargo.toml                                  |   7 
crates/cli/Cargo.toml                       |   5 
crates/cli/src/main.rs                      |  98 +++++++++++-
crates/release_channel/src/lib.rs           |   9 +
crates/zed/src/main.rs                      |  27 +++
crates/zed/src/zed/windows_only_instance.rs | 176 ++++++++++++++++++++--
7 files changed, 290 insertions(+), 33 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2653,6 +2653,7 @@ dependencies = [
  "serde",
  "tempfile",
  "util",
+ "windows 0.58.0",
 ]
 
 [[package]]

Cargo.toml 🔗

@@ -370,7 +370,7 @@ zeta = { path = "crates/zeta" }
 #
 
 aho-corasick = "1.1"
-alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", rev = "03c2907b44b4189aac5fdeaea331f5aab5c7072e"}
+alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", rev = "03c2907b44b4189aac5fdeaea331f5aab5c7072e" }
 any_vec = "0.14"
 anyhow = "1.0.86"
 arrayvec = { version = "0.7.4", features = ["serde"] }
@@ -544,7 +544,7 @@ tree-sitter-cpp = "0.23"
 tree-sitter-css = "0.23"
 tree-sitter-elixir = "0.3"
 tree-sitter-embedded-template = "0.23.0"
-tree-sitter-gitcommit = {git  = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9"}
+tree-sitter-gitcommit = { git = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9" }
 tree-sitter-go = "0.23"
 tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c", package = "tree-sitter-gomod" }
 tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" }
@@ -619,6 +619,7 @@ features = [
     "Win32_Storage_FileSystem",
     "Win32_System_Com",
     "Win32_System_Com_StructuredStorage",
+    "Win32_System_Console",
     "Win32_System_DataExchange",
     "Win32_System_LibraryLoader",
     "Win32_System_Memory",
@@ -639,7 +640,7 @@ features = [
 # TODO livekit https://github.com/RustAudio/cpal/pull/891
 [patch.crates-io]
 cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" }
-real-async-tls = { git = "https://github.com/zed-industries/async-tls", rev = "1e759a4b5e370f87dc15e40756ac4f8815b61d9d", package = "async-tls"}
+real-async-tls = { git = "https://github.com/zed-industries/async-tls", rev = "1e759a4b5e370f87dc15e40756ac4f8815b61d9d", package = "async-tls" }
 
 [profile.dev]
 split-debuginfo = "unpacked"

crates/cli/Cargo.toml 🔗

@@ -33,10 +33,13 @@ util.workspace = true
 tempfile.workspace = true
 
 [target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
-exec.workspace =  true
+exec.workspace = true
 fork.workspace = true
 
 [target.'cfg(target_os = "macos")'.dependencies]
 core-foundation.workspace = true
 core-services = "0.2"
 plist = "1.3"
+
+[target.'cfg(target_os = "windows")'.dependencies]
+windows.workspace = true

crates/cli/src/main.rs 🔗

@@ -521,30 +521,108 @@ mod flatpak {
     }
 }
 
-// todo("windows")
 #[cfg(target_os = "windows")]
 mod windows {
+    use anyhow::Context;
+    use release_channel::APP_IDENTIFIER;
+    use windows::{
+        core::HSTRING,
+        Win32::{
+            Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, GENERIC_WRITE},
+            Storage::FileSystem::{
+                CreateFileW, WriteFile, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING,
+            },
+            System::Threading::CreateMutexW,
+        },
+    };
+
     use crate::{Detect, InstalledApp};
     use std::io;
-    use std::path::Path;
+    use std::path::{Path, PathBuf};
     use std::process::ExitStatus;
 
-    struct App;
+    fn check_single_instance() -> bool {
+        let mutex = unsafe {
+            CreateMutexW(
+                None,
+                false,
+                &HSTRING::from(format!("{}-Instance-Mutex", *APP_IDENTIFIER)),
+            )
+            .expect("Unable to create instance sync event")
+        };
+        let last_err = unsafe { GetLastError() };
+        let _ = unsafe { CloseHandle(mutex) };
+        last_err != ERROR_ALREADY_EXISTS
+    }
+
+    struct App(PathBuf);
+
     impl InstalledApp for App {
         fn zed_version_string(&self) -> String {
-            unimplemented!()
+            format!(
+                "Zed {}{}{} – {}",
+                if *release_channel::RELEASE_CHANNEL_NAME == "stable" {
+                    "".to_string()
+                } else {
+                    format!("{} ", *release_channel::RELEASE_CHANNEL_NAME)
+                },
+                option_env!("RELEASE_VERSION").unwrap_or_default(),
+                match option_env!("ZED_COMMIT_SHA") {
+                    Some(commit_sha) => format!(" {commit_sha} "),
+                    None => "".to_string(),
+                },
+                self.0.display(),
+            )
         }
-        fn launch(&self, _ipc_url: String) -> anyhow::Result<()> {
-            unimplemented!()
+
+        fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
+            if check_single_instance() {
+                std::process::Command::new(self.0.clone())
+                    .arg(ipc_url)
+                    .spawn()?;
+            } else {
+                unsafe {
+                    let pipe = CreateFileW(
+                        &HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", *APP_IDENTIFIER)),
+                        GENERIC_WRITE.0,
+                        FILE_SHARE_MODE::default(),
+                        None,
+                        OPEN_EXISTING,
+                        FILE_FLAGS_AND_ATTRIBUTES::default(),
+                        None,
+                    )?;
+                    let message = ipc_url.as_bytes();
+                    let mut bytes_written = 0;
+                    WriteFile(pipe, Some(message), Some(&mut bytes_written), None)?;
+                    CloseHandle(pipe)?;
+                }
+            }
+            Ok(())
         }
-        fn run_foreground(&self, _ipc_url: String) -> io::Result<ExitStatus> {
-            unimplemented!()
+
+        fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
+            std::process::Command::new(self.0.clone())
+                .arg(ipc_url)
+                .arg("--foreground")
+                .spawn()?
+                .wait()
         }
     }
 
     impl Detect {
-        pub fn detect(_path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
-            Ok(App)
+        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
+            let path = if let Some(path) = path {
+                path.to_path_buf().canonicalize()?
+            } else {
+                std::env::current_exe()?
+                    .parent()
+                    .context("no parent path for cli")?
+                    .parent()
+                    .context("no parent path for cli folder")?
+                    .join("Zed.exe")
+            };
+
+            Ok(App(path))
         }
     }
 }

crates/release_channel/src/lib.rs 🔗

@@ -23,6 +23,15 @@ pub static RELEASE_CHANNEL: LazyLock<ReleaseChannel> =
         _ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME),
     });
 
+/// The app identifier for the current release channel, Windows only.
+#[cfg(target_os = "windows")]
+pub static APP_IDENTIFIER: LazyLock<&str> = LazyLock::new(|| match *RELEASE_CHANNEL {
+    ReleaseChannel::Dev => "Zed-Editor-Dev",
+    ReleaseChannel::Nightly => "Zed-Editor-Nightly",
+    ReleaseChannel::Preview => "Zed-Editor-Preview",
+    ReleaseChannel::Stable => "Zed-Editor-Stable",
+});
+
 /// The Git commit SHA that Zed was built at.
 #[derive(Clone)]
 pub struct AppCommitSha(pub String);

crates/zed/src/main.rs 🔗

@@ -173,6 +173,22 @@ fn fail_to_open_window(e: anyhow::Error, _cx: &mut App) {
 }
 
 fn main() {
+    let args = Args::parse();
+
+    #[cfg(target_os = "windows")]
+    let run_foreground = args.foreground;
+
+    #[cfg(all(not(debug_assertions), target_os = "windows"))]
+    if run_foreground {
+        unsafe {
+            use windows::Win32::System::Console::{AttachConsole, ATTACH_PARENT_PROCESS};
+
+            if run_foreground {
+                let _ = AttachConsole(ATTACH_PARENT_PROCESS);
+            }
+        }
+    }
+
     menu::init();
     zed_actions::init();
 
@@ -217,7 +233,10 @@ fn main() {
 
             #[cfg(target_os = "windows")]
             {
-                !crate::zed::windows_only_instance::check_single_instance()
+                !crate::zed::windows_only_instance::check_single_instance(
+                    open_listener.clone(),
+                    run_foreground,
+                )
             }
 
             #[cfg(target_os = "macos")]
@@ -574,7 +593,6 @@ fn main() {
         })
         .detach_and_log_err(cx);
 
-        let args = Args::parse();
         let urls: Vec<_> = args
             .paths_or_urls
             .iter()
@@ -1012,6 +1030,11 @@ struct Args {
     /// Instructs zed to run as a dev server on this machine. (not implemented)
     #[arg(long)]
     dev_server_token: Option<String>,
+
+    /// Run zed in the foreground, only used on Windows, to match the behavior of the behavior on macOS.
+    #[arg(long)]
+    #[cfg(target_os = "windows")]
+    foreground: bool,
 }
 
 #[derive(Clone, Debug)]

crates/zed/src/zed/windows_only_instance.rs 🔗

@@ -1,31 +1,173 @@
-use release_channel::ReleaseChannel;
+use std::{sync::Arc, thread::JoinHandle};
+
+use anyhow::Context;
+use clap::Parser;
+use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
+use parking_lot::Mutex;
+use release_channel::APP_IDENTIFIER;
+use util::ResultExt;
 use windows::{
     core::HSTRING,
     Win32::{
-        Foundation::{GetLastError, ERROR_ALREADY_EXISTS},
-        System::Threading::CreateEventW,
+        Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, GENERIC_WRITE, HANDLE},
+        Storage::FileSystem::{
+            CreateFileW, ReadFile, WriteFile, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE,
+            OPEN_EXISTING, PIPE_ACCESS_INBOUND,
+        },
+        System::{
+            Pipes::{
+                ConnectNamedPipe, CreateNamedPipeW, DisconnectNamedPipe, PIPE_READMODE_MESSAGE,
+                PIPE_TYPE_MESSAGE, PIPE_WAIT,
+            },
+            Threading::CreateMutexW,
+        },
     },
 };
 
-fn retrieve_app_instance_event_identifier() -> &'static str {
-    match *release_channel::RELEASE_CHANNEL {
-        ReleaseChannel::Dev => "Local\\Zed-Editor-Dev-Instance-Event",
-        ReleaseChannel::Nightly => "Local\\Zed-Editor-Nightly-Instance-Event",
-        ReleaseChannel::Preview => "Local\\Zed-Editor-Preview-Instance-Event",
-        ReleaseChannel::Stable => "Local\\Zed-Editor-Stable-Instance-Event",
-    }
-}
+use crate::{Args, OpenListener};
 
-pub fn check_single_instance() -> bool {
+pub fn check_single_instance(opener: OpenListener, run_foreground: bool) -> bool {
     unsafe {
-        CreateEventW(
+        CreateMutexW(
             None,
             false,
-            false,
-            &HSTRING::from(retrieve_app_instance_event_identifier()),
+            &HSTRING::from(format!("{}-Instance-Mutex", *APP_IDENTIFIER)),
         )
         .expect("Unable to create instance sync event")
     };
-    let last_err = unsafe { GetLastError() };
-    last_err != ERROR_ALREADY_EXISTS
+    let first_instance = unsafe { GetLastError() } != ERROR_ALREADY_EXISTS;
+
+    if first_instance {
+        // We are the first instance, listen for messages sent from other instances
+        std::thread::spawn(move || with_pipe(|url| opener.open_urls(vec![url])));
+    } else if !run_foreground {
+        // We are not the first instance, send args to the first instance
+        send_args_to_instance().log_err();
+    }
+
+    first_instance
+}
+
+fn with_pipe(f: impl Fn(String)) {
+    let pipe = unsafe {
+        CreateNamedPipeW(
+            &HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", *APP_IDENTIFIER)),
+            PIPE_ACCESS_INBOUND,
+            PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
+            1,
+            128,
+            128,
+            0,
+            None,
+        )
+    };
+    if pipe.is_invalid() {
+        log::error!("Failed to create named pipe: {:?}", unsafe {
+            GetLastError()
+        });
+        return;
+    }
+
+    loop {
+        if let Some(message) = retrieve_message_from_pipe(pipe)
+            .context("Failed to read from named pipe")
+            .log_err()
+        {
+            f(message);
+        }
+    }
+}
+
+fn retrieve_message_from_pipe(pipe: HANDLE) -> anyhow::Result<String> {
+    unsafe { ConnectNamedPipe(pipe, None)? };
+    let message = retrieve_message_from_pipe_inner(pipe);
+    unsafe { DisconnectNamedPipe(pipe).log_err() };
+    message
+}
+
+fn retrieve_message_from_pipe_inner(pipe: HANDLE) -> anyhow::Result<String> {
+    let mut buffer = [0u8; 128];
+    unsafe {
+        ReadFile(pipe, Some(&mut buffer), None, None)?;
+    }
+    let message = std::ffi::CStr::from_bytes_until_nul(&buffer)?;
+    Ok(message.to_string_lossy().to_string())
+}
+
+// This part of code is mostly from crates/cli/src/main.rs
+fn send_args_to_instance() -> anyhow::Result<()> {
+    let Args { paths_or_urls, .. } = Args::parse();
+    let (server, server_name) =
+        IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
+    let url = format!("zed-cli://{server_name}");
+
+    let mut paths = vec![];
+    let mut urls = vec![];
+    for path in paths_or_urls.into_iter() {
+        match std::fs::canonicalize(&path) {
+            Ok(path) => paths.push(path.to_string_lossy().to_string()),
+            Err(error) => {
+                if path.starts_with("zed://")
+                    || path.starts_with("http://")
+                    || path.starts_with("https://")
+                    || path.starts_with("file://")
+                    || path.starts_with("ssh://")
+                {
+                    urls.push(path);
+                } else {
+                    log::error!("error parsing path argument: {}", error);
+                }
+            }
+        }
+    }
+    let exit_status = Arc::new(Mutex::new(None));
+    let sender: JoinHandle<anyhow::Result<()>> = std::thread::spawn({
+        let exit_status = exit_status.clone();
+        move || {
+            let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
+            let (tx, rx) = (handshake.requests, handshake.responses);
+
+            tx.send(CliRequest::Open {
+                paths,
+                urls,
+                wait: false,
+                open_new_workspace: None,
+                env: None,
+            })?;
+
+            while let Ok(response) = rx.recv() {
+                match response {
+                    CliResponse::Ping => {}
+                    CliResponse::Stdout { message } => log::info!("{message}"),
+                    CliResponse::Stderr { message } => log::error!("{message}"),
+                    CliResponse::Exit { status } => {
+                        exit_status.lock().replace(status);
+                        return Ok(());
+                    }
+                }
+            }
+            Ok(())
+        }
+    });
+
+    unsafe {
+        let pipe = CreateFileW(
+            &HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", *APP_IDENTIFIER)),
+            GENERIC_WRITE.0,
+            FILE_SHARE_MODE::default(),
+            None,
+            OPEN_EXISTING,
+            FILE_FLAGS_AND_ATTRIBUTES::default(),
+            None,
+        )?;
+        let message = url.as_bytes();
+        let mut bytes_written = 0;
+        WriteFile(pipe, Some(message), Some(&mut bytes_written), None)?;
+        CloseHandle(pipe)?;
+    }
+    sender.join().unwrap()?;
+    if let Some(exit_status) = exit_status.lock().take() {
+        std::process::exit(exit_status);
+    }
+    Ok(())
 }