fix(split-tunnel): hot-reload exclusions into running proxy tunnel without reconnect

This commit is contained in:
ospab 2026-06-13 22:30:01 +03:00
parent 83ba39e59a
commit 4543fa82f8
2 changed files with 16 additions and 13 deletions

View File

@ -95,6 +95,7 @@ enum HelperMsg {
struct InProcessState { struct InProcessState {
shutdown_tx: Option<tokio::sync::watch::Sender<bool>>, shutdown_tx: Option<tokio::sync::watch::Sender<bool>>,
config_tx: Option<tokio::sync::watch::Sender<ostp_client::config::ClientConfig>>,
metrics: Arc<ostp_client::bridge::BridgeMetrics>, metrics: Arc<ostp_client::bridge::BridgeMetrics>,
handle: tokio::task::JoinHandle<Result<(), String>>, handle: tokio::task::JoinHandle<Result<(), String>>,
error_msg: Arc<tokio::sync::Mutex<Option<String>>>, error_msg: Arc<tokio::sync::Mutex<Option<String>>>,
@ -443,10 +444,13 @@ async fn reload_tunnel(state: tauri::State<'_, AppState>) -> Result<bool, String
); );
let _ = h.cmd_tx.send(cmd).await; let _ = h.cmd_tx.send(cmd).await;
} }
Some(TunnelHandle::InProcess(_s)) => { Some(TunnelHandle::InProcess(s)) => {
// Restarting in-process tunnel is not supported without re-calling start_tunnel, // Hot-reload exclusions by pushing new config into the watch channel.
// but we can just abort and we should really call start_tunnel again. // If config_tx is None (old tunnel without this feature), return false.
// For now, return false. if let Some(ref tx) = s.config_tx {
let _ = tx.send(core_cfg);
return Ok(true);
}
return Ok(false); return Ok(false);
} }
None => {} None => {}
@ -557,12 +561,14 @@ async fn start_proxy_in_process(
}); });
let (shutdown_tx, shutdown_rx) = watch::channel(false); 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 metrics_clone = metrics.clone();
let error_msg = Arc::new(tokio::sync::Mutex::new(None)); let error_msg = Arc::new(tokio::sync::Mutex::new(None));
let error_msg_clone = error_msg.clone(); let error_msg_clone = error_msg.clone();
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
match ostp_client::runner::run_client_core(mapped, metrics_clone, shutdown_rx, None).await { match ostp_client::runner::run_client_core(mapped, metrics_clone, shutdown_rx, Some(config_rx)).await {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(e) => { Err(e) => {
let mut err_guard = error_msg_clone.lock().await; let mut err_guard = error_msg_clone.lock().await;
@ -575,6 +581,7 @@ async fn start_proxy_in_process(
guard.tunnel = Some(TunnelHandle::InProcess(InProcessState { guard.tunnel = Some(TunnelHandle::InProcess(InProcessState {
shutdown_tx: Some(shutdown_tx), shutdown_tx: Some(shutdown_tx),
config_tx: Some(config_tx),
metrics, metrics,
handle, handle,
error_msg, error_msg,

View File

@ -431,6 +431,9 @@ async function handleSave(silent = false) {
const ok = await invoke('save_config', { jsonContent: JSON.stringify(rawConfig, null, 2) }); const ok = await invoke('save_config', { jsonContent: JSON.stringify(rawConfig, null, 2) });
if (!ok && !silent) { if (!ok && !silent) {
showToast(t('toast_error'), 'error'); showToast(t('toast_error'), 'error');
} else if (ok && appState === 'connected') {
// Hot-reload exclusions into the running tunnel (no reconnect needed)
try { await invoke('reload_tunnel'); } catch { /* ignore */ }
} }
} catch (err) { } catch (err) {
if (!silent) showToast(String(err), 'error'); if (!silent) showToast(String(err), 'error');
@ -598,14 +601,7 @@ window.addEventListener('DOMContentLoaded', async () => {
// Auto-save wiring for standard form elements (excluding tag-inputs which wire themselves) // Auto-save wiring for standard form elements (excluding tag-inputs which wire themselves)
const formInputs = document.querySelectorAll('#settings-screen input:not(#in-import-url):not(.tag-input-field), #settings-screen select'); const formInputs = document.querySelectorAll('#settings-screen input:not(#in-import-url):not(.tag-input-field), #settings-screen select');
formInputs.forEach(el => { formInputs.forEach(el => {
el.addEventListener('input', () => { el.addEventListener('input', scheduleAutoSave);
scheduleAutoSave();
if (appState === 'connected') {
if (window.__TAURI__ && window.__TAURI__.invoke) {
window.__TAURI__.invoke('reload_tunnel');
}
}
});
el.addEventListener('change', scheduleAutoSave); el.addEventListener('change', scheduleAutoSave);
}); });