use std::path::PathBuf; use std::sync::Arc; use tokio::sync::{watch, Mutex}; use serde::{Deserialize, Serialize}; use anyhow::Result; use ostp_client::bridge::BridgeMetrics; use portable_atomic::Ordering; use tauri::Emitter; mod ipc_crypto; // ── Config types ───────────────────────────────────────────────────────────── #[derive(Debug, Deserialize, Serialize, Clone)] #[serde(tag = "mode", rename_all = "lowercase")] enum AppMode { Server(serde_json::Value), #[serde(rename = "client")] Client(serde_json::Value), } #[derive(Debug, Deserialize, Serialize, Clone)] struct UnifiedConfig { #[serde(flatten)] mode: AppMode, log_level: Option, } #[derive(Serialize)] struct UIMetrics { bytes_sent: u64, bytes_recv: u64, rtt_ms: u32, } // ── Messages exchanged with the privileged helper ──────────────────────────── #[derive(Deserialize, Clone)] #[serde(tag = "type", rename_all = "lowercase")] enum HelperMsg { Status { value: u8 }, Log { message: String }, Metrics { bytes_sent: u64, bytes_recv: u64, rtt_ms: u32 }, Error { message: String }, } // ── Application state ───────────────────────────────────────────────────────── struct InProcessState { shutdown_tx: Option>, config_tx: Option>, metrics: Arc, handle: tokio::task::JoinHandle>, error_msg: Arc>>, } struct HelperState { pipe_state: Arc>, cmd_tx: tokio::sync::mpsc::Sender, token: String, port: u16, } enum TunnelHandle { InProcess(InProcessState), Helper(HelperState), } struct AppStateInner { tunnel: Option, } impl Drop for AppStateInner { fn drop(&mut self) { if let Some(TunnelHandle::InProcess(ref mut s)) = self.tunnel { if let Some(tx) = s.shutdown_tx.take() { let _ = tx.send(true); } } } } struct AppState(Mutex); // ── Config helpers ──────────────────────────────────────────────────────────── fn get_config_path() -> PathBuf { if let Ok(exe_path) = std::env::current_exe() { if let Some(parent) = exe_path.parent() { let path = parent.join("config.json"); if path.exists() { return path; } } } PathBuf::from("config.json") } // ── Tauri commands ──────────────────────────────────────────────────────────── /// Returns the directory path where wintun.dll should be placed. #[tauri::command] fn get_wintun_install_path() -> String { if let Some(helper) = find_helper_exe() { if let Some(dir) = helper.parent() { return dir.to_string_lossy().into_owned(); } } if let Ok(cwd) = std::env::current_dir() { return cwd.to_string_lossy().into_owned(); } String::new() } /// Sets or removes the app from Windows startup (HKCU\...\Run). #[tauri::command] fn set_autostart(enable: bool) -> Result<(), String> { #[cfg(target_os = "windows")] { use std::process::Command; let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run"; let app_name = "OSTP"; if enable { let exe = std::env::current_exe() .map_err(|e| format!("Cannot get exe path: {}", e))?; let exe_str = format!("\"{}\"", exe.to_string_lossy()); let out = Command::new("reg") .args(["add", key, "/v", app_name, "/t", "REG_SZ", "/d", &exe_str, "/f"]) .output() .map_err(|e| format!("reg add failed: {}", e))?; if !out.status.success() { return Err(String::from_utf8_lossy(&out.stderr).to_string()); } } else { let _ = Command::new("reg") .args(["delete", key, "/v", app_name, "/f"]) .output(); } } Ok(()) } /// Checks if the app is currently in Windows startup. #[tauri::command] fn get_autostart() -> bool { #[cfg(target_os = "windows")] { use std::process::Command; let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run"; let out = Command::new("reg") .args(["query", key, "/v", "OSTP"]) .output(); if let Ok(o) = out { return o.status.success(); } } false } /// Returns a sorted, deduplicated list of currently running process names. #[tauri::command] fn list_running_processes() -> Vec { #[cfg(target_os = "windows")] { use std::process::Command; if let Ok(out) = Command::new("tasklist") .args(["/FO", "CSV", "/NH"]) .output() { let text = String::from_utf8_lossy(&out.stdout); let mut names: std::collections::BTreeSet = std::collections::BTreeSet::new(); for line in text.lines() { // CSV format: "chrome.exe","1234","Console","1","123,456 K" let name = line.trim_matches('"').split('"').next().unwrap_or(""); if !name.is_empty() && name.ends_with(".exe") { names.insert(name.to_string()); } } return names.into_iter().collect(); } } #[cfg(target_os = "linux")] { use std::process::Command; if let Ok(out) = Command::new("ps") .args(["-e", "-o", "comm="]) .output() { let text = String::from_utf8_lossy(&out.stdout); let mut names: std::collections::BTreeSet = std::collections::BTreeSet::new(); for line in text.lines() { let name = line.trim(); if !name.is_empty() { names.insert(name.to_string()); } } return names.into_iter().collect(); } } #[cfg(target_os = "macos")] { use std::process::Command; if let Ok(out) = Command::new("ps") .args(["-e", "-o", "comm="]) .output() { let text = String::from_utf8_lossy(&out.stdout); let mut names: std::collections::BTreeSet = std::collections::BTreeSet::new(); for line in text.lines() { let name = line.trim().split('/').last().unwrap_or(""); if !name.is_empty() { names.insert(name.to_string()); } } return names.into_iter().collect(); } } vec![] } #[tauri::command] async fn get_config() -> Result { let path = get_config_path(); if !path.exists() { return Ok(r#"{ "_comment": "OSTP Client Configuration", "mode": "client", "log_level": "info", "_comment_api": "Management API Server (used by control panel)", "api": { "enabled": true, "bind": "127.0.0.1:50001", "token": "admin-secret-token" }, "_comment_server": "Address of the remote OSTP server", "server": "127.0.0.1:50000", "_comment_access_key": "Must match one of the access_keys on the server", "access_key": "your-secret-access-key-hex-or-base64", "_comment_socks5_bind": "The local port where the system/browser should connect (HTTP/SOCKS5)", "socks5_bind": "127.0.0.1:1088", "_comment_tun": "Virtual network adapter settings (requires tun2socks.exe to be present)", "tun": { "enable": false, "wintun_path": "./wintun.dll", "ipv4_address": "10.1.0.2/24", "dns": "1.1.1.1", "kill_switch": false }, "_comment_exclude": "Bypass tunnel for these domains/IPs (only works in proxy mode)", "exclude": { "domains": ["localhost", "127.0.0.1"], "ips": [], "processes": [] }, "mux": { "enabled": false, "sessions": 1 }, "debug": false }"#.into()); } std::fs::read_to_string(&path) .map_err(|e| format!("Failed to read config: {}", e)) } #[tauri::command] async fn save_config(json_content: String) -> Result { // Strip JSONC comments before validation let mut stripped = json_comments::StripComments::new(json_content.as_bytes()); let _parsed: UnifiedConfig = serde_json::from_reader(&mut stripped) .map_err(|e| format!("Invalid configuration: {}", e))?; let path = get_config_path(); let mut final_content = json_content; if !final_content.trim_start().starts_with("// OSTP") { let header = "// OSTP Configuration v0.3.1\n// DO NOT EDIT THIS COMMENT - Migrator relies on it\n"; final_content = format!("{}{}", header, final_content); } std::fs::write(path, final_content).map_err(|e| format!("Failed to write config: {}", e))?; Ok(true) } #[tauri::command] async fn get_tunnel_status(state: tauri::State<'_, AppState>) -> Result { let guard = state.0.lock().await; match &guard.tunnel { None => Ok(0), Some(TunnelHandle::InProcess(s)) => { let finished = s.handle.is_finished(); let conn_state = s.metrics.connection_state.load(Ordering::Relaxed); eprintln!("[OSTP] get_tunnel_status InProcess: finished={} conn_state={}", finished, conn_state); if finished { let mut err_guard = s.error_msg.lock().await; if let Some(e) = err_guard.take() { eprintln!("[OSTP] get_tunnel_status returning Err: {}", e); return Err(e); } return Ok(0); } Ok(conn_state) } Some(TunnelHandle::Helper(h)) => { let mut ps = h.pipe_state.lock().await; eprintln!("[OSTP] get_tunnel_status Helper: conn_state={}", ps.connection_state); if ps.connection_state == 0 { if let Some(e) = ps.error_msg.take() { eprintln!("[OSTP] get_tunnel_status returning Err: {}", e); return Err(e); } } Ok(ps.connection_state) } } } #[tauri::command] async fn get_metrics(state: tauri::State<'_, AppState>) -> Result, String> { let guard = state.0.lock().await; match &guard.tunnel { None => Ok(None), Some(TunnelHandle::InProcess(s)) => Ok(Some(UIMetrics { bytes_sent: s.metrics.bytes_sent.load(Ordering::Relaxed), bytes_recv: s.metrics.bytes_recv.load(Ordering::Relaxed), rtt_ms: s.metrics.rtt_ms.load(Ordering::Relaxed), })), Some(TunnelHandle::Helper(h)) => { let ps = h.pipe_state.lock().await; Ok(Some(UIMetrics { bytes_sent: ps.bytes_sent, bytes_recv: ps.bytes_recv, rtt_ms: ps.rtt_ms, })) } } } #[tauri::command] async fn reload_tunnel(state: tauri::State<'_, AppState>) -> Result { let guard = state.0.lock().await; if guard.tunnel.is_none() { return Ok(false); } let path = get_config_path(); let content = std::fs::read_to_string(&path) .map_err(|e| format!("Read config error: {}", e))?; let mut stripped = json_comments::StripComments::new(content.as_bytes()); let unified: UnifiedConfig = serde_json::from_reader(&mut stripped) .map_err(|e| format!("Parse config error: {}", e))?; let client_cfg = match unified.mode { AppMode::Client(c) => c, AppMode::Server(_) => return Err("GUI only supports Client mode.".into()), }; let (migrated, _) = ostp_client::config::ClientConfig::migrate_json(client_cfg); let core_cfg: ostp_client::config::ClientConfig = serde_json::from_value(migrated) .map_err(|e| format!("Failed to parse migrated config: {}", e))?; let config_str = serde_json::to_string(&core_cfg).unwrap(); match &guard.tunnel { Some(TunnelHandle::Helper(h)) => { let cmd = format!( "{{\"cmd\":\"reload\",\"config\":{},\"token\":\"{}\"}}\n", serde_json::to_string(&config_str).unwrap(), h.token ); let _ = h.cmd_tx.send(cmd).await; } Some(TunnelHandle::InProcess(s)) => { // Hot-reload exclusions by pushing new config into the watch channel. // If config_tx is None (old tunnel without this feature), return false. if let Some(ref tx) = s.config_tx { let _ = tx.send(core_cfg); return Ok(true); } return Ok(false); } None => {} } Ok(true) } #[tauri::command] async fn stop_tunnel(state: tauri::State<'_, AppState>) -> Result { let mut guard = state.0.lock().await; match guard.tunnel.take() { None => {} Some(TunnelHandle::InProcess(mut s)) => { if let Some(tx) = s.shutdown_tx.take() { let _ = tx.send(true); } s.handle.abort(); // Brief wait for cleanup let _ = tokio::time::timeout( std::time::Duration::from_secs(2), s.handle, ).await; } Some(TunnelHandle::Helper(h)) => { let stop_cmd = serde_json::json!({ "cmd": "stop", "token": h.token }).to_string(); let _ = h.cmd_tx.send(format!("{}\n", stop_cmd)).await; } } Ok(true) } #[tauri::command] async fn start_tunnel(state: tauri::State<'_, AppState>, app: tauri::AppHandle) -> Result { let mut guard = state.0.lock().await; if let Some(ref t) = guard.tunnel { match t { TunnelHandle::InProcess(s) if !s.handle.is_finished() => return Ok(true), TunnelHandle::Helper(_) => return Ok(true), _ => {} } } guard.tunnel = None; let path = get_config_path(); let content = std::fs::read_to_string(&path).map_err(|e| e.to_string())?; let mut stripped = json_comments::StripComments::new(content.as_bytes()); let unified: UnifiedConfig = serde_json::from_reader(&mut stripped) .map_err(|e| format!("Config parse error: {}", e))?; let client_cfg = match unified.mode { AppMode::Client(c) => c, AppMode::Server(_) => return Err("GUI only supports Client mode.".into()), }; let (migrated, _) = ostp_client::config::ClientConfig::migrate_json(client_cfg); let is_tun_enabled = migrated.get("inbounds") .and_then(|i| i.as_array()) .map(|i| i.iter().any(|v| v.get("type").and_then(|t| t.as_str()) == Some("tun"))) .unwrap_or(false); let parsed_config: ostp_client::config::ClientConfig = serde_json::from_value(migrated) .map_err(|e| format!("Failed to parse migrated config: {}", e))?; eprintln!("[OSTP] start_tunnel: is_tun_enabled={}", is_tun_enabled); #[cfg(target_os = "windows")] if is_tun_enabled { let mut found = false; if let Ok(cwd) = std::env::current_dir() { let p = cwd.join("wintun.dll"); eprintln!("[OSTP] checking wintun at: {:?} exists={}", p, p.exists()); if p.exists() { found = true; } } if !found { if let Some(helper) = find_helper_exe() { eprintln!("[OSTP] helper exe found at: {:?}", helper); if let Some(dir) = helper.parent() { let p = dir.join("wintun.dll"); eprintln!("[OSTP] checking wintun at: {:?} exists={}", p, p.exists()); if p.exists() { found = true; } } } else { eprintln!("[OSTP] helper exe NOT FOUND"); } } if !found { eprintln!("[OSTP] WINTUN_MISSING — returning error"); return Err("WINTUN_MISSING".to_string()); } } if is_tun_enabled { eprintln!("[OSTP] starting TUN via helper"); start_tun_via_helper(&mut guard, &parsed_config, app).await } else { eprintln!("[OSTP] starting proxy in-process"); start_proxy_in_process(&mut guard, &parsed_config, app).await } } async fn start_proxy_in_process( guard: &mut AppStateInner, parsed_config: &ostp_client::config::ClientConfig, app: tauri::AppHandle, ) -> Result { let mapped = parsed_config.clone(); let metrics = Arc::new(BridgeMetrics { bytes_sent: portable_atomic::AtomicU64::new(0), bytes_recv: portable_atomic::AtomicU64::new(0), // Start at 1 (connecting) so UI polling doesn't see 0 and flip back to disconnected // before the handshake task has had a chance to begin. connection_state: portable_atomic::AtomicU8::new(1), rtt_ms: portable_atomic::AtomicU32::new(0), }); let (shutdown_tx, shutdown_rx) = watch::channel(false); // Config hot-reload channel: allows updating exclusions while tunnel is running. let (config_tx, config_rx) = watch::channel(mapped.clone()); let metrics_clone = metrics.clone(); let error_msg = Arc::new(tokio::sync::Mutex::new(None)); let error_msg_clone = error_msg.clone(); let handle = tokio::spawn(async move { match ostp_client::runner::run_client_core(mapped, metrics_clone, shutdown_rx, Some(config_rx)).await { Ok(_) => Ok(()), Err(e) => { let mut err_guard = error_msg_clone.lock().await; *err_guard = Some(e.to_string()); let _ = app.emit("tunnel-error", e.to_string()); Err(e.to_string()) } } }); guard.tunnel = Some(TunnelHandle::InProcess(InProcessState { shutdown_tx: Some(shutdown_tx), config_tx: Some(config_tx), metrics, handle, error_msg, })); Ok(true) } async fn start_tun_via_helper( guard: &mut AppStateInner, parsed_config: &ostp_client::config::ClientConfig, app: tauri::AppHandle, ) -> Result { let port = { let listener = std::net::TcpListener::bind("127.0.0.1:0") .map_err(|e| format!("Bind error: {}", e))?; listener.local_addr() .map_err(|e| format!("Get local_addr failed: {}", e))?.port() }; let auth_token = rand::random::().to_string(); let helper_exe = find_helper_exe() .ok_or_else(|| "ostp-tun-helper.exe not found.".to_string())?; launch_as_admin(&helper_exe, &auth_token, port) .map_err(|e| format!("Failed to launch helper: {}", e))?; tokio::time::sleep(std::time::Duration::from_millis(1500)).await; let socket = tokio::time::timeout(std::time::Duration::from_secs(15), async { loop { match tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port)).await { Ok(s) => return Ok::<_, std::io::Error>(s), Err(_) => tokio::time::sleep(std::time::Duration::from_millis(200)).await, } } }).await.map_err(|_| "Timeout connecting to helper (15s)".to_string())? .map_err(|e| e.to_string())?; let key = ipc_crypto::derive_key(&auth_token); let crypto = ipc_crypto::IpcCrypto::new(&key); let mapped = parsed_config.clone(); let start_cmd = serde_json::json!({ "cmd": "start", "config": serde_json::to_string(&mapped).unwrap_or_default(), "token": auth_token }).to_string(); let encrypted_cmd = crypto.encrypt(start_cmd.as_bytes()) .map_err(|e| format!("Encryption failed: {}", e))?; let encoded_cmd = hex::encode(&encrypted_cmd); let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::channel::(16); let pipe_state = Arc::new(Mutex::new(HelperPipeState { connection_state: 1, bytes_sent: 0, bytes_recv: 0, rtt_ms: 0, error_msg: None })); let state_for_task = pipe_state.clone(); let crypto_for_task = ipc_crypto::IpcCrypto::new(&key); tokio::spawn(async move { use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, split}; let (reader_half, mut writer_half) = split(socket); let mut reader = BufReader::new(reader_half); let _ = writer_half.write_all(format!("{}\n", encoded_cmd).as_bytes()).await; let mut line = String::new(); loop { tokio::select! { result = reader.read_line(&mut line) => { if result.unwrap_or(0) == 0 { break; } let trimmed = line.trim().to_string(); line.clear(); if let Ok(encrypted_bytes) = hex::decode(&trimmed) { if let Ok(decrypted) = crypto_for_task.decrypt(&encrypted_bytes) { if let Ok(msg_str) = String::from_utf8(decrypted) { if let Ok(msg) = serde_json::from_str::(&msg_str) { let mut s = state_for_task.lock().await; match msg { HelperMsg::Status { value } => s.connection_state = value, HelperMsg::Metrics { bytes_sent, bytes_recv, rtt_ms } => { s.bytes_sent = bytes_sent; s.bytes_recv = bytes_recv; s.rtt_ms = rtt_ms; } HelperMsg::Error { message } => { s.connection_state = 0; s.error_msg = Some(message.clone()); eprintln!("Helper error: {}", message); let _ = app.emit("tunnel-error", message); } _ => {} } } } } } } cmd = cmd_rx.recv() => { if let Some(c) = cmd { if let Ok(enc) = crypto_for_task.encrypt(c.as_bytes()) { let encoded = hex::encode(&enc); let _ = writer_half.write_all(format!("{}\n", encoded).as_bytes()).await; } } else { break; } } } } state_for_task.lock().await.connection_state = 0; }); guard.tunnel = Some(TunnelHandle::Helper(HelperState { pipe_state, cmd_tx, token: auth_token, port })); Ok(true) } struct HelperPipeState { connection_state: u8, bytes_sent: u64, bytes_recv: u64, rtt_ms: u32, error_msg: Option, } #[cfg(target_os = "windows")] const HELPER_EXE_NAME: &str = "ostp-tun-helper.exe"; #[cfg(not(target_os = "windows"))] const HELPER_EXE_NAME: &str = "ostp-tun-helper"; fn find_helper_exe() -> Option { if let Ok(exe) = std::env::current_exe() { if let Some(dir) = exe.parent() { // 1. Release/Production adjacent let candidate = dir.join(HELPER_EXE_NAME); if candidate.exists() { return Some(candidate); } // 2. Tauri target directory fallback // e.g. from ostp-gui/src-tauri/target/debug/deps/ let mut parent = dir; while let Some(p) = parent.parent() { if p.file_name().map(|n| n == "target").unwrap_or(false) { let deb = p.join("debug").join(HELPER_EXE_NAME); if deb.exists() { return Some(deb); } let rel = p.join("release").join(HELPER_EXE_NAME); if rel.exists() { return Some(rel); } } parent = p; } } } // 3. Current working directory target fallback let cwd = std::env::current_dir().unwrap_or_default(); let candidates = [ cwd.join(HELPER_EXE_NAME), cwd.join("target").join("debug").join(HELPER_EXE_NAME), cwd.join("target").join("release").join(HELPER_EXE_NAME), cwd.join("..").join("target").join("debug").join(HELPER_EXE_NAME), cwd.join("..").join("target").join("release").join(HELPER_EXE_NAME), cwd.join("..").join("..").join("target").join("debug").join(HELPER_EXE_NAME), cwd.join("..").join("..").join("target").join("release").join(HELPER_EXE_NAME), ]; for path in &candidates { if path.exists() { return Some(path.clone()); } } None } #[cfg(target_os = "windows")] fn launch_as_admin(exe: &std::path::PathBuf, token: &str, port: u16) -> anyhow::Result<()> { use std::ffi::OsStr; use std::os::windows::ffi::OsStrExt; use std::ptr::null_mut; let exe_wstr: Vec = exe.as_os_str().encode_wide().chain(Some(0)).collect(); let verb_wstr: Vec = OsStr::new("runas").encode_wide().chain(Some(0)).collect(); // Write token to temp file for security instead of passing via cmdline let temp_dir = std::env::temp_dir(); let token_file = temp_dir.join(format!("ostp_auth_{}.tmp", rand::random::())); std::fs::write(&token_file, token)?; let params_str = format!("--port {} --token-file \"{}\"", port, token_file.display()); let params_wstr: Vec = OsStr::new(¶ms_str).encode_wide().chain(Some(0)).collect(); #[link(name = "shell32")] extern "system" { fn ShellExecuteW(h: *mut std::ffi::c_void, op: *const u16, f: *const u16, p: *const u16, d: *const u16, s: i32) -> isize; } // Use the GUI executable's directory as the working directory so dependencies are found let cwd_path = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from(".")); let dir_wstr: Vec = cwd_path.parent().unwrap_or(std::path::Path::new(".")).as_os_str().encode_wide().chain(Some(0)).collect(); let ret = unsafe { ShellExecuteW(null_mut(), verb_wstr.as_ptr(), exe_wstr.as_ptr(), params_wstr.as_ptr(), dir_wstr.as_ptr(), 0) }; if ret <= 32 { anyhow::bail!("UAC denied or helper missing."); } Ok(()) } #[cfg(target_os = "macos")] fn launch_as_admin(exe: &std::path::PathBuf, token: &str, port: u16) -> anyhow::Result<()> { let temp_dir = std::env::temp_dir(); let token_file = temp_dir.join(format!("ostp_auth_{}.tmp", rand::random::())); std::fs::write(&token_file, token)?; let cmd = format!("'{}' --port {} --token-file '{}'", exe.display(), port, token_file.display()); let script = format!("do shell script \"{}\" with administrator privileges", cmd); let status = std::process::Command::new("osascript") .arg("-e") .arg(&script) .status()?; if !status.success() { anyhow::bail!("osascript failed"); } Ok(()) } #[cfg(target_os = "linux")] fn launch_as_admin(exe: &std::path::PathBuf, token: &str, port: u16) -> anyhow::Result<()> { let temp_dir = std::env::temp_dir(); let token_file = temp_dir.join(format!("ostp_auth_{}.tmp", rand::random::())); std::fs::write(&token_file, token)?; let status = std::process::Command::new("pkexec") .arg(exe) .arg("--port") .arg(port.to_string()) .arg("--token-file") .arg(&token_file) .status()?; if !status.success() { anyhow::bail!("pkexec failed"); } Ok(()) } #[cfg(target_os = "windows")] fn show_error_dialog(msg: &str) { use std::os::windows::ffi::OsStrExt; let msg_w: Vec = std::ffi::OsStr::new(msg).encode_wide().chain(Some(0)).collect(); let title_w: Vec = std::ffi::OsStr::new("OSTP GUI Error").encode_wide().chain(Some(0)).collect(); #[link(name = "user32")] extern "system" { fn MessageBoxW(hWnd: *mut std::ffi::c_void, lpText: *const u16, lpCaption: *const u16, uType: u32) -> i32; } unsafe { MessageBoxW(std::ptr::null_mut(), msg_w.as_ptr(), title_w.as_ptr(), 0x10); } // 0x10 is MB_ICONERROR } #[cfg(not(target_os = "windows"))] fn show_error_dialog(msg: &str) { println!("ERROR: {}", msg); } static SINGLE_INSTANCE_LOCK: std::sync::OnceLock = std::sync::OnceLock::new(); #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { if let Ok(listener) = std::net::TcpListener::bind("127.0.0.1:49153") { let _ = SINGLE_INSTANCE_LOCK.set(listener); } else { show_error_dialog("Приложение OSTP GUI уже запущено!"); return; } let state = AppState(Mutex::new(AppStateInner { tunnel: None })); tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .manage(state) .setup(|app| { use tauri::menu::{Menu, MenuItem}; use tauri::tray::{TrayIconBuilder, TrayIconEvent, MouseButton, MouseButtonState}; use tauri::{Manager, Emitter}; let config_path = get_config_path(); let mut masked_ip = String::from("0.0.0.0"); if config_path.exists() { if let Ok(content) = std::fs::read_to_string(&config_path) { let mut stripped = json_comments::StripComments::new(content.as_bytes()); if let Ok(val) = serde_json::from_reader::<_, serde_json::Value>(&mut stripped) { if let Some(server) = val.get("server").and_then(|s| s.as_str()) { let parts: Vec<&str> = server.split(':').collect(); let ip = parts[0]; let port = if parts.len() > 1 { parts[1] } else { "" }; let octets: Vec<&str> = ip.split('.').collect(); if octets.len() == 4 { masked_ip = format!("{}.{}.**.**:{}", octets[0], octets[1], port); } else if octets.len() > 2 { masked_ip = format!("{}...:{}", octets[0], port); } else { masked_ip = server.to_string(); } } } } } let connect_i = MenuItem::with_id(app, "connect", "Подключиться", true, None::<&str>)?; let disconnect_i = MenuItem::with_id(app, "disconnect", "Отключиться", true, None::<&str>)?; let server_i = MenuItem::with_id(app, "server", format!("Сервер: {}", masked_ip), false, None::<&str>)?; let version_i = MenuItem::with_id(app, "version", format!("OSTP v{}", env!("CARGO_PKG_VERSION")), false, None::<&str>)?; let show_i = MenuItem::with_id(app, "show", "Показать окно", true, None::<&str>)?; let exit_i = MenuItem::with_id(app, "exit", "Выход", true, None::<&str>)?; let menu = Menu::with_items(app, &[ &server_i, &version_i, &connect_i, &disconnect_i, &show_i, &exit_i, ])?; let _tray = TrayIconBuilder::new() .icon(app.default_window_icon().unwrap().clone()) .menu(&menu) .on_menu_event(|app, event| { match event.id.as_ref() { "connect" => { let _ = app.emit("tray_connect", ()); } "disconnect" => { let _ = app.emit("tray_disconnect", ()); } "show" => { if let Some(window) = app.get_webview_window("main") { let _ = window.show(); let _ = window.set_focus(); } } "exit" => { app.exit(0); } _ => {} } }) .on_tray_icon_event(|tray, event| { if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event { let app = tray.app_handle(); if let Some(window) = app.get_webview_window("main") { let _ = window.show(); let _ = window.set_focus(); } } }) .build(app)?; Ok(()) }) .on_window_event(|window, event| match event { tauri::WindowEvent::CloseRequested { api, .. } => { let _ = window.hide(); api.prevent_close(); } _ => {} }) .invoke_handler(tauri::generate_handler![start_tunnel, stop_tunnel, reload_tunnel, get_tunnel_status, get_metrics, get_config, save_config, get_wintun_install_path, set_autostart, get_autostart, list_running_processes]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }