fix(web-server): release port reliably on stop by awaiting graceful shutdown

This commit is contained in:
xintaofei
2026-04-24 15:23:12 +08:00
parent 0e09711e56
commit 6a0425239f
3 changed files with 42 additions and 9 deletions

View File

@@ -409,7 +409,7 @@ mod tauri_app {
if let tauri::RunEvent::ExitRequested { .. } = event { if let tauri::RunEvent::ExitRequested { .. } = event {
APP_QUITTING.store(true, Ordering::Relaxed); APP_QUITTING.store(true, Ordering::Relaxed);
if let Some(ws) = app.try_state::<web::WebServerState>() { if let Some(ws) = app.try_state::<web::WebServerState>() {
web::do_stop_web_server(&ws); tauri::async_runtime::block_on(web::do_stop_web_server(&ws));
} }
if let Some(tm) = app.try_state::<TerminalManager>() { if let Some(tm) = app.try_state::<TerminalManager>() {
tm.kill_all(); tm.kill_all();

View File

@@ -42,7 +42,7 @@ pub async fn start_web_server(
pub async fn stop_web_server( pub async fn stop_web_server(
Extension(state): Extension<Arc<AppState>>, Extension(state): Extension<Arc<AppState>>,
) -> Result<Json<()>, AppCommandError> { ) -> Result<Json<()>, AppCommandError> {
do_stop_web_server(&state.web_server_state); do_stop_web_server(&state.web_server_state).await;
Ok(Json(())) Ok(Json(()))
} }

View File

@@ -22,6 +22,7 @@ pub const DEFAULT_WEB_SERVICE_PORT: u16 = 3080;
pub struct WebServerState { pub struct WebServerState {
handle: Mutex<Option<tokio::task::JoinHandle<()>>>, handle: Mutex<Option<tokio::task::JoinHandle<()>>>,
shutdown_tx: Mutex<Option<tokio::sync::oneshot::Sender<()>>>,
port: AtomicU16, port: AtomicU16,
token: Mutex<String>, token: Mutex<String>,
running: std::sync::atomic::AtomicBool, running: std::sync::atomic::AtomicBool,
@@ -37,6 +38,7 @@ impl WebServerState {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
handle: Mutex::new(None), handle: Mutex::new(None),
shutdown_tx: Mutex::new(None),
port: AtomicU16::new(0), port: AtomicU16::new(0),
token: Mutex::new(String::new()), token: Mutex::new(String::new()),
running: std::sync::atomic::AtomicBool::new(false), running: std::sync::atomic::AtomicBool::new(false),
@@ -323,13 +325,18 @@ pub(crate) async fn do_start_web_server_with_state(
let actual_port = listener.local_addr().map(|a| a.port()).unwrap_or(port); let actual_port = listener.local_addr().map(|a| a.port()).unwrap_or(port);
eprintln!("[WEB] Starting web server on {}", addr); eprintln!("[WEB] Starting web server on {}", addr);
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
if let Err(e) = axum::serve(listener, router).await { let serve = axum::serve(listener, router).with_graceful_shutdown(async move {
let _ = shutdown_rx.await;
});
if let Err(e) = serve.await {
eprintln!("[WEB] Server error: {}", e); eprintln!("[WEB] Server error: {}", e);
} }
}); });
*ws.handle.lock().unwrap() = Some(handle); *ws.handle.lock().unwrap() = Some(handle);
*ws.shutdown_tx.lock().unwrap() = Some(shutdown_tx);
ws.port.store(actual_port, Ordering::Relaxed); ws.port.store(actual_port, Ordering::Relaxed);
*ws.token.lock().unwrap() = token.clone(); *ws.token.lock().unwrap() = token.clone();
// running already true from compare_exchange; disarm guard so it doesn't flip back. // running already true from compare_exchange; disarm guard so it doesn't flip back.
@@ -343,13 +350,34 @@ pub(crate) async fn do_start_web_server_with_state(
}) })
} }
pub(crate) fn do_stop_web_server(state: &WebServerState) { pub(crate) async fn do_stop_web_server(state: &WebServerState) {
if let Some(handle) = state.handle.lock().unwrap().take() { let handle_opt = state.handle.lock().unwrap().take();
handle.abort(); let shutdown_tx = state.shutdown_tx.lock().unwrap().take();
// Signal graceful shutdown so axum stops accepting new connections
// and drops the listening socket once the serve future resolves.
if let Some(tx) = shutdown_tx {
let _ = tx.send(());
} }
state.running.store(false, Ordering::Relaxed);
// Await the serve task so the OS socket is guaranteed released before we return.
// A live WebSocket/keep-alive connection can block graceful drain; after a
// short grace period, force-abort and await the cancellation to complete.
if let Some(mut handle) = handle_opt {
if tokio::time::timeout(std::time::Duration::from_secs(2), &mut handle)
.await
.is_err()
{
handle.abort();
let _ = handle.await;
}
}
// Only release the running flag after the listener is guaranteed dropped,
// so a concurrent start() cannot race into a bind() while the old socket lingers.
state.port.store(0, Ordering::Relaxed); state.port.store(0, Ordering::Relaxed);
*state.token.lock().unwrap() = String::new(); *state.token.lock().unwrap() = String::new();
state.running.store(false, Ordering::Release);
eprintln!("[WEB] Web server stopped"); eprintln!("[WEB] Web server stopped");
} }
@@ -438,13 +466,18 @@ pub async fn start_web_server(
let actual_port = listener.local_addr().map(|a| a.port()).unwrap_or(port_val); let actual_port = listener.local_addr().map(|a| a.port()).unwrap_or(port_val);
eprintln!("[WEB] Starting web server on {}", addr); eprintln!("[WEB] Starting web server on {}", addr);
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
if let Err(e) = axum::serve(listener, router).await { let serve = axum::serve(listener, router).with_graceful_shutdown(async move {
let _ = shutdown_rx.await;
});
if let Err(e) = serve.await {
eprintln!("[WEB] Server error: {}", e); eprintln!("[WEB] Server error: {}", e);
} }
}); });
*ws.handle.lock().unwrap() = Some(handle); *ws.handle.lock().unwrap() = Some(handle);
*ws.shutdown_tx.lock().unwrap() = Some(shutdown_tx);
ws.port.store(actual_port, Ordering::Relaxed); ws.port.store(actual_port, Ordering::Relaxed);
*ws.token.lock().unwrap() = token.clone(); *ws.token.lock().unwrap() = token.clone();
// running already true from compare_exchange; disarm guard so it doesn't flip back. // running already true from compare_exchange; disarm guard so it doesn't flip back.
@@ -463,7 +496,7 @@ pub async fn start_web_server(
pub async fn stop_web_server( pub async fn stop_web_server(
state: tauri::State<'_, WebServerState>, state: tauri::State<'_, WebServerState>,
) -> Result<(), AppCommandError> { ) -> Result<(), AppCommandError> {
do_stop_web_server(&state); do_stop_web_server(&state).await;
Ok(()) Ok(())
} }