windows: Implement single instance (#15371)

张小白 created

This PR implements a single instance mechanism using the `CreateEventW`
function to create a mutex. If the identifier name begins with `Local`,
the single instance applies only to processes under the same user. If
the identifier begins with `Global`, it applies to all users.

Additionally, I was thinking that perhaps we should integrate the single
instance functionality into `gpui`. I believe applications developed
using `gpui` would benefit from this feature. Furthermore, incorporating
the single instance implementation into `gpui` would facilitate the
`set_dock_menu` functionality. As I mentioned in #12068, the
implementation of `set_dock_menu` on Windows depends on the single
instance feature. When a user clicks the "dock menu", Windows will open
a new application instance. To achieve behavior similar to macOS, we
need to prevent the new instance from launching and instead pass the
parameters to the existing instance.

Any advice and suggestions are welcome.




https://github.com/user-attachments/assets/c46f7e92-4411-4fa9-830e-383798a9dd93



Release Notes:

- N/A

Change summary

Cargo.lock                            |  1 
crates/zed/Cargo.toml                 |  7 +++-
crates/zed/src/main.rs                | 11 +++++++
crates/zed/src/zed.rs                 |  4 ++
crates/zed/src/zed/single_instance.rs | 39 +++++++++++++++++++++++++++++
5 files changed, 58 insertions(+), 4 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -14212,6 +14212,7 @@ dependencies = [
  "uuid",
  "vim",
  "welcome",
+ "windows 0.58.0",
  "winresource",
  "workspace",
  "zed_actions",

crates/zed/Cargo.toml 🔗

@@ -67,7 +67,7 @@ log.workspace = true
 markdown_preview.workspace = true
 menu.workspace = true
 mimalloc = { version = "0.1", optional = true }
-nix = {workspace = true, features = ["pthread", "signal"] }
+nix = { workspace = true, features = ["pthread", "signal"] }
 node_runtime.workspace = true
 notifications.workspace = true
 outline.workspace = true
@@ -99,7 +99,7 @@ tab_switcher.workspace = true
 supermaven.workspace = true
 task.workspace = true
 tasks_ui.workspace = true
-time.workspace =  true
+time.workspace = true
 telemetry_events.workspace = true
 terminal_view.workspace = true
 theme.workspace = true
@@ -114,6 +114,9 @@ welcome.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 
+[target.'cfg(target_os = "windows")'.dependencies]
+windows.workspace = true
+
 [target.'cfg(target_os = "windows")'.build-dependencies]
 winresource = "0.1"
 

crates/zed/src/main.rs 🔗

@@ -318,6 +318,15 @@ fn init_ui(
 }
 
 fn main() {
+    #[cfg(target_os = "windows")]
+    {
+        use zed::single_instance::*;
+        if !check_single_instance() {
+            println!("zed is already running");
+            return;
+        }
+    }
+
     let start_time = std::time::Instant::now();
     menu::init();
     zed_actions::init();
@@ -360,7 +369,7 @@ fn main() {
             }
         }
     }
-    #[cfg(not(target_os = "linux"))]
+    #[cfg(not(any(target_os = "linux", target_os = "windows")))]
     {
         use zed::only_instance::*;
         if ensure_only_instance() != IsOnlyInstance::Yes {

crates/zed/src/zed.rs 🔗

@@ -2,9 +2,11 @@ mod app_menus;
 pub mod inline_completion_registry;
 #[cfg(target_os = "linux")]
 pub(crate) mod linux_prompts;
-#[cfg(not(target_os = "linux"))]
+#[cfg(not(any(target_os = "linux", target_os = "windows")))]
 pub(crate) mod only_instance;
 mod open_listener;
+#[cfg(target_os = "windows")]
+pub(crate) mod single_instance;
 
 pub use app_menus::*;
 use assistant::PromptBuilder;

crates/zed/src/zed/single_instance.rs 🔗

@@ -0,0 +1,39 @@
+use release_channel::ReleaseChannel;
+use windows::{
+    core::HSTRING,
+    Win32::{
+        Foundation::{GetLastError, ERROR_ALREADY_EXISTS},
+        System::Threading::CreateEventW,
+    },
+};
+
+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",
+    }
+}
+
+pub fn check_single_instance() -> bool {
+    if *db::ZED_STATELESS || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
+        return true;
+    }
+
+    check_single_instance_event()
+}
+
+fn check_single_instance_event() -> bool {
+    unsafe {
+        CreateEventW(
+            None,
+            false,
+            false,
+            &HSTRING::from(retrieve_app_instance_event_identifier()),
+        )
+        .expect("Unable to create instance sync event")
+    };
+    let last_err = unsafe { GetLastError() };
+    last_err != ERROR_ALREADY_EXISTS
+}