Desktop: Limit application to a single instance (#3441)

* only allow single instance

* more reliable CEF cache cleanup

* some cleanup

* fix lock file location

* add simple signal handling

* fix skew handles on desktop

* mac remove unused helpers
This commit is contained in:
Timon
2025-12-03 18:13:15 +00:00
committed by GitHub
parent 600fb5c28f
commit 39b5229df7
15 changed files with 206 additions and 34 deletions

View File

@@ -56,6 +56,13 @@ impl App {
app_event_scheduler: AppEventScheduler,
launch_documents: Vec<PathBuf>,
) -> Self {
let ctrlc_app_event_scheduler = app_event_scheduler.clone();
ctrlc::set_handler(move || {
tracing::info!("Termination signal received, exiting...");
ctrlc_app_event_scheduler.schedule(AppEvent::CloseWindow);
})
.expect("Error setting Ctrl-C handler");
let rendering_app_event_scheduler = app_event_scheduler.clone();
let (start_render_sender, start_render_receiver) = std::sync::mpsc::sync_channel(1);
std::thread::spawn(move || {
@@ -365,6 +372,7 @@ impl App {
tracing::info!("Exiting main event loop");
event_loop.exit();
}
#[cfg(target_os = "macos")]
AppEvent::MenuEvent { id } => {
self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id });
}

View File

@@ -11,7 +11,7 @@ use super::CefContext;
use super::singlethreaded::SingleThreadedCefContext;
use crate::cef::CefEventHandler;
use crate::cef::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
use crate::cef::dirs::create_instance_dir;
use crate::cef::dirs::{create_instance_dir, delete_instance_dirs};
use crate::cef::input::InputState;
use crate::cef::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderProcessAppImpl, SchemeHandlerFactoryImpl};
@@ -85,6 +85,7 @@ impl<H: CefEventHandler> CefContextBuilder<H> {
#[cfg(target_os = "macos")]
pub(crate) fn initialize(self, event_handler: H, disable_gpu_acceleration: bool) -> Result<impl CefContext, InitError> {
delete_instance_dirs();
let instance_dir = create_instance_dir();
let exe = std::env::current_exe().expect("cannot get current exe path");
@@ -105,6 +106,7 @@ impl<H: CefEventHandler> CefContextBuilder<H> {
#[cfg(not(target_os = "macos"))]
pub(crate) fn initialize(self, event_handler: H, disable_gpu_acceleration: bool) -> Result<impl CefContext, InitError> {
delete_instance_dirs();
let instance_dir = create_instance_dir();
let settings = Settings {

View File

@@ -43,7 +43,19 @@ impl CefContext for SingleThreadedCefContext {
impl Drop for SingleThreadedCefContext {
fn drop(&mut self) {
cef::shutdown();
std::fs::remove_dir_all(&self.instance_dir).expect("Failed to remove CEF cache directory");
// Sometimes some CEF processes still linger at this point and hold file handles to the cache directory.
// To mitigate this, we try to remove the directory multiple times with some delay.
// TODO: find a better solution if possible.
for _ in 0..30 {
match std::fs::remove_dir_all(&self.instance_dir) {
Ok(_) => break,
Err(e) => {
tracing::warn!("Failed to remove CEF cache directory, retrying...: {e}");
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
}
}
}

View File

@@ -1,12 +1,24 @@
use std::path::PathBuf;
use crate::dirs::{ensure_dir_exists, graphite_data_dir};
use crate::dirs::{app_data_dir, ensure_dir_exists};
static CEF_DIR_NAME: &str = "browser";
pub(crate) fn delete_instance_dirs() {
let cef_dir = app_data_dir().join(CEF_DIR_NAME);
if let Ok(entries) = std::fs::read_dir(&cef_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let _ = std::fs::remove_dir_all(&path);
}
}
}
}
pub(crate) fn create_instance_dir() -> PathBuf {
let instance_id: String = (0..32).map(|_| format!("{:x}", rand::random::<u8>() % 16)).collect();
let path = graphite_data_dir().join(CEF_DIR_NAME).join(instance_id);
let path = app_data_dir().join(CEF_DIR_NAME).join(instance_id);
ensure_dir_exists(&path);
path
}

View File

@@ -1,4 +1,4 @@
use cef::sys::{cef_event_flags_t, cef_key_event_type_t, cef_mouse_button_type_t};
use cef::sys::{cef_key_event_type_t, cef_mouse_button_type_t};
use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, MouseEvent};
use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, WindowEvent};
@@ -6,7 +6,7 @@ mod keymap;
use keymap::{ToCharRepresentation, ToNativeKeycode, ToVKBits};
mod state;
pub(crate) use state::InputState;
pub(crate) use state::{CefModifiers, InputState};
use super::consts::{PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y};
@@ -129,9 +129,10 @@ pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputStat
}
let Some(host) = browser.host() else { return };
let mut mouse_event: MouseEvent = input_state.into();
mouse_event.modifiers |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0 as u32;
mouse_event.modifiers |= cef_event_flags_t::EVENTFLAG_PRECISION_SCROLLING_DELTA.0 as u32;
let mouse_event = MouseEvent {
modifiers: CefModifiers::PINCH_MODIFIERS.into(),
..input_state.into()
};
let delta = (delta * PINCH_ZOOM_SPEED).round() as i32;

View File

@@ -240,10 +240,17 @@ impl CefModifiers {
Self(inner)
}
pub(super) const PINCH_MODIFIERS: Self = Self(cef_event_flags_t(
cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0 | cef_event_flags_t::EVENTFLAG_PRECISION_SCROLLING_DELTA.0,
));
}
impl Into<u32> for CefModifiers {
fn into(self) -> u32 {
self.0.0 as u32
impl From<CefModifiers> for u32 {
fn from(val: CefModifiers) -> Self {
#[cfg(not(target_os = "windows"))]
return val.0.0;
#[cfg(target_os = "windows")]
return val.0.0 as u32;
}
}

View File

@@ -1,7 +1,8 @@
pub(crate) const APP_NAME: &str = "Graphite";
pub(crate) const APP_ID: &str = "rs.graphite.Graphite";
pub(crate) const APP_DIRECTORY_NAME: &str = "graphite-editor";
pub(crate) const APP_DIRECTORY_NAME: &str = "graphite";
pub(crate) const APP_LOCK_FILE_NAME: &str = "instance.lock";
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron";
pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents";

View File

@@ -9,14 +9,14 @@ pub(crate) fn ensure_dir_exists(path: &PathBuf) {
}
}
pub(crate) fn graphite_data_dir() -> PathBuf {
pub(crate) fn app_data_dir() -> PathBuf {
let path = dirs::data_dir().expect("Failed to get data directory").join(APP_DIRECTORY_NAME);
ensure_dir_exists(&path);
path
}
pub(crate) fn graphite_autosave_documents_dir() -> PathBuf {
let path = graphite_data_dir().join(APP_DOCUMENTS_DIRECTORY_NAME);
pub(crate) fn app_autosave_documents_dir() -> PathBuf {
let path = app_data_dir().join(APP_DOCUMENTS_DIRECTORY_NAME);
ensure_dir_exists(&path);
path
}

View File

@@ -9,7 +9,10 @@ pub(crate) enum AppEvent {
DesktopWrapperMessage(DesktopWrapperMessage),
NodeGraphExecutionResult(NodeGraphExecutionResult),
CloseWindow,
MenuEvent { id: String },
#[cfg(target_os = "macos")]
MenuEvent {
id: String,
},
}
#[derive(Clone)]

View File

@@ -23,6 +23,8 @@ use cef::CefHandler;
use cli::Cli;
use event::CreateAppEventSchedulerEventLoopExt;
use crate::consts::APP_LOCK_FILE_NAME;
pub fn start() {
tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init();
@@ -36,6 +38,22 @@ pub fn start() {
return;
}
let mut lock = pidlock::Pidlock::new_validated(dirs::app_data_dir().join(APP_LOCK_FILE_NAME)).unwrap();
match lock.acquire() {
Ok(lock) => {
tracing::info!("Acquired application lock");
lock
}
Err(pidlock::PidlockError::LockExists) => {
tracing::error!("Another instance is already running, Exiting.");
exit(0);
}
Err(err) => {
tracing::error!("Failed to acquire application lock: {err}");
exit(1);
}
};
App::init();
let cli = Cli::parse();
@@ -56,7 +74,7 @@ pub fn start() {
}
Err(cef::InitError::AlreadyRunning) => {
tracing::error!("Another instance is already running, Exiting.");
exit(0);
exit(1);
}
Err(cef::InitError::InitializationFailed(code)) => {
tracing::error!("Cef initialization failed with code: {code}");

View File

@@ -125,13 +125,13 @@ impl PersistentData {
}
fn state_file_path() -> std::path::PathBuf {
let mut path = crate::dirs::graphite_data_dir();
let mut path = crate::dirs::app_data_dir();
path.push(crate::consts::APP_STATE_FILE_NAME);
path
}
fn preferences_file_path() -> std::path::PathBuf {
let mut path = crate::dirs::graphite_data_dir();
let mut path = crate::dirs::app_data_dir();
path.push(crate::consts::APP_PREFERENCES_FILE_NAME);
path
}
@@ -189,7 +189,7 @@ impl DocumentStore {
}
fn document_path(id: &DocumentId) -> std::path::PathBuf {
let mut path = crate::dirs::graphite_autosave_documents_dir();
let mut path = crate::dirs::app_autosave_documents_dir();
path.push(format!("{:x}.graphite", id.0));
path
}