-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsettings.rs
More file actions
211 lines (194 loc) · 7.51 KB
/
Copy pathsettings.rs
File metadata and controls
211 lines (194 loc) · 7.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
//! 用户设置持久化 — settings.json(app_data_dir)
use crate::utils::logging;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};
static STORE: OnceLock<Mutex<SettingsStore>> = OnceLock::new();
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct AppSettings {
/// 瞬断判定阈值(毫秒):断连到此时间内重连视为瞬断
pub transient_threshold_ms: u64,
/// 托盘告警态自动恢复时长(秒)
pub tray_recovery_secs: u64,
/// 历史 JSON 最大保留条数
pub max_history_entries: usize,
/// 前端 Timeline 最大显示条数
pub timeline_display_max: usize,
/// Timeline 排序:desc = 最新在前,asc = 最早在前
pub timeline_order: String,
/// 主窗口隐藏时发送 Windows 原生 Toast
pub native_notifications: bool,
/// 登录 Windows 时自动启动
pub launch_at_startup: bool,
/// 关闭主窗口时驻留托盘(false = 完全退出)
pub close_to_tray: bool,
/// 启动时请求管理员权限(UAC 提升)
pub run_as_admin: bool,
/// 显示设备实例、服务状态和错误码等底层信息
pub advanced_display: bool,
/// 蓝牙 WMI 轮询间隔(秒),空闲时降低 CPU 占用
pub bluetooth_poll_secs: u64,
/// 全面体检中每个面板允许等待的时间(秒)
pub full_scan_timeout_secs: u64,
/// PowerShell / WMI 系统查询允许等待的时间(秒)
pub system_query_timeout_secs: u64,
/// 网络测速整体超时(秒)
pub network_test_timeout_secs: u64,
/// WinDbg / DbgEng 单个蓝屏转储分析超时(秒)
pub bsod_debugger_timeout_secs: u64,
/// 界面语言(BCP 47,如 zh-CN、en)
pub locale: String,
#[serde(default)]
pub locale_auto_configured: bool,
}
impl Default for AppSettings {
fn default() -> Self {
Self {
transient_threshold_ms: 500,
tray_recovery_secs: 45,
max_history_entries: 500,
timeline_display_max: 80,
timeline_order: "desc".into(),
native_notifications: true,
launch_at_startup: false,
close_to_tray: true,
run_as_admin: false,
advanced_display: false,
bluetooth_poll_secs: 60,
full_scan_timeout_secs: 25,
system_query_timeout_secs: 20,
network_test_timeout_secs: 20,
bsod_debugger_timeout_secs: 90,
locale: "en".into(),
locale_auto_configured: false,
}
}
}
impl AppSettings {
pub fn validate(&self) -> Result<(), String> {
if !(100..=10_000).contains(&self.transient_threshold_ms) {
return Err("瞬断阈值须在 100–10000 ms 之间".into());
}
if !(5..=600).contains(&self.tray_recovery_secs) {
return Err("托盘恢复时长须在 5–600 秒之间".into());
}
if !(50..=2000).contains(&self.max_history_entries) {
return Err("历史条数须在 50–2000 之间".into());
}
if !(10..=500).contains(&self.timeline_display_max) {
return Err("Timeline 显示条数须在 10–500 之间".into());
}
if !matches!(self.timeline_order.as_str(), "desc" | "asc") {
return Err("Timeline 排序方式无效".into());
}
if !(15..=300).contains(&self.bluetooth_poll_secs) {
return Err("蓝牙轮询间隔须在 15–300 秒之间".into());
}
if !(10..=120).contains(&self.full_scan_timeout_secs) {
return Err("全面体检单项超时须在 10–120 秒之间".into());
}
if !(5..=120).contains(&self.system_query_timeout_secs) {
return Err("Windows 系统查询超时须在 5–120 秒之间".into());
}
if !(10..=120).contains(&self.network_test_timeout_secs) {
return Err("网络测速超时须在 10–120 秒之间".into());
}
if !(30..=300).contains(&self.bsod_debugger_timeout_secs) {
return Err("蓝屏调试分析超时须在 30–300 秒之间".into());
}
if !crate::i18n::is_supported(&self.locale) {
return Err(format!("不支持的语言: {}", self.locale));
}
Ok(())
}
}
struct SettingsStore {
path: PathBuf,
current: AppSettings,
}
pub fn init(path: PathBuf) -> Result<(), String> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| format!("创建设置目录失败: {e}"))?;
}
let current = if path.exists() {
match load_from_file(&path).and_then(|settings| {
settings.validate()?;
Ok(settings)
}) {
Ok(settings) => settings,
Err(error) => {
// Settings are local preferences, not diagnostic evidence. A partial
// write or an obsolete invalid value must not prevent the repair tool
// from starting. Keep the original file for inspection and use safe
// defaults until the user saves settings again.
logging::error(format!(
"设置文件无效,当前会话使用默认设置,原文件保持不变: {error}"
));
AppSettings::default()
}
}
} else {
AppSettings::default()
};
let _ = STORE.set(Mutex::new(SettingsStore { path, current }));
Ok(())
}
pub fn get() -> AppSettings {
STORE
.get()
.and_then(|m| m.lock().ok())
.map(|s| s.current.clone())
.unwrap_or_default()
}
pub fn save(mut settings: AppSettings) -> Result<AppSettings, String> {
settings.locale = crate::i18n::normalize_locale(&settings.locale);
settings.validate()?;
let mutex = STORE.get().ok_or("设置存储未初始化")?;
let mut store = mutex.lock().map_err(|_| "设置存储锁失败")?;
if store.current == settings {
return Ok(store.current.clone());
}
persist(&store.path, &settings)?;
store.current = settings;
Ok(store.current.clone())
}
fn load_from_file(path: &PathBuf) -> Result<AppSettings, String> {
let raw = fs::read_to_string(path).map_err(|e| format!("读取设置失败: {e}"))?;
if raw.trim().is_empty() {
return Ok(AppSettings::default());
}
serde_json::from_str(&raw).map_err(|e| format!("解析设置 JSON 失败: {e}"))
}
fn persist(path: &PathBuf, settings: &AppSettings) -> Result<(), String> {
let json =
serde_json::to_string_pretty(settings).map_err(|e| format!("序列化设置失败: {e}"))?;
fs::write(path, json).map_err(|e| format!("写入设置失败: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_settings_are_valid() {
AppSettings::default().validate().unwrap();
}
#[test]
fn rejects_invalid_threshold() {
let s = AppSettings {
transient_threshold_ms: 50,
..AppSettings::default()
};
assert!(s.validate().is_err());
}
#[test]
fn older_settings_receive_new_scan_defaults() {
let settings: AppSettings = serde_json::from_str(r#"{"locale":"zh-CN"}"#).unwrap();
assert_eq!(settings.full_scan_timeout_secs, 25);
assert_eq!(settings.system_query_timeout_secs, 20);
assert_eq!(settings.network_test_timeout_secs, 20);
assert_eq!(settings.bsod_debugger_timeout_secs, 90);
assert_eq!(settings.timeline_order, "desc");
settings.validate().unwrap();
}
}