mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-01 03:35:30 +08:00
Harden Codex takeover ownership signaling and serialize switch/takeover
Gate provider sync and switching on the restore backup / live placeholder
("is this live file owned by takeover?") instead of the lagging
proxy_config.enabled and proxy-running flags. The backup is created
before enabled=true is committed, so during that activation window the
old guards were blind and a concurrent sync/switch could rewrite the
taken-over live file, clearing Codex auth.json for a mis-categorized
provider.
Acquire a per-app switch lock around both set_takeover_for_app and
provider switching so the two cannot interleave, splitting the locking
entry points into outer (lock) / inner (no-lock) pairs to stay
deadlock-free. Preserve the official OAuth auth in provider-rebuilt
restore backups by routing the provider token into config.toml. Refine
takeover idempotency to require the live config to point at the current
proxy URL, rebuilding from backup when it does not.
Add unit and integration tests covering the official -> DeepSeek ->
takeover on/off lifecycle and the stopped-proxy switch path.
This commit is contained in:
@@ -923,6 +923,48 @@ pub(crate) fn sync_current_provider_for_app_to_live(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync_current_provider_for_app_respecting_takeover(
|
||||
state: &AppState,
|
||||
app_type: &AppType,
|
||||
) -> Result<(), AppError> {
|
||||
let current_id = match crate::settings::get_effective_current_provider(&state.db, app_type)? {
|
||||
Some(id) => id,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let providers = state.db.get_all_providers(app_type.as_str())?;
|
||||
let Some(provider) = providers.get(¤t_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let has_live_backup = futures::executor::block_on(state.db.get_live_backup(app_type.as_str()))
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some();
|
||||
let live_taken_over = state
|
||||
.proxy_service
|
||||
.detect_takeover_in_live_config_for_app(app_type);
|
||||
|
||||
// `enabled` is set only after takeover writes complete. During that
|
||||
// activation window, backup/live placeholders are the authoritative signal
|
||||
// that normal provider sync must not rewrite the managed live file.
|
||||
if has_live_backup || live_taken_over {
|
||||
if matches!(app_type, AppType::ClaudeDesktop) {
|
||||
write_live_with_common_config(state.db.as_ref(), app_type, provider)?;
|
||||
} else {
|
||||
futures::executor::block_on(
|
||||
state
|
||||
.proxy_service
|
||||
.update_live_backup_from_provider(app_type.as_str(), provider),
|
||||
)
|
||||
.map_err(|e| AppError::Message(format!("更新 Live 备份失败: {e}")))?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
write_live_with_common_config(state.db.as_ref(), app_type, provider)
|
||||
}
|
||||
|
||||
/// Sync current provider to live configuration
|
||||
///
|
||||
/// 使用有效的当前供应商 ID(验证过存在性)。
|
||||
@@ -937,19 +979,10 @@ pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> {
|
||||
// Additive mode: sync ALL providers
|
||||
sync_all_providers_to_live(state, &app_type)?;
|
||||
} else {
|
||||
// Switch mode: sync only current provider
|
||||
let current_id =
|
||||
match crate::settings::get_effective_current_provider(&state.db, &app_type)? {
|
||||
Some(id) => id,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let providers = state.db.get_all_providers(app_type.as_str())?;
|
||||
if let Some(provider) = providers.get(¤t_id) {
|
||||
write_live_with_common_config(state.db.as_ref(), &app_type, provider)?;
|
||||
}
|
||||
// Note: get_effective_current_provider already validates existence,
|
||||
// so providers.get() should always succeed here
|
||||
// Switch mode: sync only current provider. During proxy takeover,
|
||||
// update the restore backup instead of rewriting the taken-over
|
||||
// live file.
|
||||
sync_current_provider_for_app_respecting_takeover(state, &app_type)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1391,11 +1391,13 @@ impl ProviderService {
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some();
|
||||
let is_proxy_running = futures::executor::block_on(state.proxy_service.is_running());
|
||||
let live_taken_over = state
|
||||
.proxy_service
|
||||
.detect_takeover_in_live_config_for_app(&app_type);
|
||||
let should_sync_via_proxy = is_proxy_running && (has_live_backup || live_taken_over);
|
||||
// Backup or live placeholders mean the live file is currently owned
|
||||
// by proxy takeover, including the short activation window before
|
||||
// proxy_config.enabled is committed.
|
||||
let should_sync_via_proxy = has_live_backup || live_taken_over;
|
||||
|
||||
if should_sync_via_proxy {
|
||||
if matches!(app_type, AppType::ClaudeDesktop) {
|
||||
@@ -1409,7 +1411,9 @@ impl ProviderService {
|
||||
.map_err(|e| AppError::Message(format!("更新 Live 备份失败: {e}")))?;
|
||||
}
|
||||
|
||||
if matches!(app_type, AppType::Claude) {
|
||||
if matches!(app_type, AppType::Claude)
|
||||
&& futures::executor::block_on(state.proxy_service.is_running())
|
||||
{
|
||||
futures::executor::block_on(
|
||||
state
|
||||
.proxy_service
|
||||
@@ -1589,21 +1593,32 @@ impl ProviderService {
|
||||
return Self::switch_normal(state, app_type, id, &providers);
|
||||
}
|
||||
|
||||
// Check if proxy takeover mode is active AND proxy server is actually running
|
||||
// Both conditions must be true to use hot-switch mode
|
||||
// Use blocking wait since this is a sync function
|
||||
// Provider switches and takeover toggles both mutate live config and the
|
||||
// restore backup. Serialize them per app, then decide from the locked
|
||||
// current state so a just-started takeover cannot be overwritten by a
|
||||
// normal live write.
|
||||
let _switch_guard =
|
||||
if matches!(app_type, AppType::Claude | AppType::Codex | AppType::Gemini) {
|
||||
Some(futures::executor::block_on(
|
||||
state.proxy_service.lock_switch_for_app(app_type.as_str()),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Backup or live placeholders mean the live file is owned by proxy
|
||||
// takeover, even if the proxy server is temporarily stopped or is in the
|
||||
// activation window before enabled=true is committed.
|
||||
let is_app_taken_over =
|
||||
futures::executor::block_on(state.db.get_live_backup(app_type.as_str()))
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some();
|
||||
let is_proxy_running = futures::executor::block_on(state.proxy_service.is_running());
|
||||
let live_taken_over = state
|
||||
.proxy_service
|
||||
.detect_takeover_in_live_config_for_app(&app_type);
|
||||
|
||||
// Hot-switch only when BOTH: this app is taken over AND proxy server is actually running
|
||||
let should_hot_switch = (is_app_taken_over || live_taken_over) && is_proxy_running;
|
||||
let should_hot_switch = is_app_taken_over || live_taken_over;
|
||||
|
||||
// Block switching to official providers when proxy takeover is active.
|
||||
// Using a proxy with official APIs (Anthropic/OpenAI/Google) may cause account bans.
|
||||
@@ -1626,7 +1641,7 @@ impl ProviderService {
|
||||
futures::executor::block_on(
|
||||
state
|
||||
.proxy_service
|
||||
.hot_switch_provider(app_type.as_str(), id),
|
||||
.hot_switch_provider_inner(app_type.as_str(), id),
|
||||
)
|
||||
.map_err(|e| AppError::Message(format!("热切换失败: {e}")))?;
|
||||
|
||||
@@ -1800,11 +1815,6 @@ impl ProviderService {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let takeover_enabled =
|
||||
futures::executor::block_on(state.db.get_proxy_config_for_app(app_type.as_str()))
|
||||
.map(|config| config.enabled)
|
||||
.unwrap_or(false);
|
||||
|
||||
let has_live_backup =
|
||||
futures::executor::block_on(state.db.get_live_backup(app_type.as_str()))
|
||||
.ok()
|
||||
@@ -1815,7 +1825,9 @@ impl ProviderService {
|
||||
.proxy_service
|
||||
.detect_takeover_in_live_config_for_app(&app_type);
|
||||
|
||||
if takeover_enabled && (has_live_backup || live_taken_over) {
|
||||
// See the save path above: backup/placeholders are the ownership signal
|
||||
// here, not just proxy_config.enabled.
|
||||
if has_live_backup || live_taken_over {
|
||||
if matches!(app_type, AppType::ClaudeDesktop) {
|
||||
write_live_with_common_config(state.db.as_ref(), &app_type, provider)?;
|
||||
return Ok(());
|
||||
|
||||
Reference in New Issue
Block a user