mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 12:34:22 +08:00
fix: 修复 OAuth 回调服务器启动卡顿 40s
定位:oauth_start_callback_server 在端口被占用时调用 lsof 未加 -n/-P, 触发反向 DNS 解析,遇到不可达解析器会卡 ~40s 才超时返回。 修复: - lsof 加 -n -P 跳过 DNS/端口名解析,并用 tokio::time::timeout 包 5s - 改用 JoinHandle + abort() 硬中止服务任务,停止时立即 drop listener 释放端口,避免遗留服务导致下次启动走到 kill 路径 - netstat 同样加 5s 超时 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,8 @@ use std::net::SocketAddr;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
@@ -25,7 +25,8 @@ pub struct OAuthCallbackResult {
|
|||||||
pub error_description: Option<String>,
|
pub error_description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
static OAUTH_SERVER_SHUTDOWN: once_cell::sync::Lazy<Arc<Mutex<Option<oneshot::Sender<()>>>>> =
|
/// 持有回调服务器任务的 JoinHandle,用于在停止时硬中止任务以立即释放端口。
|
||||||
|
static OAUTH_SERVER_HANDLE: once_cell::sync::Lazy<Arc<Mutex<Option<JoinHandle<()>>>>> =
|
||||||
once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(None)));
|
once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(None)));
|
||||||
|
|
||||||
/// OAuth 本地回调服务器监听端口
|
/// OAuth 本地回调服务器监听端口
|
||||||
@@ -46,15 +47,17 @@ pub async fn oauth_start_callback_server(
|
|||||||
};
|
};
|
||||||
log("oauth_start_callback_server enter");
|
log("oauth_start_callback_server enter");
|
||||||
|
|
||||||
let mut shutdown_tx = OAUTH_SERVER_SHUTDOWN.lock().await;
|
let mut handle_guard = OAUTH_SERVER_HANDLE.lock().await;
|
||||||
log("after acquire shutdown lock");
|
log("after acquire handle lock");
|
||||||
|
|
||||||
// 如果服务器已经在运行,直接返回 URL
|
// 如果服务器已经在运行,直接返回 URL
|
||||||
if shutdown_tx.is_some() {
|
if let Some(h) = handle_guard.as_ref() {
|
||||||
let port = OAUTH_CALLBACK_PORT;
|
if !h.is_finished() {
|
||||||
let url = format!("http://127.0.0.1:{}/oauth/callback", port);
|
let port = OAUTH_CALLBACK_PORT;
|
||||||
log("server already running, return early");
|
let url = format!("http://127.0.0.1:{}/oauth/callback", port);
|
||||||
return Ok(IpcResponse::success(OAuthServerStartResult { url, port }));
|
log("server already running, return early");
|
||||||
|
return Ok(IpcResponse::success(OAuthServerStartResult { url, port }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let port = OAUTH_CALLBACK_PORT;
|
let port = OAUTH_CALLBACK_PORT;
|
||||||
@@ -76,7 +79,7 @@ pub async fn oauth_start_callback_server(
|
|||||||
port,
|
port,
|
||||||
t0.elapsed().as_millis()
|
t0.elapsed().as_millis()
|
||||||
);
|
);
|
||||||
if let Err(kill_err) = kill_processes_on_port(port) {
|
if let Err(kill_err) = kill_processes_on_port(port).await {
|
||||||
eprintln!("[OAuth Callback] 强杀端口占用进程失败: {}", kill_err);
|
eprintln!("[OAuth Callback] 强杀端口占用进程失败: {}", kill_err);
|
||||||
}
|
}
|
||||||
log("after kill_processes_on_port");
|
log("after kill_processes_on_port");
|
||||||
@@ -92,98 +95,121 @@ pub async fn oauth_start_callback_server(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (tx, rx) = oneshot::channel::<()>();
|
|
||||||
*shutdown_tx = Some(tx);
|
|
||||||
drop(shutdown_tx);
|
|
||||||
log("after store shutdown tx");
|
|
||||||
|
|
||||||
let app_handle_clone = app_handle.clone();
|
let app_handle_clone = app_handle.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
let join = tokio::spawn(async move {
|
||||||
let app = axum::Router::new()
|
let app = axum::Router::new()
|
||||||
.route("/oauth/callback", axum::routing::get(handle_oauth_callback))
|
.route("/oauth/callback", axum::routing::get(handle_oauth_callback))
|
||||||
.layer(axum::extract::Extension(app_handle_clone));
|
.layer(axum::extract::Extension(app_handle_clone));
|
||||||
|
|
||||||
println!("OAuth callback server started at {}", url_for_spawn);
|
println!("OAuth callback server started at {}", url_for_spawn);
|
||||||
|
|
||||||
let server = axum::serve(listener, app);
|
// 服务运行至此任务被 abort(drop listener → 释放端口)或自身结束
|
||||||
|
axum::serve(listener, app).await.ok();
|
||||||
tokio::select! {
|
println!("OAuth callback server task exited");
|
||||||
_ = server => {},
|
|
||||||
_ = rx => {
|
|
||||||
println!("OAuth callback server shutting down");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
*handle_guard = Some(join);
|
||||||
|
drop(handle_guard);
|
||||||
log("after tokio::spawn, returning");
|
log("after tokio::spawn, returning");
|
||||||
|
|
||||||
Ok(IpcResponse::success(OAuthServerStartResult { url, port }))
|
Ok(IpcResponse::success(OAuthServerStartResult { url, port }))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 查找并强杀占用指定端口的进程。
|
/// 查找并强杀占用指定端口的进程。
|
||||||
/// macOS/Linux 使用 lsof,Windows 使用 netstat + taskkill。
|
/// macOS/Linux 使用 lsof (-n -P 跳过 DNS/端口名解析),Windows 使用 netstat + taskkill。
|
||||||
fn kill_processes_on_port(port: u16) -> Result<(), String> {
|
/// 全程异步并带 5s 超时,避免 DNS 解析挂起导致命令阻塞数十秒。
|
||||||
#[cfg(unix)]
|
async fn kill_processes_on_port(port: u16) -> Result<(), String> {
|
||||||
{
|
let pids = find_pids_on_port(port).await?;
|
||||||
let output = std::process::Command::new("lsof")
|
for pid in pids {
|
||||||
.args(["-t", "-i", &format!(":{}", port)])
|
let _ = kill_pid(pid).await;
|
||||||
.output()
|
println!("[OAuth Callback] 已强杀占用端口 {} 的进程 {}", port, pid);
|
||||||
.map_err(|e| format!("执行 lsof 失败: {}", e))?;
|
|
||||||
let pids = String::from_utf8_lossy(&output.stdout)
|
|
||||||
.lines()
|
|
||||||
.filter_map(|line| line.trim().parse::<i32>().ok())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
for pid in pids {
|
|
||||||
let _ = std::process::Command::new("kill")
|
|
||||||
.args(["-9", &pid.to_string()])
|
|
||||||
.status();
|
|
||||||
println!("[OAuth Callback] 已强杀占用端口 {} 的进程 {}", port, pid);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
Ok(())
|
||||||
{
|
}
|
||||||
let output = std::process::Command::new("netstat")
|
|
||||||
.args(["-ano"])
|
#[cfg(unix)]
|
||||||
.output()
|
async fn find_pids_on_port(port: u16) -> Result<Vec<u32>, String> {
|
||||||
.map_err(|e| format!("执行 netstat 失败: {}", e))?;
|
use tokio::process::Command;
|
||||||
let text = String::from_utf8_lossy(&output.stdout);
|
let port_arg = format!(":{}", port);
|
||||||
let needle = format!(":{}", port);
|
let output = tokio::time::timeout(
|
||||||
let mut pids = std::collections::HashSet::<String>::new();
|
std::time::Duration::from_secs(5),
|
||||||
for line in text.lines() {
|
Command::new("lsof")
|
||||||
// 只匹配监听/连接行且包含目标端口
|
.args(["-t", "-n", "-P", "-i", &port_arg])
|
||||||
if line.contains(&needle) {
|
.output(),
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
)
|
||||||
if let Some(pid) = parts.last() {
|
.await
|
||||||
if pid.chars().all(|c| c.is_ascii_digit()) {
|
.map_err(|_| format!("lsof 执行超时 (5s), 端口 {}", port))?
|
||||||
pids.push(pid.to_string());
|
.map_err(|e| format!("执行 lsof 失败: {}", e))?;
|
||||||
}
|
let pids = String::from_utf8_lossy(&output.stdout)
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| line.trim().parse::<u32>().ok())
|
||||||
|
.collect();
|
||||||
|
Ok(pids)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
async fn find_pids_on_port(port: u16) -> Result<Vec<u32>, String> {
|
||||||
|
use tokio::process::Command;
|
||||||
|
let output = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(5),
|
||||||
|
Command::new("netstat").args(["-ano"]).output(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| format!("netstat 执行超时 (5s), 端口 {}", port))?
|
||||||
|
.map_err(|e| format!("执行 netstat 失败: {}", e))?;
|
||||||
|
let text = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let needle = format!(":{}", port);
|
||||||
|
let mut pids = std::collections::HashSet::<u32>::new();
|
||||||
|
for line in text.lines() {
|
||||||
|
if line.contains(&needle) {
|
||||||
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if let Some(pid_str) = parts.last() {
|
||||||
|
if let Ok(pid) = pid_str.parse::<u32>() {
|
||||||
|
pids.insert(pid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for pid in pids {
|
|
||||||
let _ = std::process::Command::new("taskkill")
|
|
||||||
.args(["/PID", &pid, "/F"])
|
|
||||||
.status();
|
|
||||||
println!("[OAuth Callback] 已强杀占用端口 {} 的进程 {}", port, pid);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
#[cfg(not(any(unix, windows)))]
|
|
||||||
{
|
|
||||||
let _ = port;
|
|
||||||
Err("当前平台不支持强杀端口占用进程".into())
|
|
||||||
}
|
}
|
||||||
|
Ok(pids.into_iter().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
async fn kill_pid(pid: u32) -> Result<(), String> {
|
||||||
|
std::process::Command::new("kill")
|
||||||
|
.args(["-9", &pid.to_string()])
|
||||||
|
.status()
|
||||||
|
.map_err(|e| format!("kill 失败: {}", e))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
async fn kill_pid(pid: u32) -> Result<(), String> {
|
||||||
|
std::process::Command::new("taskkill")
|
||||||
|
.args(["/PID", &pid.to_string(), "/F"])
|
||||||
|
.status()
|
||||||
|
.map_err(|e| format!("taskkill 失败: {}", e))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(unix, windows)))]
|
||||||
|
async fn find_pids_on_port(_port: u16) -> Result<Vec<u32>, String> {
|
||||||
|
Err("当前平台不支持强杀端口占用进程".into())
|
||||||
|
}
|
||||||
|
#[cfg(not(any(unix, windows)))]
|
||||||
|
async fn kill_pid(_pid: u32) -> Result<(), String> {
|
||||||
|
Err("当前平台不支持强杀端口占用进程".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn oauth_stop_callback_server(
|
pub async fn oauth_stop_callback_server(
|
||||||
_state: State<'_, Arc<RwLock<AppState>>>,
|
_state: State<'_, Arc<RwLock<AppState>>>,
|
||||||
) -> Result<IpcResponse<()>, String> {
|
) -> Result<IpcResponse<()>, String> {
|
||||||
let mut shutdown_tx = OAUTH_SERVER_SHUTDOWN.lock().await;
|
let mut handle_guard = OAUTH_SERVER_HANDLE.lock().await;
|
||||||
|
|
||||||
if let Some(tx) = shutdown_tx.take() {
|
if let Some(join) = handle_guard.take() {
|
||||||
let _ = tx.send(());
|
// 硬中止任务:drop 其持有的 listener,立即释放端口
|
||||||
|
join.abort();
|
||||||
|
println!("[OAuth Callback] 已中止回调服务器任务,端口已释放");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(IpcResponse::success(()))
|
Ok(IpcResponse::success(()))
|
||||||
|
|||||||
Reference in New Issue
Block a user