Skip to content

Commit 8f5081b

Browse files
authored
fix: ProxyManager.stop/stop_silent send signal + abort task 保证端口释放 (#207)
silent failure 修: 用户点"停止转发"后 UI/日志显示 "forwarding stopped" 但端口实际仍被占用。 根因: stop_silent (proxy_runner.rs:127) 只 fire-and-forget 发 oneshot shutdown 信号立刻返, 但 axum::serve.with_graceful_shutdown (start line 81-87) 收到信号后**等所有 in-flight HTTP connection 完成**才 drop listener。SSE / long polling / keep-alive idle / hung connection 可能 永远等不到 → listener 不 drop → 端口不释放, 但 UI / log 已"stopped" → 用户感知"日志说停了实际没停"。 修法: - ProxyHandle 加 task: tokio::task::JoinHandle<()> 字段, start 把 tokio::spawn 的 JoinHandle 存进 handle。 - stop / stop_silent 在 shutdown_tx.send(()) **之后**立刻调 h.task.abort() 强制 drop server future → listener 同步 drop → 端口立刻释放, 不依赖 graceful drain。 - race condition path (proxy already started by another path) 同样 abort 自己的 task 防 listener leak。 代价: in-flight HTTP connection 被强断, client 见 connection reset。 但"用户点停止 = 真要停"语义对, 这是合理 trade-off。 测试: 95/95 pass, 无 regression。proxy_runner.rs 现无单测基础设施, unit test 加 mock config_file 工程量大, 靠 PR 真机测验: 1. 启转发, lsof -iTCP:18080 应看到 axum process 2. 点停止 3. lsof -iTCP:18080 应**立即**空, 无残留 4. 立刻重启转发应成功(端口已释放, bind 不报 Address already in use)
1 parent 7e42148 commit 8f5081b

1 file changed

Lines changed: 26 additions & 3 deletions

File tree

src-tauri/src/proxy_runner.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use codex_app_transfer_registry::{config_file, Config};
2222
use serde::Serialize;
2323
use tokio::net::TcpListener;
2424
use tokio::sync::oneshot;
25+
use tokio::task::JoinHandle;
2526

2627
#[derive(Debug, Serialize, Clone)]
2728
pub struct ProxyStatus {
@@ -37,6 +38,15 @@ pub struct ProxyStatus {
3738
struct ProxyHandle {
3839
addr: SocketAddr,
3940
shutdown_tx: oneshot::Sender<()>,
41+
/// axum::serve task 的 JoinHandle — stop / stop_silent 在发 graceful
42+
/// shutdown 信号**之后**立刻 task.abort() 强制 drop server future,
43+
/// listener 同步 drop → 端口立刻释放。
44+
///
45+
/// 修 silent failure:graceful_shutdown 等所有 in-flight connection
46+
/// 完成才 drop listener,SSE / long-polling / hung connection 可能
47+
/// 永远等不到 → 端口不释放,但 UI / log 已 "stopped" → 用户感知
48+
/// "停了但端口还占"。abort 保证 stop_silent 同步返时端口必释放。
49+
task: JoinHandle<()>,
4050
gateway_auth: bool,
4151
provider_count: usize,
4252
active_provider: Option<String>,
@@ -78,7 +88,7 @@ impl ProxyManager {
7888
.map_err(|e| format!("cannot read listener address: {e}"))?;
7989
let router = build_router(Arc::new(snapshot.resolver));
8090
let (tx, rx) = oneshot::channel::<()>();
81-
tokio::spawn(async move {
91+
let task = tokio::spawn(async move {
8292
let _ = axum::serve(listener, router.into_make_service())
8393
.with_graceful_shutdown(async move {
8494
let _ = rx.await;
@@ -90,14 +100,17 @@ impl ProxyManager {
90100
let new_handle = ProxyHandle {
91101
addr,
92102
shutdown_tx: tx,
103+
task,
93104
gateway_auth: snapshot.gateway_auth,
94105
provider_count: snapshot.provider_count,
95106
active_provider: snapshot.active_provider.clone(),
96107
};
97108
let mut guard = self.handle.lock().unwrap();
98109
if guard.is_some() {
99-
// race condition,自己的 listener 让出去:发 shutdown 给自己再报错
110+
// race condition,自己的 listener 让出去:发 shutdown + abort task
111+
// (abort 同步 drop listener,端口立刻释放,不依赖 graceful drain)
100112
let _ = new_handle.shutdown_tx.send(());
113+
new_handle.task.abort();
101114
return Err("proxy already started by another path".to_owned());
102115
}
103116
*guard = Some(new_handle);
@@ -110,24 +123,34 @@ impl ProxyManager {
110123
})
111124
}
112125

113-
/// 触发 graceful shutdown。未 running 时报错。
126+
/// 触发 graceful shutdown 后立刻 abort task 释放端口。未 running 时报错。
127+
///
128+
/// **为什么两步**:`shutdown_tx.send(())` 给 axum graceful drain 机会让
129+
/// 正常完成的 request 跑完;紧跟 `task.abort()` 同步 drop server future
130+
/// → listener drop → **端口立刻释放**,不会卡在 in-flight SSE / long
131+
/// polling / hung connection 上(那种 connection 可能永远 drain 不完)。
132+
/// 代价:in-flight connection 被强断,client 见 connection reset —
133+
/// 但"用户点停止 = 真要停",这是合理 trade-off。
114134
#[allow(dead_code)]
115135
pub fn stop(&self) -> Result<(), String> {
116136
let mut guard = self.handle.lock().unwrap();
117137
match guard.take() {
118138
Some(h) => {
119139
let _ = h.shutdown_tx.send(());
140+
h.task.abort();
120141
Ok(())
121142
}
122143
None => Err("proxy is not running".to_owned()),
123144
}
124145
}
125146

126147
/// 静默 stop:用于 app exit / 异常路径,不报错只尽力关。
148+
/// 同样走 send signal + abort 双保险确保端口释放(详见 [`Self::stop`])。
127149
pub fn stop_silent(&self) {
128150
let mut guard = self.handle.lock().unwrap();
129151
if let Some(h) = guard.take() {
130152
let _ = h.shutdown_tx.send(());
153+
h.task.abort();
131154
}
132155
}
133156

0 commit comments

Comments
 (0)