mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-20 02:47:34 +03:00
Prevent duplicate instances by coordinating via a socket (#2691)
We've been getting a bunch of panics from duplicate app instances competing over the local sqlite DB. After chatting with @mikayla-maki we determined it was probably best to add our own mechanism to prevent duplicates rather than just relying on the OS. My logic is that we'd need to build a system like this eventually for Windows/Linux anyway so it's more appealing than reworking our local DB access to be able to cooperate with another process while likely isn't something we want to support anyway. I attempted to keep this mechanism conservative so in the case of another program interfering with it we should fail somewhat gracefully and still continue to launch, albeit without the ability to prevent another instance from launching. Fixes https://linear.app/zed-industries/issue/Z-2435/thread-background-executor-1-panicked-at-could-not-send-write-action Release Notes: - Added a mechanism to prevent duplicate Zed instances from launching to avoid a crash.
This commit is contained in:
commit
da7dce79f6
@ -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()
|
||||||
|
@ -41,8 +41,7 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
|
|||||||
const DB_FILE_NAME: &'static str = "db.sqlite";
|
const DB_FILE_NAME: &'static str = "db.sqlite";
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
// !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING
|
pub static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
|
||||||
static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
|
|
||||||
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
|
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
|
||||||
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
|
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
103
crates/zed/src/only_instance.rs
Normal file
103
crates/zed/src/only_instance.rs
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
use std::{
|
||||||
|
io::{Read, Write},
|
||||||
|
net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream},
|
||||||
|
thread,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use util::channel::ReleaseChannel;
|
||||||
|
|
||||||
|
const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
|
||||||
|
const CONNECT_TIMEOUT: Duration = Duration::from_millis(10);
|
||||||
|
const RECEIVE_TIMEOUT: Duration = Duration::from_millis(35);
|
||||||
|
const SEND_TIMEOUT: Duration = Duration::from_millis(20);
|
||||||
|
|
||||||
|
fn address() -> SocketAddr {
|
||||||
|
let port = match *util::channel::RELEASE_CHANNEL {
|
||||||
|
ReleaseChannel::Dev => 43737,
|
||||||
|
ReleaseChannel::Preview => 43738,
|
||||||
|
ReleaseChannel::Stable => 43739,
|
||||||
|
};
|
||||||
|
|
||||||
|
SocketAddr::V4(SocketAddrV4::new(LOCALHOST, port))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instance_handshake() -> &'static str {
|
||||||
|
match *util::channel::RELEASE_CHANNEL {
|
||||||
|
ReleaseChannel::Dev => "Zed Editor Dev Instance Running",
|
||||||
|
ReleaseChannel::Preview => "Zed Editor Preview Instance Running",
|
||||||
|
ReleaseChannel::Stable => "Zed Editor Stable Instance Running",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum IsOnlyInstance {
|
||||||
|
Yes,
|
||||||
|
No,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_only_instance() -> IsOnlyInstance {
|
||||||
|
if *db::ZED_STATELESS {
|
||||||
|
return IsOnlyInstance::Yes;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user