1use std::{sync::Arc, thread::JoinHandle};
2
3use anyhow::Context;
4use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};
5use parking_lot::Mutex;
6use release_channel::app_identifier;
7use util::ResultExt;
8use windows::{
9 Win32::{
10 Foundation::{CloseHandle, ERROR_ALREADY_EXISTS, GENERIC_WRITE, GetLastError, HANDLE},
11 Storage::FileSystem::{
12 CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING,
13 PIPE_ACCESS_INBOUND, ReadFile, WriteFile,
14 },
15 System::{
16 Pipes::{
17 ConnectNamedPipe, CreateNamedPipeW, DisconnectNamedPipe, PIPE_READMODE_MESSAGE,
18 PIPE_TYPE_MESSAGE, PIPE_WAIT,
19 },
20 Threading::CreateMutexW,
21 },
22 },
23 core::HSTRING,
24};
25
26use crate::{Args, OpenListener, RawOpenRequest};
27
28#[inline]
29fn is_first_instance() -> bool {
30 unsafe {
31 CreateMutexW(
32 None,
33 false,
34 &HSTRING::from(format!("{}-Instance-Mutex", app_identifier())),
35 )
36 .expect("Unable to create instance mutex.")
37 };
38 unsafe { GetLastError() != ERROR_ALREADY_EXISTS }
39}
40
41pub fn handle_single_instance(opener: OpenListener, args: &Args) -> bool {
42 let is_first_instance = is_first_instance();
43 if is_first_instance {
44 // We are the first instance, listen for messages sent from other instances
45 std::thread::Builder::new()
46 .name("EnsureSingleton".to_owned())
47 .spawn(move || {
48 with_pipe(|url| {
49 opener.open(RawOpenRequest {
50 urls: vec![url],
51 ..Default::default()
52 })
53 })
54 })
55 .unwrap();
56 } else if !args.foreground {
57 // We are not the first instance, send args to the first instance
58 send_args_to_instance(args).log_err();
59 }
60
61 is_first_instance
62}
63
64fn with_pipe(f: impl Fn(String)) {
65 let pipe = unsafe {
66 CreateNamedPipeW(
67 &HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", app_identifier())),
68 PIPE_ACCESS_INBOUND,
69 PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
70 1,
71 128,
72 128,
73 0,
74 None,
75 )
76 };
77 if pipe.is_invalid() {
78 log::error!("Failed to create named pipe: {:?}", unsafe {
79 GetLastError()
80 });
81 return;
82 }
83
84 loop {
85 if let Some(message) = retrieve_message_from_pipe(pipe)
86 .context("Failed to read from named pipe")
87 .log_err()
88 {
89 f(message);
90 }
91 }
92}
93
94fn retrieve_message_from_pipe(pipe: HANDLE) -> anyhow::Result<String> {
95 unsafe { ConnectNamedPipe(pipe, None)? };
96 let message = retrieve_message_from_pipe_inner(pipe);
97 unsafe { DisconnectNamedPipe(pipe).log_err() };
98 message
99}
100
101fn retrieve_message_from_pipe_inner(pipe: HANDLE) -> anyhow::Result<String> {
102 let mut buffer = [0u8; 128];
103 unsafe {
104 ReadFile(pipe, Some(&mut buffer), None, None)?;
105 }
106 let message = std::ffi::CStr::from_bytes_until_nul(&buffer)?;
107 Ok(message.to_string_lossy().into_owned())
108}
109
110// This part of code is mostly from crates/cli/src/main.rs
111fn send_args_to_instance(args: &Args) -> anyhow::Result<()> {
112 if let Some(dock_menu_action_idx) = args.dock_action {
113 let url = format!("zed-dock-action://{}", dock_menu_action_idx);
114 return write_message_to_instance_pipe(url.as_bytes());
115 }
116
117 let (server, server_name) =
118 IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
119 let url = format!("zed-cli://{server_name}");
120
121 let request = {
122 let mut paths = vec![];
123 let mut urls = vec![];
124 let mut diff_paths = vec![];
125 for path in args.paths_or_urls.iter() {
126 match std::fs::canonicalize(&path) {
127 Ok(path) => paths.push(path.to_string_lossy().into_owned()),
128 Err(error) => {
129 if path.starts_with("zed://")
130 || path.starts_with("http://")
131 || path.starts_with("https://")
132 || path.starts_with("file://")
133 || path.starts_with("ssh://")
134 {
135 urls.push(path.clone());
136 } else {
137 log::error!("error parsing path argument: {}", error);
138 }
139 }
140 }
141 }
142
143 for path in args.diff.chunks(2) {
144 let old = std::fs::canonicalize(&path[0]).log_err();
145 let new = std::fs::canonicalize(&path[1]).log_err();
146 if let Some((old, new)) = old.zip(new) {
147 diff_paths.push([
148 old.to_string_lossy().into_owned(),
149 new.to_string_lossy().into_owned(),
150 ]);
151 }
152 }
153
154 CliRequest::Open {
155 paths,
156 urls,
157 diff_paths,
158 wait: false,
159 wsl: args.wsl.clone(),
160 open_new_workspace: None,
161 env: None,
162 user_data_dir: args.user_data_dir.clone(),
163 }
164 };
165
166 let exit_status = Arc::new(Mutex::new(None));
167 let sender: JoinHandle<anyhow::Result<()>> = std::thread::Builder::new()
168 .name("CliReceiver".to_owned())
169 .spawn({
170 let exit_status = exit_status.clone();
171 move || {
172 let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
173 let (tx, rx) = (handshake.requests, handshake.responses);
174
175 tx.send(request)?;
176
177 while let Ok(response) = rx.recv() {
178 match response {
179 CliResponse::Ping => {}
180 CliResponse::Stdout { message } => log::info!("{message}"),
181 CliResponse::Stderr { message } => log::error!("{message}"),
182 CliResponse::Exit { status } => {
183 exit_status.lock().replace(status);
184 return Ok(());
185 }
186 }
187 }
188 Ok(())
189 }
190 })
191 .unwrap();
192
193 write_message_to_instance_pipe(url.as_bytes())?;
194 sender.join().unwrap()?;
195 if let Some(exit_status) = exit_status.lock().take() {
196 std::process::exit(exit_status);
197 }
198 Ok(())
199}
200
201fn write_message_to_instance_pipe(message: &[u8]) -> anyhow::Result<()> {
202 unsafe {
203 let pipe = CreateFileW(
204 &HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", app_identifier())),
205 GENERIC_WRITE.0,
206 FILE_SHARE_MODE::default(),
207 None,
208 OPEN_EXISTING,
209 FILE_FLAGS_AND_ATTRIBUTES::default(),
210 None,
211 )?;
212 WriteFile(pipe, Some(message), None, None)?;
213 CloseHandle(pipe)?;
214 }
215 Ok(())
216}