mirror of https://github.com/ospab/ostp.git
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:
parent
c26e63250c
commit
e21e612e5c
File diff suppressed because it is too large
Load Diff
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
} 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.');
|
||||
setUIState('disconnected');
|
||||
}
|
||||
};
|
||||
setTimeout(check, 1500);
|
||||
function startGlobalPolling() {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
pollInterval = setInterval(uiSyncTick, 1000);
|
||||
uiSyncTick();
|
||||
}
|
||||
|
||||
async function fetchMetrics() {
|
||||
async function uiSyncTick() {
|
||||
try {
|
||||
const statusCode = await invoke('get_tunnel_status');
|
||||
|
||||
if (statusCode === 0) {
|
||||
setUIState('disconnected');
|
||||
return;
|
||||
} else if (statusCode === 1) {
|
||||
setUIState('connecting');
|
||||
} else if (statusCode === 2) {
|
||||
setUIState('connected');
|
||||
}
|
||||
|
||||
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);
|
||||
console.error('Sync error', e);
|
||||
setUIState('disconnected');
|
||||
}
|
||||
|
||||
try {
|
||||
const isAlive = await invoke('get_tunnel_status');
|
||||
if (!isAlive && appState === 'connected') {
|
||||
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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue