Prevent duplicate instances by coordinating via a socket

This commit is contained in:
Julia 2023-07-06 17:18:45 -04:00
parent abf3b4a54e
commit 66bf56fc4f
4 changed files with 91 additions and 2 deletions

View File

@ -201,6 +201,7 @@ impl Bundle {
self.zed_version_string() self.zed_version_string()
); );
} }
Self::LocalPath { executable, .. } => { Self::LocalPath { executable, .. } => {
let executable_parent = executable let executable_parent = executable
.parent() .parent()

View File

@ -57,8 +57,9 @@ use staff_mode::StaffMode;
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace}; use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace};
use zed::{ use zed::{
assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace, assets::Assets,
languages, menus, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
only_instance::{ensure_only_instance, IsOnlyInstance},
}; };
fn main() { fn main() {
@ -66,6 +67,10 @@ fn main() {
init_paths(); init_paths();
init_logger(); init_logger();
if ensure_only_instance() != IsOnlyInstance::Yes {
return;
}
log::info!("========== starting zed =========="); log::info!("========== starting zed ==========");
let mut app = gpui::App::new(Assets).unwrap(); let mut app = gpui::App::new(Assets).unwrap();

View File

@ -0,0 +1,82 @@
use std::{
io::{Read, Write},
net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream},
thread,
time::Duration,
};
const PORT: u16 = 43739;
const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
const ADDRESS: SocketAddr = SocketAddr::V4(SocketAddrV4::new(LOCALHOST, PORT));
const INSTANCE_HANDSHAKE: &str = "Zed Editor Instance Running";
const CONNECT_TIMEOUT: Duration = Duration::from_millis(10);
const RECEIVE_TIMEOUT: Duration = Duration::from_millis(35);
const SEND_TIMEOUT: Duration = Duration::from_millis(20);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IsOnlyInstance {
Yes,
No,
}
pub fn ensure_only_instance() -> IsOnlyInstance {
if check_got_handshake() {
return IsOnlyInstance::No;
}
let listener = match TcpListener::bind(ADDRESS) {
Ok(listener) => listener,
Err(err) => {
log::warn!("Error binding to single instance port: {err}");
if check_got_handshake() {
return IsOnlyInstance::No;
}
// Avoid failing to start when some other application by chance already has
// a claim on the port. This is sub-par as any other instance that gets launched
// will be unable to communicate with this instance and will duplicate
log::warn!("Backup handshake request failed, continuing without handshake");
return IsOnlyInstance::Yes;
}
};
thread::spawn(move || {
for stream in listener.incoming() {
let mut stream = match stream {
Ok(stream) => stream,
Err(_) => return,
};
_ = stream.set_nodelay(true);
_ = stream.set_read_timeout(Some(SEND_TIMEOUT));
_ = stream.write_all(INSTANCE_HANDSHAKE.as_bytes());
}
});
IsOnlyInstance::Yes
}
fn check_got_handshake() -> bool {
match TcpStream::connect_timeout(&ADDRESS, CONNECT_TIMEOUT) {
Ok(mut stream) => {
let mut buf = vec![0u8; INSTANCE_HANDSHAKE.len()];
stream.set_read_timeout(Some(RECEIVE_TIMEOUT)).unwrap();
if let Err(err) = stream.read_exact(&mut buf) {
log::warn!("Connected to single instance port but failed to read: {err}");
return false;
}
if buf == INSTANCE_HANDSHAKE.as_bytes() {
log::info!("Got instance handshake");
return true;
}
log::warn!("Got wrong instance handshake value");
false
}
Err(_) => false,
}
}

View File

@ -1,6 +1,7 @@
pub mod assets; pub mod assets;
pub mod languages; pub mod languages;
pub mod menus; pub mod menus;
pub mod only_instance;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub mod test; pub mod test;