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

1
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -521,30 +521,108 @@ mod flatpak {
} }
} }
// todo("windows")
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
mod 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 crate::{Detect, InstalledApp};
use std::io; use std::io;
use std::path::Path; use std::path::{Path, PathBuf};
use std::process::ExitStatus; 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 { impl InstalledApp for App {
fn zed_version_string(&self) -> String { 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 { impl Detect {
pub fn detect(_path: Option<&Path>) -> anyhow::Result<impl InstalledApp> { pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
Ok(App) 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))
} }
} }
} }

View file

@ -23,6 +23,15 @@ pub static RELEASE_CHANNEL: LazyLock<ReleaseChannel> =
_ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME), _ => 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. /// The Git commit SHA that Zed was built at.
#[derive(Clone)] #[derive(Clone)]
pub struct AppCommitSha(pub String); pub struct AppCommitSha(pub String);

View file

@ -173,6 +173,22 @@ fn fail_to_open_window(e: anyhow::Error, _cx: &mut App) {
} }
fn main() { 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(); menu::init();
zed_actions::init(); zed_actions::init();
@ -217,7 +233,10 @@ fn main() {
#[cfg(target_os = "windows")] #[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")] #[cfg(target_os = "macos")]
@ -574,7 +593,6 @@ fn main() {
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
let args = Args::parse();
let urls: Vec<_> = args let urls: Vec<_> = args
.paths_or_urls .paths_or_urls
.iter() .iter()
@ -1012,6 +1030,11 @@ struct Args {
/// Instructs zed to run as a dev server on this machine. (not implemented) /// Instructs zed to run as a dev server on this machine. (not implemented)
#[arg(long)] #[arg(long)]
dev_server_token: Option<String>, 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)] #[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::{ use windows::{
core::HSTRING, core::HSTRING,
Win32::{ Win32::{
Foundation::{GetLastError, ERROR_ALREADY_EXISTS}, Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, GENERIC_WRITE, HANDLE},
System::Threading::CreateEventW, 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 { use crate::{Args, OpenListener};
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 { pub fn check_single_instance(opener: OpenListener, run_foreground: bool) -> bool {
unsafe { unsafe {
CreateEventW( CreateMutexW(
None, None,
false, false,
false, &HSTRING::from(format!("{}-Instance-Mutex", *APP_IDENTIFIER)),
&HSTRING::from(retrieve_app_instance_event_identifier()),
) )
.expect("Unable to create instance sync event") .expect("Unable to create instance sync event")
}; };
let last_err = unsafe { GetLastError() }; let first_instance = unsafe { GetLastError() } != ERROR_ALREADY_EXISTS;
last_err != 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(())
} }