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::time::{Duration, SystemTime};
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use portable_atomic::AtomicU64;
|
use portable_atomic::{AtomicU64, AtomicU8};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
|
@ -19,6 +19,7 @@ use crate::tunnel::{ProxyEvent, ProxyToClientMsg};
|
||||||
pub struct BridgeMetrics {
|
pub struct BridgeMetrics {
|
||||||
pub bytes_sent: AtomicU64,
|
pub bytes_sent: AtomicU64,
|
||||||
pub bytes_recv: AtomicU64,
|
pub bytes_recv: AtomicU64,
|
||||||
|
pub connection_state: AtomicU8,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_datagram(socket: &UdpSocket, frame: &Bytes, turn_enabled: bool) -> std::io::Result<usize> {
|
async fn send_datagram(socket: &UdpSocket, frame: &Bytes, turn_enabled: bool) -> std::io::Result<usize> {
|
||||||
|
|
@ -123,6 +124,7 @@ impl Bridge {
|
||||||
_ = shutdown.changed() => {
|
_ = shutdown.changed() => {
|
||||||
if *shutdown.borrow() {
|
if *shutdown.borrow() {
|
||||||
self.running = false;
|
self.running = false;
|
||||||
|
self.metrics.connection_state.store(0, Ordering::Relaxed);
|
||||||
_proxy_guard = None;
|
_proxy_guard = None;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -132,6 +134,7 @@ impl Bridge {
|
||||||
Some(BridgeCommand::ToggleTunnel) => {
|
Some(BridgeCommand::ToggleTunnel) => {
|
||||||
if self.running {
|
if self.running {
|
||||||
self.running = false;
|
self.running = false;
|
||||||
|
self.metrics.connection_state.store(0, Ordering::Relaxed);
|
||||||
_proxy_guard = None;
|
_proxy_guard = None;
|
||||||
sessions_opt = None;
|
sessions_opt = None;
|
||||||
udp_rx_opt = None;
|
udp_rx_opt = None;
|
||||||
|
|
@ -142,6 +145,7 @@ impl Bridge {
|
||||||
} else {
|
} else {
|
||||||
tx.send(UiEvent::Log("Handshaking started".to_string())).await.ok();
|
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();
|
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 session_count = if self.mux_enabled { self.mux_sessions.max(1) } else { 1 };
|
||||||
let (udp_tx, udp_rx) = mpsc::channel(10000);
|
let (udp_tx, udp_rx) = mpsc::channel(10000);
|
||||||
|
|
@ -195,6 +199,7 @@ impl Bridge {
|
||||||
_proxy_guard = None;
|
_proxy_guard = None;
|
||||||
tx.send(UiEvent::Log(format!("Handshake failed: {err}"))).await.ok();
|
tx.send(UiEvent::Log(format!("Handshake failed: {err}"))).await.ok();
|
||||||
tx.send(UiEvent::TunnelStopped).await.ok();
|
tx.send(UiEvent::TunnelStopped).await.ok();
|
||||||
|
self.metrics.connection_state.store(0, Ordering::Relaxed);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,7 +217,7 @@ impl Bridge {
|
||||||
rtt_ms: self.last_rtt_ms,
|
rtt_ms: self.last_rtt_ms,
|
||||||
throughput_bps: 0,
|
throughput_bps: 0,
|
||||||
}).await.ok();
|
}).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();
|
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();
|
tx.send(UiEvent::Log("Runtime config reloaded".to_string())).await.ok();
|
||||||
if self.running {
|
if self.running {
|
||||||
self.running = false;
|
self.running = false;
|
||||||
|
self.metrics.connection_state.store(0, Ordering::Relaxed);
|
||||||
|
self.metrics.connection_state.store(0, Ordering::Relaxed);
|
||||||
_proxy_guard = None;
|
_proxy_guard = None;
|
||||||
sessions_opt = None;
|
sessions_opt = None;
|
||||||
stream_map.clear();
|
stream_map.clear();
|
||||||
|
|
@ -261,6 +268,7 @@ impl Bridge {
|
||||||
sessions_opt = None;
|
sessions_opt = None;
|
||||||
stream_map.clear();
|
stream_map.clear();
|
||||||
let _ = tx.send(UiEvent::TunnelStopped).await;
|
let _ = tx.send(UiEvent::TunnelStopped).await;
|
||||||
|
self.metrics.connection_state.store(0, Ordering::Relaxed);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(sessions) = sessions_opt.as_mut() {
|
if let Some(sessions) = sessions_opt.as_mut() {
|
||||||
|
|
@ -314,6 +322,7 @@ impl Bridge {
|
||||||
udp_rx_opt = None;
|
udp_rx_opt = None;
|
||||||
stream_map.clear();
|
stream_map.clear();
|
||||||
let _ = tx.send(UiEvent::TunnelStopped).await;
|
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 {
|
let metrics = Arc::new(BridgeMetrics {
|
||||||
bytes_sent: portable_atomic::AtomicU64::new(0),
|
bytes_sent: portable_atomic::AtomicU64::new(0),
|
||||||
bytes_recv: 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);
|
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
||||||
|
|
|
||||||
|
|
@ -134,12 +134,18 @@ async fn save_config(json_content: String) -> Result<bool, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[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;
|
let guard = state.0.lock().await;
|
||||||
if let Some(ref handle) = guard.handle {
|
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 {
|
} 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 {
|
let metrics = Arc::new(BridgeMetrics {
|
||||||
bytes_sent: portable_atomic::AtomicU64::new(0),
|
bytes_sent: portable_atomic::AtomicU64::new(0),
|
||||||
bytes_recv: 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);
|
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ function formatTime(seconds) {
|
||||||
|
|
||||||
// State Updates
|
// State Updates
|
||||||
function setUIState(state) {
|
function setUIState(state) {
|
||||||
|
if (appState === state) return;
|
||||||
appState = state;
|
appState = state;
|
||||||
|
|
||||||
// Clean up classes
|
// Clean up classes
|
||||||
|
|
@ -84,15 +85,16 @@ function setUIState(state) {
|
||||||
statusText.classList.add('status-connecting');
|
statusText.classList.add('status-connecting');
|
||||||
uptimeText.textContent = 'Establishing secure tunnel';
|
uptimeText.textContent = 'Establishing secure tunnel';
|
||||||
|
|
||||||
|
clearInterval(elapsedTimer);
|
||||||
|
elapsedTimer = null;
|
||||||
|
elapsedSeconds = 0;
|
||||||
|
|
||||||
} else if (state === 'connected') {
|
} else if (state === 'connected') {
|
||||||
btnConnect.classList.add('connected');
|
btnConnect.classList.add('connected');
|
||||||
powerContainer.classList.add('connected');
|
powerContainer.classList.add('connected');
|
||||||
statusText.textContent = 'Protected';
|
statusText.textContent = 'Protected';
|
||||||
statusText.classList.add('status-connected');
|
statusText.classList.add('status-connected');
|
||||||
|
|
||||||
if (!pollInterval) {
|
|
||||||
pollInterval = setInterval(fetchMetrics, 1000);
|
|
||||||
}
|
|
||||||
if (!elapsedTimer) {
|
if (!elapsedTimer) {
|
||||||
elapsedSeconds = 0;
|
elapsedSeconds = 0;
|
||||||
elapsedTimer = setInterval(() => {
|
elapsedTimer = setInterval(() => {
|
||||||
|
|
@ -110,7 +112,7 @@ async function handleToggleConnect() {
|
||||||
try {
|
try {
|
||||||
const success = await invoke('start_tunnel');
|
const success = await invoke('start_tunnel');
|
||||||
if (success) {
|
if (success) {
|
||||||
monitorTunnelState();
|
startGlobalPolling();
|
||||||
} else {
|
} else {
|
||||||
alert('Failed to start tunnel process.');
|
alert('Failed to start tunnel process.');
|
||||||
setUIState('disconnected');
|
setUIState('disconnected');
|
||||||
|
|
@ -129,45 +131,34 @@ async function handleToggleConnect() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function monitorTunnelState() {
|
function startGlobalPolling() {
|
||||||
let attempts = 0;
|
if (pollInterval) clearInterval(pollInterval);
|
||||||
const check = async () => {
|
pollInterval = setInterval(uiSyncTick, 1000);
|
||||||
try {
|
uiSyncTick();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchMetrics() {
|
async function uiSyncTick() {
|
||||||
try {
|
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');
|
const stats = await invoke('get_metrics');
|
||||||
if (stats) {
|
if (stats) {
|
||||||
metricDown.textContent = formatBytes(stats.bytes_recv);
|
metricDown.textContent = formatBytes(stats.bytes_recv);
|
||||||
metricUp.textContent = formatBytes(stats.bytes_sent);
|
metricUp.textContent = formatBytes(stats.bytes_sent);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to fetch metrics', e);
|
console.error('Sync error', e);
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const isAlive = await invoke('get_tunnel_status');
|
|
||||||
if (!isAlive && appState === 'connected') {
|
|
||||||
setUIState('disconnected');
|
setUIState('disconnected');
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchScreen(target) {
|
function switchScreen(target) {
|
||||||
|
|
@ -320,9 +311,9 @@ window.addEventListener('DOMContentLoaded', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isAlive = await invoke('get_tunnel_status');
|
const statusCode = await invoke('get_tunnel_status');
|
||||||
if (isAlive) {
|
if (statusCode > 0) {
|
||||||
setUIState('connected');
|
startGlobalPolling();
|
||||||
} else {
|
} else {
|
||||||
setUIState('disconnected');
|
setUIState('disconnected');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_startClient(
|
||||||
let metrics = Arc::new(BridgeMetrics {
|
let metrics = Arc::new(BridgeMetrics {
|
||||||
bytes_sent: portable_atomic::AtomicU64::new(0),
|
bytes_sent: portable_atomic::AtomicU64::new(0),
|
||||||
bytes_recv: 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)) {
|
let bridge = match Bridge::new(&config, Arc::clone(&metrics)) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue