mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-07 06:04:53 +08:00
refactor: improve file writing and error handling in PluginDataStore
- Replaced asynchronous file operations with a synchronous approach to ensure file handles are properly closed before replacement. - Introduced a new `write_once` function for atomic file writing, including directory creation, temporary file writing, and target file replacement. - Enhanced error handling to retry on transient errors specific to Windows, improving robustness during file operations. - Updated the `clear` method in `PluginRegistry` to ensure OAuth sessions are only removed after successful persistence of resources.
This commit is contained in:
+75
-47
@@ -71,23 +71,44 @@ impl PluginDataStore {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 全程使用同步 IO 在阻塞线程完成:tokio 异步文件的关闭是延迟的,
|
||||||
|
/// 替换前句柄可能仍被本进程持有;同步写入保证替换时句柄已确定关闭。
|
||||||
async fn write_locked(&self, path: &Path, key: &str, value: &serde_json::Value) -> Result<()> {
|
async fn write_locked(&self, path: &Path, key: &str, value: &serde_json::Value) -> Result<()> {
|
||||||
let directory = path.parent().expect("plugin data path has a parent");
|
let directory = path
|
||||||
tokio::fs::create_dir_all(directory).await?;
|
.parent()
|
||||||
set_directory_permissions(directory)?;
|
.expect("plugin data path has a parent")
|
||||||
|
.to_owned();
|
||||||
let temporary = directory.join(format!(".{key}.{}.tmp", uuid::Uuid::new_v4()));
|
let temporary = directory.join(format!(".{key}.{}.tmp", uuid::Uuid::new_v4()));
|
||||||
|
let target = path.to_owned();
|
||||||
let bytes = serde_json::to_vec_pretty(value)?;
|
let bytes = serde_json::to_vec_pretty(value)?;
|
||||||
tokio::fs::write(&temporary, bytes).await?;
|
tokio::task::spawn_blocking(move || {
|
||||||
set_file_permissions(&temporary)?;
|
// Windows 上杀软或索引器会短暂锁住新建文件,任何一步都可能
|
||||||
let file = tokio::fs::OpenOptions::new()
|
// 拒绝访问,因此把整个序列作为一个整体重试。
|
||||||
.read(true)
|
let mut attempts = 0;
|
||||||
.open(&temporary)
|
loop {
|
||||||
.await?;
|
match write_once(&directory, &temporary, &target, &bytes) {
|
||||||
file.sync_all().await?;
|
Ok(()) => return Ok(()),
|
||||||
drop(file);
|
Err((step, error)) if attempts < 20 && transient(&error) => {
|
||||||
replace_file(&temporary, path).await?;
|
attempts += 1;
|
||||||
set_file_permissions(path)?;
|
tracing::debug!(step, attempts, %error, "retrying plugin data write");
|
||||||
Ok(())
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
Err((step, error)) => {
|
||||||
|
let _ = std::fs::remove_file(&temporary);
|
||||||
|
tracing::warn!(
|
||||||
|
path = %target.display(),
|
||||||
|
step,
|
||||||
|
attempts,
|
||||||
|
%error,
|
||||||
|
"plugin data write failed"
|
||||||
|
);
|
||||||
|
return Err(Error::Config(format!("{step}: {error}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("plugin data write task panicked")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn clear(&self, plugin_id: &str) -> Result<()> {
|
pub async fn clear(&self, plugin_id: &str) -> Result<()> {
|
||||||
@@ -120,46 +141,53 @@ impl PluginDataStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 原子替换目标文件。Windows 上 rename 不覆盖已存在文件,且目标可能被
|
/// 单次完整写入:建目录、写临时文件、落盘、原子替换。
|
||||||
/// 杀毒软件或索引器短暂锁定(拒绝访问/共享冲突),需删除后重试。
|
/// 失败时返回失败步骤的标签,供上层区分重试与报错。
|
||||||
async fn replace_file(temporary: &Path, path: &Path) -> Result<()> {
|
fn write_once(
|
||||||
|
directory: &Path,
|
||||||
|
temporary: &Path,
|
||||||
|
target: &Path,
|
||||||
|
bytes: &[u8],
|
||||||
|
) -> std::result::Result<(), (&'static str, std::io::Error)> {
|
||||||
|
use std::io::Write;
|
||||||
|
std::fs::create_dir_all(directory).map_err(|error| ("create data directory", error))?;
|
||||||
|
let _ = set_directory_permissions(directory);
|
||||||
|
let mut file =
|
||||||
|
std::fs::File::create(temporary).map_err(|error| ("create temporary file", error))?;
|
||||||
|
file.write_all(bytes)
|
||||||
|
.map_err(|error| ("write temporary file", error))?;
|
||||||
|
file.sync_all()
|
||||||
|
.map_err(|error| ("sync temporary file", error))?;
|
||||||
|
drop(file);
|
||||||
|
let _ = set_file_permissions(temporary);
|
||||||
|
// Windows 的 rename 不覆盖已存在文件,先删除旧文件。
|
||||||
|
#[cfg(windows)]
|
||||||
|
match std::fs::remove_file(target) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
||||||
|
Err(error) => return Err(("remove previous file", error)),
|
||||||
|
}
|
||||||
|
std::fs::rename(temporary, target).map_err(|error| ("replace target file", error))?;
|
||||||
|
let _ = set_file_permissions(target);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Windows 下拒绝访问(5)与共享冲突(32)通常是杀软或索引器的
|
||||||
|
/// 瞬时锁定,值得重试;其余错误与其他平台一律直接失败。
|
||||||
|
fn transient(error: &std::io::Error) -> bool {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
const ACCESS_DENIED: i32 = 5;
|
const ACCESS_DENIED: i32 = 5;
|
||||||
const SHARING_VIOLATION: i32 = 32;
|
const SHARING_VIOLATION: i32 = 32;
|
||||||
let mut attempts = 0;
|
matches!(
|
||||||
loop {
|
error.raw_os_error(),
|
||||||
if path.exists() {
|
Some(ACCESS_DENIED | SHARING_VIOLATION)
|
||||||
let _ = tokio::fs::remove_file(path).await;
|
)
|
||||||
}
|
|
||||||
match tokio::fs::rename(temporary, path).await {
|
|
||||||
Ok(()) => return Ok(()),
|
|
||||||
Err(error)
|
|
||||||
if attempts < 20
|
|
||||||
&& matches!(
|
|
||||||
error.raw_os_error(),
|
|
||||||
Some(ACCESS_DENIED | SHARING_VIOLATION)
|
|
||||||
) =>
|
|
||||||
{
|
|
||||||
attempts += 1;
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
tracing::warn!(
|
|
||||||
path = %path.display(),
|
|
||||||
attempts,
|
|
||||||
%error,
|
|
||||||
"plugin data file replacement failed"
|
|
||||||
);
|
|
||||||
return Err(error.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
{
|
{
|
||||||
tokio::fs::rename(temporary, path).await?;
|
let _ = error;
|
||||||
Ok(())
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -382,12 +382,13 @@ impl PluginRegistry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
OAuth2Poll::Completed { resources } => {
|
OAuth2Poll::Completed { resources } => {
|
||||||
self.inner.oauth_sessions.lock().await.remove(session_id);
|
// 持久化成功后才销毁会话:写盘瞬时失败时下次轮询还能重试。
|
||||||
let outcome = self
|
let outcome = self
|
||||||
.inner
|
.inner
|
||||||
.state
|
.state
|
||||||
.upsert_resources(&plugin_id, &resource_type, resources)
|
.upsert_resources(&plugin_id, &resource_type, resources)
|
||||||
.await?;
|
.await?;
|
||||||
|
self.inner.oauth_sessions.lock().await.remove(session_id);
|
||||||
let model_sync_error = self
|
let model_sync_error = self
|
||||||
.sync_provider_models_for_resource(&entry, &executable, &resource_type)
|
.sync_provider_models_for_resource(&entry, &executable, &resource_type)
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
Reference in New Issue
Block a user