windows: Implement cli and handle open_urls (#25412)

Closes #ISSUE

Release Notes:

- N/A
This commit is contained in:
张小白 2025-02-27 08:27:19 +08:00 committed by GitHub
parent 9822d9673c
commit 672a472a23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 290 additions and 33 deletions

View file

@ -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)]

View file

@ -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(())
}