修复配置覆盖层写入失败后的部分更新
提前序列化配置变更并在多文件写入失败时逆序回滚 保留单文件保存路径且不增加外部诊断或保存后回读 补充覆盖层写入失败回归测试及配置保存文档
This commit is contained in:
@@ -2040,7 +2040,7 @@ fn persist_game_creator_app_config(
|
||||
let config = normalize_game_creator_app_config(config)?;
|
||||
let path = writable_game_creator_config_path()?;
|
||||
let content = serialize_game_creator_app_config_for_renderer_write(&config)?;
|
||||
write_game_creator_config_atomically(&path, &format!("{content}\n"))?;
|
||||
let mut writes = vec![(path, format!("{content}\n"))];
|
||||
let saved: serde_json::Value = serde_json::from_str(&content)
|
||||
.map_err(|error| format!("解析已序列化客户端配置失败:{error}"))?;
|
||||
for (overlay_path, mut overlay) in overlays {
|
||||
@@ -2062,9 +2062,10 @@ fn persist_game_creator_app_config(
|
||||
if overlay != previous {
|
||||
let content = serde_json::to_string_pretty(&overlay)
|
||||
.map_err(|error| format!("序列化客户端覆盖配置失败:{error}"))?;
|
||||
write_game_creator_config_atomically(&overlay_path, &format!("{content}\n"))?;
|
||||
writes.push((overlay_path, format!("{content}\n")));
|
||||
}
|
||||
}
|
||||
crate::config::write_game_creator_config_batch(&writes)?;
|
||||
// `config` is already normalized and is exactly what was persisted.
|
||||
// Avoid reloading it here: a reload repeats the Windows private-path and
|
||||
// ACL checks and made saving the settings panel appear to hang.
|
||||
|
||||
@@ -3678,6 +3678,45 @@ fn game_creator_config_backup_path(path: &Path) -> PathBuf {
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn write_game_creator_config_batch(writes: &[(PathBuf, String)]) -> Result<(), String> {
|
||||
if writes.len() == 1 {
|
||||
return write_game_creator_config_atomically(&writes[0].0, &writes[0].1);
|
||||
}
|
||||
let originals = writes
|
||||
.iter()
|
||||
.map(|(path, _)| read_game_creator_config_file(path))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
for (index, (path, content)) in writes.iter().enumerate() {
|
||||
if let Err(mut error) = write_game_creator_config_atomically(path, content) {
|
||||
// 写入可能在替换后的权限检查失败,因此失败目标也需要核对并恢复。
|
||||
for rollback_index in (0..=index).rev() {
|
||||
let path = &writes[rollback_index].0;
|
||||
let original = &originals[rollback_index];
|
||||
if read_game_creator_config_file(path).ok().as_ref() == Some(original) {
|
||||
continue;
|
||||
}
|
||||
let restored = match original {
|
||||
Some(content) => write_game_creator_config_atomically(path, content),
|
||||
None => fs::remove_file(path)
|
||||
.or_else(|error| {
|
||||
if error.kind() == std::io::ErrorKind::NotFound {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(error)
|
||||
}
|
||||
})
|
||||
.map_err(|error| error.to_string()),
|
||||
};
|
||||
if let Err(restore_error) = restored {
|
||||
error.push_str(&format!(";恢复配置失败:{}: {restore_error}", path.display()));
|
||||
}
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn write_game_creator_config_atomically(
|
||||
path: &Path,
|
||||
content: &str,
|
||||
|
||||
@@ -807,6 +807,36 @@ fn app_config_commands_write_runtime_config_file() {
|
||||
fs::remove_dir_all(root).expect("cleanup runtime config dir");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_config_batch_restores_main_when_overlay_write_fails() {
|
||||
for main_exists in [false, true] {
|
||||
let root = unique_project_path();
|
||||
fs::create_dir_all(&root).expect("config dir");
|
||||
let main = root.join(GAME_CREATOR_CONFIG_FILE_NAME);
|
||||
let overlay = root.join(GAME_CREATOR_LOCAL_CONFIG_FILE_NAME);
|
||||
let original = "{\"llm\":{\"stream\":false}}\n";
|
||||
if main_exists {
|
||||
fs::write(&main, original).expect("main config");
|
||||
}
|
||||
fs::write(&overlay, original).expect("overlay config");
|
||||
// 普通目录占据备份路径,让覆盖文件在替换前失败。
|
||||
fs::create_dir(root.join(format!(".{}.previous", GAME_CREATOR_LOCAL_CONFIG_FILE_NAME)))
|
||||
.expect("block overlay replacement");
|
||||
crate::config::write_game_creator_config_batch(&[
|
||||
(main.clone(), "{\"llm\":{\"stream\":true}}\n".to_string()),
|
||||
(overlay.clone(), "{\"llm\":{\"stream\":true}}\n".to_string()),
|
||||
])
|
||||
.expect_err("overlay replacement must fail");
|
||||
if main_exists {
|
||||
assert_eq!(fs::read_to_string(&main).expect("restored main"), original);
|
||||
} else {
|
||||
assert!(!main.exists());
|
||||
}
|
||||
assert_eq!(fs::read_to_string(&overlay).expect("unchanged overlay"), original);
|
||||
fs::remove_dir_all(root).expect("cleanup config dir");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_config_save_updates_conflicting_local_overlay() {
|
||||
let root = unique_project_path();
|
||||
|
||||
Reference in New Issue
Block a user