mirror of https://github.com/ospab/ostp.git
feat(gui): privileged TUN helper architecture - GUI runs unprivileged, UAC prompt shown only for TUN mode via ostp-tun-helper.exe IPC
This commit is contained in:
parent
5d9034ca1e
commit
b0491e14e3
|
|
@ -2687,6 +2687,19 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ostp-tun-helper"
|
||||
version = "0.1.43"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ostp-client",
|
||||
"portable-atomic",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"winres",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
|
|
@ -4169,6 +4182,15 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.2"
|
||||
|
|
@ -5204,6 +5226,15 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winres"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
|
||||
dependencies = [
|
||||
"toml 0.5.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ members = [
|
|||
"ostp-client",
|
||||
"ostp-server",
|
||||
"ostp-jni", "ostp",
|
||||
"ostp-gui/src-tauri"
|
||||
"ostp-gui/src-tauri",
|
||||
"ostp-tun-helper"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,232 @@
|
|||
{
|
||||
"name": "ostp-gui",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ostp-gui",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.1.tgz",
|
||||
"integrity": "sha512-rpEbaJ/HzNb6fwsquwoAbq29/Vt4gADhS423A8fdkwL4edJ0wZmoB8ar7O6JPDL834MUKOCm/rrJ7c9oAaEaYQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
"tauri": "tauri.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.11.1",
|
||||
"@tauri-apps/cli-darwin-x64": "2.11.1",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.11.1",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.11.1",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.11.1",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.11.1",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.11.1",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.11.1",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.11.1",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.11.1",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.1.tgz",
|
||||
"integrity": "sha512-6eEKMBXsQPCuM1EmvrjT2+aBuxWQuFdKdW8pzNuNQtpq45nEEpBlD5gr8pUeAyOU1DQKlkFaEc/MPBxb/Pfjtg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.1.tgz",
|
||||
"integrity": "sha512-LQUO7exfRWjWALNhetph5guWpMeHphRpokOLk0OIbTTExaNwJNFu3I4vb+CCM/4G/QGoZe/5XikZOJdNEFP1ig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.1.tgz",
|
||||
"integrity": "sha512-5i/awiBCRRhOUG8yjn0fMHXIWD5Ez8eEk5LtvOxyQrKuJkRaZDvnbIjZbE183blAwkoA4xN3aO/prJiqscl02Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.1.tgz",
|
||||
"integrity": "sha512-9LrwDw3S9Fygtw/Q6WDhOP+3svJRGAsejeE+GKrc0eO1ThMVhwi2LL6hw4dlKw93IfS7VY1G19sWGxJ/NcU4nA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.1.tgz",
|
||||
"integrity": "sha512-mNA5dbbqPqDUdTIwdUYYuhO2GvIe9UnB2r0VU2njxBOS3Opbx4gKNC5yP0Iu4rYmEmqdlwry9VzGZQ3wq9dyFg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.1.tgz",
|
||||
"integrity": "sha512-fZj3Gwq+6fUs305T5WQiD5iSGJw+j/4w/HGmk4sHDAcy+rp9zU5eaxB7nOyz5/I/nkNAuKPqfp6uIbiUBXkBCw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.1.tgz",
|
||||
"integrity": "sha512-XFxGxOvHM7jjeD6ozCKdGfhzJ7lERYDGZl1/Kb4fsvchaJsfLJ981TlyTG8Qy/gFq+f5GitH3bfrX9JAkjPEyw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.1.tgz",
|
||||
"integrity": "sha512-d5C2/Zm+68v7R9wTuTCjRQEVrWjcdMkJBZ1+rXse+QdMMlTB9+u9PDNDLw9PQflWxYLaYZ7tjxxL9Nb9II6PbA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.1.tgz",
|
||||
"integrity": "sha512-YdeVWFAR1pTXzUU6NLstPq4G6OLxuDrXCXEBdmBH+5EZIDXUx0D2kJlz3+YjpazkKvAzYpgziTsyRagls0OfRQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.1.tgz",
|
||||
"integrity": "sha512-VBGkuH0eB9K9LLSMv361Gzr5Ou72sCS4+ztpmkWEQ+wd/amhcYOsf3X6qn1RJZDzIhiOYHJEOysZUC3baD01rA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.1.tgz",
|
||||
"integrity": "sha512-b3ORhIAKgp9ZYY+zBt7b7r0kLU2kjvyGF0+MS2SBym3emsweGPybEqocJcmtMuxyBhkOKHP4CiuEJEDuAlTx6A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +1,3 @@
|
|||
fn main() {
|
||||
let mut windows = tauri_build::WindowsAttributes::new();
|
||||
|
||||
// Define the manifest with requireAdministrator to allow TUN mode without terminal
|
||||
// and include Common-Controls v6 for modern UI elements/dialogs.
|
||||
let manifest = r#"
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*" />
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
</assembly>
|
||||
"#;
|
||||
|
||||
windows = windows.app_manifest(manifest);
|
||||
|
||||
tauri_build::try_build(
|
||||
tauri_build::Attributes::new()
|
||||
.windows_attributes(windows)
|
||||
)
|
||||
.expect("failed to run build script");
|
||||
tauri_build::build()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@ use anyhow::Result;
|
|||
use ostp_client::bridge::BridgeMetrics;
|
||||
use portable_atomic::Ordering;
|
||||
|
||||
// Config deserialization matching ostp core
|
||||
// ── Config types ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
#[serde(tag = "mode", rename_all = "lowercase")]
|
||||
enum AppMode {
|
||||
Server(serde_json::Value), // We ignore server config in GUI
|
||||
Server(serde_json::Value),
|
||||
Client(ClientConfigRaw),
|
||||
}
|
||||
|
||||
|
|
@ -69,26 +70,58 @@ struct UIMetrics {
|
|||
bytes_recv: u64,
|
||||
}
|
||||
|
||||
struct AppStateInner {
|
||||
// ── 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 },
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
// ── Application state ─────────────────────────────────────────────────────────
|
||||
|
||||
// For proxy (non-TUN) mode: runs in-process.
|
||||
struct InProcessState {
|
||||
shutdown_tx: Option<watch::Sender<bool>>,
|
||||
metrics: Option<Arc<BridgeMetrics>>,
|
||||
handle: Option<JoinHandle<Result<(), String>>>,
|
||||
metrics: Arc<BridgeMetrics>,
|
||||
handle: JoinHandle<Result<(), String>>,
|
||||
}
|
||||
|
||||
// For TUN mode: communicates with the privileged helper via named pipe.
|
||||
struct HelperState {
|
||||
/// Shared state updated by pipe reader task
|
||||
pipe_state: Arc<Mutex<HelperPipeState>>,
|
||||
/// Send commands to helper over named pipe
|
||||
cmd_tx: tokio::sync::mpsc::Sender<String>,
|
||||
}
|
||||
|
||||
enum TunnelHandle {
|
||||
InProcess(InProcessState),
|
||||
Helper(HelperState),
|
||||
}
|
||||
|
||||
struct AppStateInner {
|
||||
tunnel: Option<TunnelHandle>,
|
||||
}
|
||||
|
||||
impl Drop for AppStateInner {
|
||||
fn drop(&mut self) {
|
||||
// Send final signal to ensure the core background threads exit immediately
|
||||
// and activate Wintun routing cleanup Drop routines.
|
||||
if let Some(tx) = self.shutdown_tx.take() {
|
||||
let _ = tx.send(true);
|
||||
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<AppStateInner>);
|
||||
|
||||
// ── Config helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
fn get_config_path() -> PathBuf {
|
||||
// Standard behavior: same dir as current exe, or fall back to current working dir
|
||||
if let Ok(exe_path) = std::env::current_exe() {
|
||||
if let Some(parent) = exe_path.parent() {
|
||||
let path = parent.join("config.json");
|
||||
|
|
@ -100,11 +133,12 @@ fn get_config_path() -> PathBuf {
|
|||
PathBuf::from("config.json")
|
||||
}
|
||||
|
||||
// ── Tauri commands ────────────────────────────────────────────────────────────
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_config() -> Result<String, String> {
|
||||
let path = get_config_path();
|
||||
if !path.exists() {
|
||||
// Return default template if file missing
|
||||
return Ok(r#"{
|
||||
"mode": "client",
|
||||
"log_level": "info",
|
||||
|
|
@ -124,10 +158,8 @@ async fn get_config() -> Result<String, String> {
|
|||
|
||||
#[tauri::command]
|
||||
async fn save_config(json_content: String) -> Result<bool, String> {
|
||||
// Validate formatting
|
||||
let _parsed: UnifiedConfig = serde_json::from_str(&json_content)
|
||||
.map_err(|e| format!("Invalid OSTP config JSON: {}", e))?;
|
||||
|
||||
let path = get_config_path();
|
||||
std::fs::write(path, json_content).map_err(|e| format!("Write error: {}", e))?;
|
||||
Ok(true)
|
||||
|
|
@ -136,74 +168,105 @@ async fn save_config(json_content: String) -> Result<bool, String> {
|
|||
#[tauri::command]
|
||||
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 {
|
||||
if handle.is_finished() {
|
||||
return Ok(0);
|
||||
match &guard.tunnel {
|
||||
None => Ok(0),
|
||||
Some(TunnelHandle::InProcess(s)) => {
|
||||
if s.handle.is_finished() {
|
||||
return Ok(0);
|
||||
}
|
||||
Ok(s.metrics.connection_state.load(Ordering::Relaxed))
|
||||
}
|
||||
if let Some(ref metrics) = guard.metrics {
|
||||
return Ok(metrics.connection_state.load(Ordering::Relaxed));
|
||||
Some(TunnelHandle::Helper(h)) => {
|
||||
let ps = h.pipe_state.lock().await;
|
||||
Ok(ps.connection_state)
|
||||
}
|
||||
Ok(0)
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_metrics(state: tauri::State<'_, AppState>) -> Result<Option<UIMetrics>, String> {
|
||||
let guard = state.0.lock().await;
|
||||
if let Some(ref metrics) = guard.metrics {
|
||||
Ok(Some(UIMetrics {
|
||||
bytes_sent: metrics.bytes_sent.load(Ordering::Relaxed),
|
||||
bytes_recv: metrics.bytes_recv.load(Ordering::Relaxed),
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
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),
|
||||
})),
|
||||
Some(TunnelHandle::Helper(h)) => {
|
||||
let ps = h.pipe_state.lock().await;
|
||||
Ok(Some(UIMetrics {
|
||||
bytes_sent: ps.bytes_sent,
|
||||
bytes_recv: ps.bytes_recv,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn stop_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String> {
|
||||
let mut guard = state.0.lock().await;
|
||||
if let Some(tx) = guard.shutdown_tx.take() {
|
||||
let _ = tx.send(true);
|
||||
match guard.tunnel.take() {
|
||||
None => {}
|
||||
Some(TunnelHandle::InProcess(mut s)) => {
|
||||
if let Some(tx) = s.shutdown_tx.take() {
|
||||
let _ = tx.send(true);
|
||||
}
|
||||
drop(s.handle);
|
||||
}
|
||||
Some(TunnelHandle::Helper(h)) => {
|
||||
let _ = h.cmd_tx.send("{\"cmd\":\"stop\"}\n".to_string()).await;
|
||||
}
|
||||
}
|
||||
if let Some(handle) = guard.handle.take() {
|
||||
let _ = handle.await;
|
||||
}
|
||||
guard.metrics = None;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn start_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String> {
|
||||
// Ensure it's stopped first
|
||||
let mut guard = state.0.lock().await;
|
||||
if let Some(ref h) = guard.handle {
|
||||
if !h.is_finished() {
|
||||
return Ok(true); // Already running
|
||||
}
|
||||
|
||||
// Already running?
|
||||
match &guard.tunnel {
|
||||
Some(TunnelHandle::InProcess(s)) if !s.handle.is_finished() => return Ok(true),
|
||||
Some(TunnelHandle::Helper(_)) => return Ok(true),
|
||||
_ => {}
|
||||
}
|
||||
// Clean up finished handle
|
||||
guard.tunnel = None;
|
||||
|
||||
let path = get_config_path();
|
||||
if !path.exists() {
|
||||
return Err("config.json not found. Go to Settings and configure your key first.".into());
|
||||
return Err("config.json not found. Go to Settings and configure your connection first.".into());
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
||||
let unified: UnifiedConfig = serde_json::from_str(&content).map_err(|e| format!("Config parse error: {}", e))?;
|
||||
let unified: UnifiedConfig = serde_json::from_str(&content)
|
||||
.map_err(|e| format!("Config parse error: {}", e))?;
|
||||
|
||||
let client_cfg = match unified.mode {
|
||||
AppMode::Client(c) => c,
|
||||
AppMode::Server(_) => return Err("Configuration is in Server mode. GUI only supports Client configurations.".into()),
|
||||
AppMode::Server(_) => return Err("Configuration is in Server mode. GUI only supports Client mode.".into()),
|
||||
};
|
||||
|
||||
// Translate to ostp_client domain struct
|
||||
let is_tun_enabled = client_cfg.tun.as_ref().map(|t| t.enable).unwrap_or(false);
|
||||
let turn_cfg = client_cfg.turn.as_ref();
|
||||
|
||||
let mapped_config = ostp_client::config::ClientConfig {
|
||||
mode: if is_tun_enabled { "tun".to_string() } else { "proxy".to_string() },
|
||||
if is_tun_enabled {
|
||||
// ── TUN mode: launch privileged helper ────────────────────────────────
|
||||
start_tun_via_helper(&mut guard, client_cfg, content).await
|
||||
} else {
|
||||
// ── Proxy mode: run in-process ────────────────────────────────────────
|
||||
start_proxy_in_process(&mut guard, client_cfg).await
|
||||
}
|
||||
}
|
||||
|
||||
// ── In-process proxy tunnel ──────────────────────────────────────────────────
|
||||
|
||||
async fn start_proxy_in_process(
|
||||
guard: &mut AppStateInner,
|
||||
client_cfg: ClientConfigRaw,
|
||||
) -> Result<bool, String> {
|
||||
let turn_cfg = client_cfg.turn.as_ref();
|
||||
let mapped = ostp_client::config::ClientConfig {
|
||||
mode: "proxy".to_string(),
|
||||
debug: client_cfg.debug.unwrap_or(false),
|
||||
ostp: ostp_client::config::OstpConfig {
|
||||
server_addr: client_cfg.server.clone(),
|
||||
|
|
@ -231,14 +294,9 @@ async fn start_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String>
|
|||
enabled: client_cfg.mux.as_ref().and_then(|m| m.enabled).unwrap_or(false),
|
||||
sessions: client_cfg.mux.as_ref().and_then(|m| m.sessions).unwrap_or(1),
|
||||
},
|
||||
dns_server: client_cfg.tun.as_ref().and_then(|t| t.dns.clone()),
|
||||
dns_server: None,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if mapped_config.mode == "tun" && !ostp_client::runner::is_admin() {
|
||||
return Err("Administrator privileges are required to initialize TUN mode. Please run the application as Administrator.".to_string());
|
||||
}
|
||||
|
||||
let metrics = Arc::new(BridgeMetrics {
|
||||
bytes_sent: portable_atomic::AtomicU64::new(0),
|
||||
bytes_recv: portable_atomic::AtomicU64::new(0),
|
||||
|
|
@ -246,44 +304,209 @@ async fn start_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String>
|
|||
});
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
||||
|
||||
let metrics_clone = metrics.clone();
|
||||
let engine_handle = tokio::spawn(async move {
|
||||
match ostp_client::runner::run_client_core(mapped_config, metrics_clone, shutdown_rx).await {
|
||||
let handle = tokio::spawn(async move {
|
||||
match ostp_client::runner::run_client_core(mapped, metrics_clone, shutdown_rx).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
});
|
||||
|
||||
guard.shutdown_tx = Some(shutdown_tx);
|
||||
guard.metrics = Some(metrics);
|
||||
guard.handle = Some(engine_handle);
|
||||
guard.tunnel = Some(TunnelHandle::InProcess(InProcessState {
|
||||
shutdown_tx: Some(shutdown_tx),
|
||||
metrics,
|
||||
handle,
|
||||
}));
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
// ── Privileged TUN helper via named pipe ─────────────────────────────────────
|
||||
|
||||
const PIPE_NAME: &str = r"\\.\pipe\ostp-tun-helper";
|
||||
|
||||
async fn start_tun_via_helper(
|
||||
guard: &mut AppStateInner,
|
||||
_client_cfg: ClientConfigRaw,
|
||||
raw_config_json: String,
|
||||
) -> Result<bool, String> {
|
||||
// Find the helper binary next to our exe
|
||||
let helper_exe = find_helper_exe().ok_or_else(|| {
|
||||
"ostp-tun-helper.exe not found next to the application. Please reinstall.".to_string()
|
||||
})?;
|
||||
|
||||
// Launch with UAC elevation via ShellExecuteW("runas")
|
||||
launch_as_admin(&helper_exe).map_err(|e| format!("Failed to launch helper: {}", e))?;
|
||||
|
||||
// Give the helper time to start and create the pipe
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
|
||||
|
||||
// Connect to the helper's named pipe
|
||||
let pipe = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
async {
|
||||
loop {
|
||||
match tokio::net::windows::named_pipe::ClientOptions::new().open(PIPE_NAME) {
|
||||
Ok(p) => return Ok::<_, std::io::Error>(p),
|
||||
Err(_) => tokio::time::sleep(std::time::Duration::from_millis(200)).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
).await.map_err(|_| "Timed out connecting to TUN helper. It may have been denied by UAC.".to_string())?
|
||||
.map_err(|e| format!("Pipe connection error: {}", e))?;
|
||||
|
||||
// Build the config JSON and send start command
|
||||
let mut mapped_config = serde_json::from_str::<serde_json::Value>(&raw_config_json)
|
||||
.map_err(|e| e.to_string())?;
|
||||
// Ensure mode is set
|
||||
if let Some(obj) = mapped_config.as_object_mut() {
|
||||
obj.insert("mode".to_string(), serde_json::Value::String("tun".to_string()));
|
||||
}
|
||||
let start_cmd = serde_json::json!({
|
||||
"cmd": "start",
|
||||
"config": serde_json::to_string(&mapped_config).unwrap_or_default()
|
||||
}).to_string();
|
||||
|
||||
// Set up channel for sending commands to helper task
|
||||
let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::channel::<String>(16);
|
||||
|
||||
// Spawn a task that manages the pipe I/O
|
||||
let pipe_state: Arc<Mutex<HelperPipeState>> = Arc::new(Mutex::new(HelperPipeState {
|
||||
connection_state: 1,
|
||||
bytes_sent: 0,
|
||||
bytes_recv: 0,
|
||||
}));
|
||||
let state_for_task = pipe_state.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::io::split;
|
||||
|
||||
let (reader_half, mut writer_half) = split(pipe);
|
||||
let mut reader = BufReader::new(reader_half);
|
||||
|
||||
// Send the start command
|
||||
let _ = writer_half.write_all(format!("{}\n", start_cmd).as_bytes()).await;
|
||||
|
||||
// Concurrently: read from pipe, write commands from channel
|
||||
let mut line = String::new();
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = reader.read_line(&mut line) => {
|
||||
let n = result.unwrap_or(0);
|
||||
if n == 0 { break; } // Helper disconnected
|
||||
let trimmed = line.trim().to_string();
|
||||
line.clear();
|
||||
if trimmed.is_empty() { continue; }
|
||||
if let Ok(msg) = serde_json::from_str::<HelperMsg>(&trimmed) {
|
||||
let mut s = state_for_task.lock().await;
|
||||
match msg {
|
||||
HelperMsg::Status { value } => s.connection_state = value,
|
||||
HelperMsg::Metrics { bytes_sent, bytes_recv } => {
|
||||
s.bytes_sent = bytes_sent;
|
||||
s.bytes_recv = bytes_recv;
|
||||
}
|
||||
HelperMsg::Error { .. } => s.connection_state = 0,
|
||||
HelperMsg::Log { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd = cmd_rx.recv() => {
|
||||
match cmd {
|
||||
Some(c) => { let _ = writer_half.write_all(c.as_bytes()).await; }
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Mark stopped
|
||||
let mut s = state_for_task.lock().await;
|
||||
s.connection_state = 0;
|
||||
});
|
||||
|
||||
guard.tunnel = Some(TunnelHandle::Helper(HelperState {
|
||||
pipe_state,
|
||||
cmd_tx,
|
||||
}));
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn apply_webview_loopback_exemption() {
|
||||
use std::os::windows::process::CommandExt;
|
||||
if ostp_client::runner::is_admin() {
|
||||
// Silently whitelist the standard WebView2 sandbox to communicate with elevated localhost/dev server
|
||||
let _ = std::process::Command::new("CheckNetIsolation.exe")
|
||||
.args(["LoopbackExempt", "-a", "-n=Microsoft.Win32WebView2Sandbox_cw5n1h2txyewy"])
|
||||
.creation_flags(0x08000000)
|
||||
.output();
|
||||
}
|
||||
struct HelperPipeState {
|
||||
connection_state: u8,
|
||||
bytes_sent: u64,
|
||||
bytes_recv: u64,
|
||||
}
|
||||
|
||||
fn find_helper_exe() -> Option<PathBuf> {
|
||||
// First look next to current exe
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
let candidate = dir.join("ostp-tun-helper.exe");
|
||||
if candidate.exists() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Dev: look in target/debug next to workspace root
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(dir) = exe.parent() {
|
||||
let candidate = dir.join("ostp-tun-helper.exe");
|
||||
if candidate.exists() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn launch_as_admin(exe: &PathBuf) -> Result<()> {
|
||||
use std::ffi::OsStr;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::ptr::null_mut;
|
||||
|
||||
let exe_wstr: Vec<u16> = exe.as_os_str().encode_wide().chain(Some(0)).collect();
|
||||
let verb_wstr: Vec<u16> = OsStr::new("runas").encode_wide().chain(Some(0)).collect();
|
||||
|
||||
#[link(name = "shell32")]
|
||||
extern "system" {
|
||||
fn ShellExecuteW(
|
||||
hwnd: *mut std::ffi::c_void,
|
||||
lpOperation: *const u16,
|
||||
lpFile: *const u16,
|
||||
lpParameters: *const u16,
|
||||
lpDirectory: *const u16,
|
||||
nShowCmd: i32,
|
||||
) -> isize;
|
||||
}
|
||||
|
||||
let ret = unsafe {
|
||||
ShellExecuteW(
|
||||
null_mut(),
|
||||
verb_wstr.as_ptr(),
|
||||
exe_wstr.as_ptr(),
|
||||
null_mut(),
|
||||
null_mut(),
|
||||
0, // SW_HIDE
|
||||
)
|
||||
};
|
||||
|
||||
if ret <= 32 {
|
||||
anyhow::bail!("ShellExecuteW failed (code {}). UAC was denied or helper not found.", ret);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn launch_as_admin(_exe: &PathBuf) -> Result<()> {
|
||||
anyhow::bail!("TUN mode via helper is only supported on Windows");
|
||||
}
|
||||
|
||||
// ── Tauri setup ───────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
#[cfg(target_os = "windows")]
|
||||
apply_webview_loopback_exemption();
|
||||
|
||||
let state = AppState(Mutex::new(AppStateInner {
|
||||
shutdown_tx: None,
|
||||
metrics: None,
|
||||
handle: None,
|
||||
}));
|
||||
let state = AppState(Mutex::new(AppStateInner { tunnel: None }));
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "ostp-tun-helper"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "ostp-tun-helper"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
ostp-client = { path = "../ostp-client" }
|
||||
tokio = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
portable-atomic = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
# no extra build deps needed; manifest is embedded via build.rs
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winres = "0.1"
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// build.rs for ostp-tun-helper
|
||||
// Embeds a Windows manifest that requests Administrator privileges.
|
||||
// This makes Windows show a UAC prompt when the binary is double-clicked
|
||||
// or launched via ShellExecuteW("runas").
|
||||
|
||||
fn main() {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut res = winres::WindowsResource::new();
|
||||
res.set_manifest(r#"
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0" processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
</assembly>
|
||||
"#);
|
||||
res.compile().expect("failed to compile Windows resources");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
// ostp-tun-helper/src/main.rs
|
||||
//
|
||||
// Privileged helper for TUN mode. Runs with Administrator rights.
|
||||
// Communicates with ostp-gui via a named pipe IPC channel.
|
||||
//
|
||||
// Protocol over the named pipe (newline-delimited JSON):
|
||||
// GUI -> Helper: {"cmd":"start","config":<config json string>}
|
||||
// GUI -> Helper: {"cmd":"stop"}
|
||||
// Helper -> GUI: {"type":"status","value":0|1|2} (0=stopped,1=connecting,2=connected)
|
||||
// Helper -> GUI: {"type":"log","message":"..."}
|
||||
// Helper -> GUI: {"type":"metrics","bytes_sent":N,"bytes_recv":N}
|
||||
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::sync::{watch, Mutex};
|
||||
use portable_atomic::Ordering;
|
||||
|
||||
const PIPE_NAME: &str = r"\\.\pipe\ostp-tun-helper";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "cmd", rename_all = "lowercase")]
|
||||
enum GuiCmd {
|
||||
Start { config: String },
|
||||
Stop,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
enum HelperMsg {
|
||||
Status { value: u8 },
|
||||
Log { message: String },
|
||||
Metrics { bytes_sent: u64, bytes_recv: u64 },
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
struct TunnelState {
|
||||
shutdown_tx: Option<watch::Sender<bool>>,
|
||||
metrics: Option<Arc<ostp_client::bridge::BridgeMetrics>>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// The helper is always launched by the GUI. If no client connects within
|
||||
// 60 seconds, exit to avoid lingering admin processes.
|
||||
run_pipe_server().await
|
||||
}
|
||||
|
||||
async fn run_pipe_server() -> Result<()> {
|
||||
use tokio::net::windows::named_pipe::{ServerOptions};
|
||||
|
||||
let state = Arc::new(Mutex::new(TunnelState {
|
||||
shutdown_tx: None,
|
||||
metrics: None,
|
||||
}));
|
||||
|
||||
// Create the named pipe server
|
||||
let server = ServerOptions::new()
|
||||
.first_pipe_instance(true)
|
||||
.create(PIPE_NAME)?;
|
||||
|
||||
// Wait for GUI to connect (60 second timeout)
|
||||
let connect_timeout = tokio::time::timeout(
|
||||
Duration::from_secs(60),
|
||||
server.connect()
|
||||
).await;
|
||||
|
||||
let pipe = match connect_timeout {
|
||||
Ok(Ok(())) => server,
|
||||
_ => {
|
||||
// No client connected — exit silently
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let (reader_half, writer_half) = tokio::io::split(pipe);
|
||||
let writer = Arc::new(Mutex::new(writer_half));
|
||||
let mut reader = BufReader::new(reader_half);
|
||||
|
||||
// Helper to send a message back to GUI
|
||||
let send_msg = {
|
||||
let writer = writer.clone();
|
||||
move |msg: HelperMsg| {
|
||||
let writer = writer.clone();
|
||||
let json = serde_json::to_string(&msg).unwrap_or_default();
|
||||
tokio::spawn(async move {
|
||||
let mut w = writer.lock().await;
|
||||
let _ = w.write_all(format!("{}\n", json).as_bytes()).await;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Read commands from GUI
|
||||
let mut line = String::new();
|
||||
loop {
|
||||
line.clear();
|
||||
let n = reader.read_line(&mut line).await.unwrap_or(0);
|
||||
if n == 0 {
|
||||
// GUI disconnected — stop tunnel and exit
|
||||
let mut st = state.lock().await;
|
||||
if let Some(tx) = st.shutdown_tx.take() {
|
||||
let _ = tx.send(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cmd: GuiCmd = match serde_json::from_str(trimmed) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
send_msg(HelperMsg::Error { message: format!("Bad command: {}", e) });
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match cmd {
|
||||
GuiCmd::Start { config } => {
|
||||
// Stop any existing tunnel first
|
||||
{
|
||||
let mut st = state.lock().await;
|
||||
if let Some(tx) = st.shutdown_tx.take() {
|
||||
let _ = tx.send(true);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
// Parse config
|
||||
let cfg: ostp_client::config::ClientConfig = match serde_json::from_str(&config) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
send_msg(HelperMsg::Error { message: format!("Config parse error: {}", e) });
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let metrics = Arc::new(ostp_client::bridge::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);
|
||||
|
||||
{
|
||||
let mut st = state.lock().await;
|
||||
st.shutdown_tx = Some(shutdown_tx);
|
||||
st.metrics = Some(metrics.clone());
|
||||
}
|
||||
|
||||
// Spawn the tunnel
|
||||
let metrics_for_runner = metrics.clone();
|
||||
let send_log = {
|
||||
let writer = writer.clone();
|
||||
move |msg: String| {
|
||||
let writer = writer.clone();
|
||||
let json = serde_json::to_string(&HelperMsg::Log { message: msg }).unwrap_or_default();
|
||||
tokio::spawn(async move {
|
||||
let mut w = writer.lock().await;
|
||||
let _ = w.write_all(format!("{}\n", json).as_bytes()).await;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let writer_for_tick = writer.clone();
|
||||
let metrics_for_tick = metrics.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match ostp_client::runner::run_client_core(cfg, metrics_for_runner, shutdown_rx).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let json = serde_json::to_string(&HelperMsg::Error { message: e.to_string() }).unwrap_or_default();
|
||||
let mut w = writer_for_tick.lock().await;
|
||||
let _ = w.write_all(format!("{}\n", json).as_bytes()).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn a tick that forwards status + metrics to GUI every second
|
||||
let writer_tick = writer.clone();
|
||||
let metrics_tick = metrics_for_tick.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut last_state = 99u8;
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
let cs = metrics_tick.connection_state.load(Ordering::Relaxed);
|
||||
let sent = metrics_tick.bytes_sent.load(Ordering::Relaxed);
|
||||
let recv = metrics_tick.bytes_recv.load(Ordering::Relaxed);
|
||||
|
||||
let mut w = writer_tick.lock().await;
|
||||
// Only send status change events
|
||||
if cs != last_state {
|
||||
last_state = cs;
|
||||
let json = serde_json::to_string(&HelperMsg::Status { value: cs }).unwrap_or_default();
|
||||
if w.write_all(format!("{}\n", json).as_bytes()).await.is_err() { break; }
|
||||
}
|
||||
// Always send metrics
|
||||
let json = serde_json::to_string(&HelperMsg::Metrics { bytes_sent: sent, bytes_recv: recv }).unwrap_or_default();
|
||||
if w.write_all(format!("{}\n", json).as_bytes()).await.is_err() { break; }
|
||||
drop(w);
|
||||
}
|
||||
});
|
||||
|
||||
send_msg(HelperMsg::Status { value: 1 });
|
||||
}
|
||||
|
||||
GuiCmd::Stop => {
|
||||
let mut st = state.lock().await;
|
||||
if let Some(tx) = st.shutdown_tx.take() {
|
||||
let _ = tx.send(true);
|
||||
}
|
||||
st.metrics = None;
|
||||
send_msg(HelperMsg::Status { value: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Binary file not shown.
Loading…
Reference in New Issue