Skip to content

Commit 90f602a

Browse files
committed
Keep monitoring responsive and save configuration atomically
Isolate native sampling from slow GPU and settings queries with bounded workers, monotonic scheduling, refresh coalescing, and interruptible waits. Publish only each source's fields and clear unavailable GPU samples. Write configuration to a unique sibling file and flush before replacement so failed writes do not truncate the last valid configuration. Tested: 47 Linux debug and release tests, including 7 new regressions Tested: monitor concurrency regressions repeated 20 times Tested: cargo fmt and Linux release build (eframe/x11 enabled for Linux) Tested: Windows GNU all-targets cargo check and clippy Windows GUI, packaging, and real game performance were not run locally.
1 parent b34bdad commit 90f602a

2 files changed

Lines changed: 445 additions & 30 deletions

File tree

‎src/config/settings.rs‎

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use serde::{Deserialize, Serialize};
22
use std::collections::HashSet;
3+
use std::io::Write;
34
use std::path::{Path, PathBuf};
5+
use std::sync::atomic::{AtomicU64, Ordering};
46
use std::sync::OnceLock;
57

68
static CONFIG_DIRECTORY: OnceLock<PathBuf> = OnceLock::new();
@@ -113,7 +115,7 @@ impl AppConfig {
113115
std::fs::create_dir_all(parent)
114116
.map_err(|e| format!("无法创建配置目录({}):{}", parent.display(), e))?;
115117
}
116-
std::fs::write(path, content)
118+
atomic_write(path, content.as_bytes())
117119
.map_err(|e| format!("无法保存配置({}):{}", path.display(), e))
118120
}
119121

@@ -126,10 +128,115 @@ impl AppConfig {
126128
}
127129
}
128130

131+
/// Keep the previous config intact until the complete new file is flushed.
132+
/// A sibling temporary file keeps the replacement on the same filesystem;
133+
/// std::fs::rename replaces an existing file on both Windows and Unix.
134+
fn atomic_write(path: &Path, content: &[u8]) -> std::io::Result<()> {
135+
atomic_write_with(path, |file| file.write_all(content))
136+
}
137+
138+
fn atomic_write_with(
139+
path: &Path,
140+
write: impl FnOnce(&mut std::fs::File) -> std::io::Result<()>,
141+
) -> std::io::Result<()> {
142+
static NEXT_TEMP: AtomicU64 = AtomicU64::new(0);
143+
let name = path.file_name().ok_or_else(|| {
144+
std::io::Error::new(std::io::ErrorKind::InvalidInput, "配置文件名不能为空")
145+
})?;
146+
// create_new never truncates a stale temporary file or another instance's
147+
// save. Retry collisions, but fail clearly rather than looping forever.
148+
let (mut file, temporary) = (0..100)
149+
.find_map(|_| {
150+
let mut temporary_name = name.to_os_string();
151+
temporary_name.push(format!(
152+
".{}.{}.tmp",
153+
std::process::id(),
154+
NEXT_TEMP.fetch_add(1, Ordering::Relaxed)
155+
));
156+
let temporary = path.with_file_name(temporary_name);
157+
match std::fs::OpenOptions::new()
158+
.write(true)
159+
.create_new(true)
160+
.open(&temporary)
161+
{
162+
Ok(file) => Some(Ok((file, temporary))),
163+
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => None,
164+
Err(error) => Some(Err(error)),
165+
}
166+
})
167+
.unwrap_or_else(|| {
168+
Err(std::io::Error::new(
169+
std::io::ErrorKind::AlreadyExists,
170+
"无法创建唯一的临时配置文件",
171+
))
172+
})?;
173+
let result = write(&mut file).and_then(|_| file.sync_all());
174+
// Windows replacement must not leave this process's temporary handle open.
175+
drop(file);
176+
let result = result.and_then(|_| std::fs::rename(&temporary, path));
177+
if result.is_err() {
178+
let _ = std::fs::remove_file(&temporary);
179+
}
180+
result
181+
}
182+
129183
#[cfg(test)]
130184
mod tests {
131185
use super::*;
132186

187+
fn temp_directory(label: &str) -> PathBuf {
188+
static NEXT: AtomicU64 = AtomicU64::new(0);
189+
let path = std::env::temp_dir().join(format!(
190+
"game-accelerator-{label}-{}-{}",
191+
std::process::id(),
192+
NEXT.fetch_add(1, Ordering::Relaxed)
193+
));
194+
std::fs::create_dir(&path).unwrap();
195+
path
196+
}
197+
198+
#[test]
199+
fn atomic_save_creates_and_replaces_config_in_unicode_directory() {
200+
let directory = temp_directory("配置保存");
201+
let path = directory.join("config.toml");
202+
let mut config = AppConfig::default();
203+
config.save_to(&path).unwrap();
204+
assert_eq!(AppConfig::load_from(&path).unwrap(), config);
205+
config.select_competitive_game("League of Legends.exe");
206+
config.save_to(&path).unwrap();
207+
assert_eq!(AppConfig::load_from(&path).unwrap(), config);
208+
assert_eq!(std::fs::read_dir(&directory).unwrap().count(), 1);
209+
std::fs::remove_dir_all(directory).unwrap();
210+
}
211+
212+
#[test]
213+
fn interrupted_write_keeps_previous_config_and_removes_temporary_file() {
214+
let directory = temp_directory("failed-write");
215+
let path = directory.join("config.toml");
216+
let original = b"previous valid config";
217+
std::fs::write(&path, original).unwrap();
218+
let result = atomic_write_with(&path, |file| {
219+
file.write_all(b"incomplete replacement")?;
220+
Err(std::io::Error::other("simulated disk failure"))
221+
});
222+
assert!(result.is_err());
223+
assert_eq!(std::fs::read(&path).unwrap(), original);
224+
assert_eq!(std::fs::read_dir(&directory).unwrap().count(), 1);
225+
std::fs::remove_dir_all(directory).unwrap();
226+
}
227+
228+
#[test]
229+
fn failed_replacement_preserves_destination_and_cleans_temporary_file() {
230+
let directory = temp_directory("failed-replace");
231+
let path = directory.join("config.toml");
232+
std::fs::create_dir(&path).unwrap();
233+
std::fs::write(path.join("keep"), b"original").unwrap();
234+
assert!(atomic_write(&path, b"new config").is_err());
235+
assert_eq!(std::fs::read(path.join("keep")).unwrap(), b"original");
236+
assert_eq!(std::fs::read_dir(&directory).unwrap().count(), 1);
237+
std::fs::remove_dir_all(directory).unwrap();
238+
}
239+
133240
#[test]
134241
fn first_run_does_not_manipulate_processes() {
135242
let config = AppConfig::default();

0 commit comments

Comments
 (0)