Evan's Blog
EvanZhu0721 / 公开仓库

SBMS

镜像已同步

SBMS complete the calculation rules for the default logical desktop topology relationship and the physical dimensions of the displays in the Windows multi-screen collaboration system.

上游 GitHub
READ-ONLY MIRROR

README、源码与 Release 已保存到本站;源码视图固定到 commit bd87fa1e。

main/ root / src/ui
bd87fa1e· 固定源码快照
src/ui/diagnostic_view.rs6.1 KB
1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4
5use crate::diagnostics::{self, Level, Record};
6
7use super::QuickAccess;
8
9pub(super) fn populate_diagnostics(ui: &QuickAccess) {
10 let (records, read_error) = match diagnostics::persisted_recent() {
11 Ok(records) => (records, None),
12 Err(error) => (diagnostics::recent(), Some(error)),
13 };
14 ui.set_diagnostic_summary(format_last_error(&records).into());
15 ui.set_diagnostic_text(format_recent_diagnostics(&records).into());
16 let path = diagnostics::default_log_path()
17 .map(|path| path.display().to_string())
18 .unwrap_or_else(|error| format!("Log path unavailable: {error}"));
19 ui.set_diagnostic_path(path.into());
20 ui.set_diagnostic_action_error(
21 read_error
22 .map(|error| format!("Couldn’t read the log file; showing this process only: {error}"))
23 .unwrap_or_default()
24 .into(),
25 );
26}
27
28pub(super) fn format_recent_diagnostics(records: &[Record]) -> String {
29 format_recent_diagnostics_at(records)
30}
31
32fn format_recent_diagnostics_at(records: &[Record]) -> String {
33 let mut selected = records.iter().collect::<Vec<_>>();
34 selected.sort_by_key(|record| std::cmp::Reverse(record.time));
35 selected.truncate(40);
36 if selected.is_empty() {
37 return "No diagnostic records are available.".into();
38 }
39
40 selected
41 .into_iter()
42 .map(|record| {
43 let local_time = if record.local_time.is_empty() {
44 diagnostics::local_time_string(record.time)
45 .unwrap_or_else(|| record.time.to_string())
46 } else {
47 record.local_time.clone()
48 };
49 let session = if record.mapping_session_id == "-" {
50 String::new()
51 } else {
52 format!(" · {}", record.mapping_session_id)
53 };
54 let message = record.message.replace(['\r', '\n'], " ");
55 format!(
56 "[{local_time}] {} · {}/{}{}\n{}",
57 diagnostic_level(record),
58 record.module,
59 record.stage,
60 session,
61 message
62 )
63 })
64 .collect::<Vec<_>>()
65 .join("\n\n")
66}
67
68fn format_last_error(records: &[Record]) -> String {
69 records
70 .iter()
71 .filter(|record| record.level == Level::Error)
72 .max_by_key(|record| record.time)
73 .map(|record| {
74 format!(
75 "{} / {}: {}",
76 record.module,
77 record.stage,
78 record.message.replace(['\r', '\n'], " ")
79 )
80 })
81 .unwrap_or_default()
82}
83
84fn diagnostic_level(record: &Record) -> &'static str {
85 match record.level {
86 Level::Debug => "DEBUG",
87 Level::Info => "INFO",
88 Level::Warn => "WARN",
89 Level::Error => "ERROR",
90 }
91}
92
93pub(super) fn open_log_folder() -> Result<(), String> {
94 let path = diagnostics::default_log_path()
95 .map_err(|error| format!("Couldn’t locate the log folder: {error}"))?;
96 let folder = ensure_log_folder(&path)?;
97 Command::new("explorer.exe")
98 .arg(folder)
99 .spawn()
100 .map(|_| ())
101 .map_err(|error| format!("Couldn’t open the log folder: {error}"))
102}
103
104fn ensure_log_folder(log_path: &Path) -> Result<PathBuf, String> {
105 let folder = log_path
106 .parent()
107 .filter(|folder| !folder.as_os_str().is_empty())
108 .ok_or_else(|| "The diagnostic log path has no parent folder".to_string())?;
109 fs::create_dir_all(folder).map_err(|error| {
110 format!(
111 "Couldn’t create the log folder at {}: {error}",
112 folder.display()
113 )
114 })?;
115 Ok(folder.to_path_buf())
116}
117
118#[cfg(test)]
119mod tests {
120 use std::sync::atomic::{AtomicUsize, Ordering};
121
122 use super::*;
123
124 static NEXT_TEMP_DIRECTORY: AtomicUsize = AtomicUsize::new(1);
125
126 fn record(time: u64, level: Level, session: &str, message: &str) -> Record {
127 Record {
128 time,
129 local_time: String::new(),
130 level,
131 module: "mapping".into(),
132 stage: "test".into(),
133 mapping_session_id: session.into(),
134 message: message.into(),
135 }
136 }
137
138 #[test]
139 fn recent_activity_keeps_newer_sessions_after_an_error() {
140 let records = [
141 record(1_000, Level::Error, "1-1", "failed"),
142 record(2_000, Level::Info, "1-2", "recovered"),
143 ];
144
145 let text = format_recent_diagnostics_at(&records);
146
147 assert!(text.contains("failed"));
148 assert!(text.contains("recovered"));
149 assert!(text.find("recovered").unwrap() < text.find("failed").unwrap());
150 }
151
152 #[test]
153 fn last_error_is_empty_without_an_error() {
154 assert_eq!(
155 format_last_error(&[record(1_000, Level::Info, "1-1", "healthy")]),
156 ""
157 );
158 }
159
160 #[test]
161 fn last_error_uses_the_latest_error_across_sessions() {
162 let records = [
163 record(1_000, Level::Error, "1-1", "first"),
164 record(2_000, Level::Info, "1-2", "healthy"),
165 record(3_000, Level::Error, "1-3", "latest"),
166 ];
167
168 assert_eq!(format_last_error(&records), "mapping / test: latest");
169 }
170
171 #[test]
172 fn ensure_log_folder_creates_the_directory_containing_the_log() {
173 let sequence = NEXT_TEMP_DIRECTORY.fetch_add(1, Ordering::Relaxed);
174 let test_root = std::env::temp_dir().join(format!(
175 "sbms-open-log-folder-{}-{sequence}",
176 std::process::id()
177 ));
178 let log_path = test_root.join("SBMS").join("logs").join("sbms.log");
179
180 let folder = ensure_log_folder(&log_path).expect("log folder should be created");
181
182 assert_eq!(folder, test_root.join("SBMS").join("logs"));
183 assert!(folder.is_dir());
184
185 fs::remove_dir_all(&test_root).expect("temporary log folder should be removable");
186 }
187
188 #[test]
189 fn ensure_log_folder_rejects_a_path_without_a_directory() {
190 let error = ensure_log_folder(Path::new("sbms.log"))
191 .expect_err("a bare file name has no log folder");
192
193 assert_eq!(error, "The diagnostic log path has no parent folder");
194 }
195}
196