feat(gui): implement real-time atomic status polling and multi-state UI feedback (Stopped/Handshaking/Established) and update JNI/core layers

This commit is contained in:
ospab 2026-05-15 22:37:50 +03:00
parent c26e63250c
commit e21e612e5c
6 changed files with 4619 additions and 70 deletions

4598
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
use std::time::{Duration, SystemTime};
use std::sync::atomic::Ordering;
use portable_atomic::AtomicU64;
use portable_atomic::{AtomicU64, AtomicU8};
use std::sync::Arc;
use anyhow::{Context, Result};
@ -19,6 +19,7 @@ use crate::tunnel::{ProxyEvent, ProxyToClientMsg};
pub struct BridgeMetrics {
pub bytes_sent: AtomicU64,
pub bytes_recv: AtomicU64,
pub connection_state: AtomicU8,
}
async fn send_datagram(socket: &UdpSocket, frame: &Bytes, turn_enabled: bool) -> std::io::Result<usize> {
@ -123,6 +124,7 @@ impl Bridge {
_ = shutdown.changed() => {
if *shutdown.borrow() {
self.running = false;
self.metrics.connection_state.store(0, Ordering::Relaxed);
_proxy_guard = None;
break;
}
@ -132,6 +134,7 @@ impl Bridge {
Some(BridgeCommand::ToggleTunnel) => {
if self.running {
self.running = false;
self.metrics.connection_state.store(0, Ordering::Relaxed);
_proxy_guard = None;
sessions_opt = None;
udp_rx_opt = None;
@ -142,6 +145,7 @@ impl Bridge {
} else {
tx.send(UiEvent::Log("Handshaking started".to_string())).await.ok();
tx.send(UiEvent::Metrics { status: ConnectionStatus::Handshaking, rtt_ms: 0.0, throughput_bps: 0 }).await.ok();
self.metrics.connection_state.store(1, Ordering::Relaxed);
let session_count = if self.mux_enabled { self.mux_sessions.max(1) } else { 1 };
let (udp_tx, udp_rx) = mpsc::channel(10000);
@ -195,6 +199,7 @@ impl Bridge {
_proxy_guard = None;
tx.send(UiEvent::Log(format!("Handshake failed: {err}"))).await.ok();
tx.send(UiEvent::TunnelStopped).await.ok();
self.metrics.connection_state.store(0, Ordering::Relaxed);
continue;
}
@ -212,7 +217,7 @@ impl Bridge {
rtt_ms: self.last_rtt_ms,
throughput_bps: 0,
}).await.ok();
let start_msg = if self.mode == "tun" { "TUN Tunnel established" } else { "Bridge connection established" };
self.metrics.connection_state.store(2, Ordering::Relaxed); let start_msg = if self.mode == "tun" { "TUN Tunnel established" } else { "Bridge connection established" };
tx.send(UiEvent::Log(start_msg.to_string())).await.ok();
}
}
@ -228,6 +233,8 @@ impl Bridge {
tx.send(UiEvent::Log("Runtime config reloaded".to_string())).await.ok();
if self.running {
self.running = false;
self.metrics.connection_state.store(0, Ordering::Relaxed);
self.metrics.connection_state.store(0, Ordering::Relaxed);
_proxy_guard = None;
sessions_opt = None;
stream_map.clear();
@ -261,6 +268,7 @@ impl Bridge {
sessions_opt = None;
stream_map.clear();
let _ = tx.send(UiEvent::TunnelStopped).await;
self.metrics.connection_state.store(0, Ordering::Relaxed);
continue;
}
if let Some(sessions) = sessions_opt.as_mut() {
@ -314,6 +322,7 @@ impl Bridge {
udp_rx_opt = None;
stream_map.clear();
let _ = tx.send(UiEvent::TunnelStopped).await;
self.metrics.connection_state.store(0, Ordering::Relaxed);
}
}
}

View File

@ -109,6 +109,7 @@ pub async fn run_client(config: crate::config::ClientConfig) -> Result<()> {
let metrics = Arc::new(BridgeMetrics {
bytes_sent: portable_atomic::AtomicU64::new(0),
bytes_recv: portable_atomic::AtomicU64::new(0),
connection_state: portable_atomic::AtomicU8::new(0),
});
let (shutdown_tx, shutdown_rx) = watch::channel(false);

View File

@ -134,12 +134,18 @@ async fn save_config(json_content: String) -> Result<bool, String> {
}
#[tauri::command]
async fn get_tunnel_status(state: tauri::State<'_, AppState>) -> Result<bool, String> {
async fn get_tunnel_status(state: tauri::State<'_, AppState>) -> Result<u8, String> {
let guard = state.0.lock().await;
if let Some(ref handle) = guard.handle {
Ok(!handle.is_finished())
if handle.is_finished() {
return Ok(0);
}
if let Some(ref metrics) = guard.metrics {
return Ok(metrics.connection_state.load(Ordering::Relaxed));
}
Ok(0)
} else {
Ok(false)
Ok(0)
}
}
@ -231,6 +237,7 @@ async fn start_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String>
let metrics = Arc::new(BridgeMetrics {
bytes_sent: portable_atomic::AtomicU64::new(0),
bytes_recv: portable_atomic::AtomicU64::new(0),
connection_state: portable_atomic::AtomicU8::new(0),
});
let (shutdown_tx, shutdown_rx) = watch::channel(false);

View File

@ -59,6 +59,7 @@ function formatTime(seconds) {
// State Updates
function setUIState(state) {
if (appState === state) return;
appState = state;
// Clean up classes
@ -84,15 +85,16 @@ function setUIState(state) {
statusText.classList.add('status-connecting');
uptimeText.textContent = 'Establishing secure tunnel';
clearInterval(elapsedTimer);
elapsedTimer = null;
elapsedSeconds = 0;
} else if (state === 'connected') {
btnConnect.classList.add('connected');
powerContainer.classList.add('connected');
statusText.textContent = 'Protected';
statusText.classList.add('status-connected');
if (!pollInterval) {
pollInterval = setInterval(fetchMetrics, 1000);
}
if (!elapsedTimer) {
elapsedSeconds = 0;
elapsedTimer = setInterval(() => {
@ -110,7 +112,7 @@ async function handleToggleConnect() {
try {
const success = await invoke('start_tunnel');
if (success) {
monitorTunnelState();
startGlobalPolling();
} else {
alert('Failed to start tunnel process.');
setUIState('disconnected');
@ -129,45 +131,34 @@ async function handleToggleConnect() {
}
}
async function monitorTunnelState() {
let attempts = 0;
const check = async () => {
try {
const isAlive = await invoke('get_tunnel_status');
if (isAlive) {
setUIState('connected');
return true;
function startGlobalPolling() {
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(uiSyncTick, 1000);
uiSyncTick();
}
} catch (e) {}
attempts++;
if (attempts < 5 && appState === 'connecting') {
setTimeout(check, 1000);
} else if (appState === 'connecting') {
alert('Tunnel failed to stay alive. Check log files or Admin rights.');
async function uiSyncTick() {
try {
const statusCode = await invoke('get_tunnel_status');
if (statusCode === 0) {
setUIState('disconnected');
}
};
setTimeout(check, 1500);
return;
} else if (statusCode === 1) {
setUIState('connecting');
} else if (statusCode === 2) {
setUIState('connected');
}
async function fetchMetrics() {
try {
const stats = await invoke('get_metrics');
if (stats) {
metricDown.textContent = formatBytes(stats.bytes_recv);
metricUp.textContent = formatBytes(stats.bytes_sent);
}
} catch (e) {
console.error('Failed to fetch metrics', e);
}
try {
const isAlive = await invoke('get_tunnel_status');
if (!isAlive && appState === 'connected') {
console.error('Sync error', e);
setUIState('disconnected');
}
} catch (e) {}
}
function switchScreen(target) {
@ -320,9 +311,9 @@ window.addEventListener('DOMContentLoaded', async () => {
});
try {
const isAlive = await invoke('get_tunnel_status');
if (isAlive) {
setUIState('connected');
const statusCode = await invoke('get_tunnel_status');
if (statusCode > 0) {
startGlobalPolling();
} else {
setUIState('disconnected');
}

View File

@ -80,6 +80,7 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_startClient(
let metrics = Arc::new(BridgeMetrics {
bytes_sent: portable_atomic::AtomicU64::new(0),
bytes_recv: portable_atomic::AtomicU64::new(0),
connection_state: portable_atomic::AtomicU8::new(0),
});
let bridge = match Bridge::new(&config, Arc::clone(&metrics)) {