fix(proxy): roll back the saved listen address when the restart fails to bind

When the new address or port could not be bound, update_config had already
stopped the old server and written the new config, so the service stayed down
and the next save returned Ok(false) because nothing was running: the UI
reported success while clients still pointed at the old port. On a failed
restart write the previous config back (global four fields or the legacy full
row) and start the server on it again, so a failed save changes nothing.
This commit is contained in:
Jason committed 2026-10-03 23:29:00 +08:00
1 parent edc8c2b966
commit 55fa8e852e
1 file changed
+105 -14
+105 -14
View File
@@ -25,6 +25,22 @@ pub struct ProxyService {
switch_locks: SwitchLockManager,
}
/// 重启失败时写回库的那份配置:设置页只动四个全局字段,旧接口是整份七个字段。
enum SavedProxyConfig {
Global(GlobalProxyConfig),
Legacy(ProxyConfig),
}
impl SavedProxyConfig {
async fn write(&self, db: &Database) -> Result<(), String> {
match self {
Self::Global(config) => db.update_global_proxy_config(config.clone()).await,
Self::Legacy(config) => db.update_proxy_config(config.clone()).await,
}
.map_err(|e| format!("恢复原代理配置失败: {e}"))
}
}
/// 客户端连接代理用的地址。`listen_address` 可能是 `0.0.0.0` / `::`(监听所有网卡),
/// 客户端连不上这个地址,改用本机回环;IPv6 加方括号。
pub(crate) fn proxy_origin(listen_address: &str, listen_port: u16) -> String {
@@ -468,7 +484,8 @@ impl ProxyService {
// 判断是否需要重启(地址或端口变更)
let require_restart = new_config.listen_address != previous.listen_address
|| new_config.listen_port != previous.listen_port;
self.apply_saved_config(require_restart).await
self.apply_saved_config(require_restart, SavedProxyConfig::Legacy(previous))
.await
}
/// 设置页「保存并重启服务」:只写四个全局字段。各应用自己的重试次数和超时不碰——
@@ -488,12 +505,19 @@ impl ProxyService {
let require_restart = config.listen_address != previous.listen_address
|| config.listen_port != previous.listen_port;
self.apply_saved_config(require_restart).await
self.apply_saved_config(require_restart, SavedProxyConfig::Global(previous))
.await
}
/// 让运行中的服务用上刚写进库的配置:地址或端口变了就重启(返回 true),否则实时应用。
/// 服务没在跑时什么都不做。
async fn apply_saved_config(&self, require_restart: bool) -> Result<bool, String> {
/// 服务没在跑时什么都不做。新地址绑不上就把 `previous` 写回库、按旧地址重新拉起,让
/// 「保存失败」就是什么都没变:服务停着不管的话,下一次保存会因为没在跑而假成功,
/// 客户端也还指着旧地址。
async fn apply_saved_config(
&self,
require_restart: bool,
previous: SavedProxyConfig,
) -> Result<bool, String> {
let mut server_guard = self.server.write().await;
if server_guard.is_none() {
return Ok(false);
@@ -519,23 +543,48 @@ impl ProxyService {
.map_err(|e| format!("重启前停止代理服务器失败: {e}"))?;
}
let error = match self.start_with(&new_config).await {
Ok(server) => {
*server_guard = Some(server);
log::info!("代理配置已更新,服务器已自动重启应用最新配置");
return Ok(true);
}
Err(error) => format!("重启代理服务器失败: {error}"),
};
log::warn!("{error},恢复原配置并重新启动");
match self.restore_and_start(previous).await {
Ok(server) => {
*server_guard = Some(server);
Err(format!("{error};已按原配置重新启动"))
}
Err(restore_error) => Err(format!("{error};恢复原配置也失败: {restore_error}")),
}
}
/// 按 `config` 起一个新服务,动态端口回写进库。
async fn start_with(&self, config: &ProxyConfig) -> Result<ProxyServer, String> {
let app_handle = self.app_handle.read().await.clone();
let new_server = ProxyServer::new(new_config.clone(), self.db.clone(), app_handle);
let info = new_server
.start()
.await
.map_err(|e| format!("重启代理服务器失败: {e}"))?;
let server = ProxyServer::new(config.clone(), self.db.clone(), app_handle);
let info = server.start().await.map_err(|e| e.to_string())?;
if let Err(e) = self
.persist_ephemeral_listen_port_if_needed(&new_config, info.port)
.persist_ephemeral_listen_port_if_needed(config, info.port)
.await
{
let _ = new_server.stop().await;
let _ = server.stop().await;
return Err(e);
}
Ok(server)
}
*server_guard = Some(new_server);
log::info!("代理配置已更新,服务器已自动重启应用最新配置");
Ok(true)
async fn restore_and_start(&self, previous: SavedProxyConfig) -> Result<ProxyServer, String> {
previous.write(&self.db).await?;
let config = self
.db
.get_proxy_config()
.await
.map_err(|e| format!("获取代理配置失败: {e}"))?;
self.start_with(&config).await
}
/// 检查服务器是否正在运行
@@ -679,4 +728,46 @@ mod tests {
serde_json::to_value(global).unwrap()
);
}
/// 新端口绑不上时「保存失败」要等于什么都没变:库里还是旧端口、服务还在旧端口上跑。
/// 服务停着不管的话,下一次保存会因为「没在跑」直接假成功。
#[tokio::test]
async fn failed_restart_restores_previous_config_and_server() {
let db = Arc::new(Database::memory().unwrap());
let service = ProxyService::new(db.clone());
let mut global = db.get_global_proxy_config().await.unwrap();
global.listen_address = "127.0.0.1".to_string();
global.listen_port = 0;
db.update_global_proxy_config(global).await.unwrap();
let running_port = service.start().await.unwrap().port;
assert_ne!(running_port, 0);
// 占住另一个端口,让重启时绑定失败
let blocker = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let taken_port = blocker.local_addr().unwrap().port();
let mut attempt = db.get_global_proxy_config().await.unwrap();
attempt.listen_port = taken_port;
let error = service
.update_global_config(&attempt)
.await
.expect_err("binding a taken port must fail");
assert!(error.contains("重启代理服务器失败"), "{error}");
assert_eq!(
db.get_global_proxy_config().await.unwrap().listen_port,
running_port,
"库里应回到原端口"
);
let status = service.get_status().await.unwrap();
assert!(status.running, "服务应按原配置重新拉起");
assert_eq!(status.port, running_port);
// 再保存一次不再假成功:服务在跑,配置没变就实时应用
let again = db.get_global_proxy_config().await.unwrap();
assert!(!service.update_global_config(&again).await.unwrap());
assert!(service.is_running().await);
service.stop().await.unwrap();
drop(blocker);
}
}