feat: add single instance enforcement with TCP socket lock

Add single instance check using TCP listener on 127.0.0.1:53190 to prevent multiple application instances. Move AppState initialization into setup closure to include single_instance_lock field. Remove window close prevention and focus restoration handlers. Make main window non-resizable and non-maximizable.
This commit is contained in:
2026-03-18 07:06:20 +01:00
parent 31369a7743
commit d72a32cce1
2 changed files with 14 additions and 18 deletions

View File

@@ -1,6 +1,6 @@
mod tunnel_manager; mod tunnel_manager;
use std::{fs, path::PathBuf, sync::Mutex}; use std::{fs, net::TcpListener, path::PathBuf, sync::Mutex};
use base64::{engine::general_purpose::STANDARD, Engine as _}; use base64::{engine::general_purpose::STANDARD, Engine as _};
use rand_core::OsRng; use rand_core::OsRng;
@@ -15,9 +15,11 @@ use x25519_dalek::{PublicKey, StaticSecret};
const PROFILE_NAME: &str = "NexaVPN"; const PROFILE_NAME: &str = "NexaVPN";
const MAIN_WINDOW_LABEL: &str = "main"; const MAIN_WINDOW_LABEL: &str = "main";
const SINGLE_INSTANCE_ADDR: &str = "127.0.0.1:53190";
struct AppState { struct AppState {
session: Mutex<Option<SessionState>>, session: Mutex<Option<SessionState>>,
single_instance_lock: TcpListener,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -362,12 +364,6 @@ fn ensure_app_dir(app: &AppHandle) -> Result<PathBuf, String> {
Ok(dir) Ok(dir)
} }
fn restore_window(window: &Window) {
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
}
fn restore_webview_window(window: &WebviewWindow) { fn restore_webview_window(window: &WebviewWindow) {
let _ = window.show(); let _ = window.show();
let _ = window.unminimize(); let _ = window.unminimize();
@@ -382,6 +378,9 @@ fn hide_main_window(window: &Window) {
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.setup(|app| { .setup(|app| {
let single_instance_lock = TcpListener::bind(SINGLE_INSTANCE_ADDR)
.map_err(|_| format!("{} is already running.", app.package_info().name))?;
let open_item = MenuItemBuilder::with_id("open", "Open NexaVPN").build(app)?; let open_item = MenuItemBuilder::with_id("open", "Open NexaVPN").build(app)?;
let quit_item = MenuItemBuilder::with_id("quit", "Quit NexaVPN").build(app)?; let quit_item = MenuItemBuilder::with_id("quit", "Quit NexaVPN").build(app)?;
let menu = MenuBuilder::new(app).items(&[&open_item, &quit_item]).build()?; let menu = MenuBuilder::new(app).items(&[&open_item, &quit_item]).build()?;
@@ -418,24 +417,20 @@ pub fn run() {
}) })
.build(app)?; .build(app)?;
app.manage(AppState {
session: Mutex::new(None),
single_instance_lock,
});
Ok(()) Ok(())
}) })
.manage(AppState {
session: Mutex::new(None),
})
.on_window_event(|window, event| match event { .on_window_event(|window, event| match event {
WindowEvent::CloseRequested { api, .. } => { WindowEvent::CloseRequested { .. } => {}
api.prevent_close();
hide_main_window(window);
}
WindowEvent::Resized(_) => { WindowEvent::Resized(_) => {
if window.is_minimized().unwrap_or(false) { if window.is_minimized().unwrap_or(false) {
hide_main_window(window); hide_main_window(window);
} }
} }
WindowEvent::Focused(true) => {
restore_window(window);
}
_ => {} _ => {}
}) })
.invoke_handler(tauri::generate_handler![load_state, clear_session, enroll_device, sync_profile, connect_tunnel, disconnect_tunnel, tunnel_status]) .invoke_handler(tauri::generate_handler![load_state, clear_session, enroll_device, sync_profile, connect_tunnel, disconnect_tunnel, tunnel_status])

View File

@@ -15,7 +15,8 @@
"title": "NexaVPN", "title": "NexaVPN",
"width": 1120, "width": 1120,
"height": 760, "height": 760,
"resizable": true "resizable": false,
"maximizable": false
} }
], ],
"security": { "security": {