Skip to content

Commit ee474ca

Browse files
committed
fix(linux): 增加白屏诊断日志
说明: - 支持通过 AQBOT_LOG_FILE 将 tracing 诊断写入指定文件。 - 记录 Linux/Wayland/WebKit、数据库和主窗口启动关键节点。 - 增加前端启动错误兜底面板和诊断日志写入命令。 - 将应用版本提升到 0.0.76,保留 Linux WebKit 兼容尝试。 验证: - cargo fmt --manifest-path src-tauri/Cargo.toml --check - cargo test --manifest-path src-tauri/Cargo.toml -p aqbot - cargo test --manifest-path src-tauri/Cargo.toml -p aqbot diagnostics - pnpm vitest run src/lib/__tests__/startupDiagnostics.test.ts - pnpm typecheck - pnpm build - git diff --check Refs #83
1 parent 09e900f commit ee474ca

9 files changed

Lines changed: 572 additions & 14 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "aqbot",
33
"private": true,
4-
"version": "0.0.75",
4+
"version": "0.0.76",
55
"license": "AGPL-3.0-only",
66
"packageManager": "pnpm@10.32.1",
77
"type": "module",

src-tauri/src/commands/desktop.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,32 @@ pub async fn open_devtools(webview_window: tauri::WebviewWindow) -> Result<(), S
100100
Ok(())
101101
}
102102

103+
#[tauri::command]
104+
pub async fn write_diagnostic_log(level: String, message: String) -> Result<(), String> {
105+
let message = truncate_diagnostic_message(&message);
106+
match level.trim().to_ascii_lowercase().as_str() {
107+
"trace" => tracing::trace!(target: "aqbot_frontend", "{}", message),
108+
"debug" => tracing::debug!(target: "aqbot_frontend", "{}", message),
109+
"warn" | "warning" => tracing::warn!(target: "aqbot_frontend", "{}", message),
110+
"error" => tracing::error!(target: "aqbot_frontend", "{}", message),
111+
_ => tracing::info!(target: "aqbot_frontend", "{}", message),
112+
}
113+
Ok(())
114+
}
115+
116+
fn truncate_diagnostic_message(message: &str) -> String {
117+
const MAX_DIAGNOSTIC_MESSAGE_LEN: usize = 8 * 1024;
118+
let mut result = String::with_capacity(message.len().min(MAX_DIAGNOSTIC_MESSAGE_LEN));
119+
for ch in message.chars() {
120+
if result.len() + ch.len_utf8() > MAX_DIAGNOSTIC_MESSAGE_LEN {
121+
result.push_str("...<truncated>");
122+
break;
123+
}
124+
result.push(ch);
125+
}
126+
result
127+
}
128+
103129
#[tauri::command]
104130
pub async fn test_proxy(
105131
proxy_type: String,

src-tauri/src/diagnostics.rs

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
use std::env;
2+
use std::ffi::OsString;
3+
use std::fs::{self, File, OpenOptions};
4+
use std::io::{self, Write};
5+
use std::path::{Path, PathBuf};
6+
use std::sync::{Arc, Mutex};
7+
use tracing_subscriber::fmt::MakeWriter;
8+
9+
pub const LOG_FILE_ENV: &str = "AQBOT_LOG_FILE";
10+
11+
#[derive(Clone)]
12+
struct SharedLogFile {
13+
file: Arc<Mutex<File>>,
14+
}
15+
16+
struct SharedLogFileWriter {
17+
file: Arc<Mutex<File>>,
18+
}
19+
20+
impl SharedLogFile {
21+
fn new(file: File) -> Self {
22+
Self {
23+
file: Arc::new(Mutex::new(file)),
24+
}
25+
}
26+
}
27+
28+
impl Write for SharedLogFileWriter {
29+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
30+
let mut file = self
31+
.file
32+
.lock()
33+
.map_err(|_| io::Error::new(io::ErrorKind::Other, "log file lock poisoned"))?;
34+
file.write(buf)
35+
}
36+
37+
fn flush(&mut self) -> io::Result<()> {
38+
let mut file = self
39+
.file
40+
.lock()
41+
.map_err(|_| io::Error::new(io::ErrorKind::Other, "log file lock poisoned"))?;
42+
file.flush()
43+
}
44+
}
45+
46+
impl<'writer> MakeWriter<'writer> for SharedLogFile {
47+
type Writer = SharedLogFileWriter;
48+
49+
fn make_writer(&'writer self) -> Self::Writer {
50+
SharedLogFileWriter {
51+
file: Arc::clone(&self.file),
52+
}
53+
}
54+
}
55+
56+
pub fn init_tracing() {
57+
let log_path = log_file_path_from_value(env::var_os(LOG_FILE_ENV));
58+
59+
if let Some(path) = log_path {
60+
match open_log_file(&path) {
61+
Ok(file) => {
62+
if let Err(err) = tracing_subscriber::fmt()
63+
.with_env_filter(env_filter())
64+
.with_writer(SharedLogFile::new(file))
65+
.try_init()
66+
{
67+
eprintln!("failed to initialize AQBot file logging: {err}");
68+
return;
69+
}
70+
tracing::info!(
71+
log_file = %path.display(),
72+
"AQBot diagnostic file logging enabled"
73+
);
74+
return;
75+
}
76+
Err(err) => {
77+
eprintln!(
78+
"failed to open AQBot diagnostic log file '{}': {err}",
79+
path.display()
80+
);
81+
init_stderr_tracing();
82+
tracing::warn!(
83+
log_file = %path.display(),
84+
error = %err,
85+
"Falling back to stderr logging because diagnostic log file could not be opened"
86+
);
87+
return;
88+
}
89+
}
90+
}
91+
92+
init_stderr_tracing();
93+
}
94+
95+
pub fn log_process_startup() {
96+
tracing::info!(
97+
package_name = env!("CARGO_PKG_NAME"),
98+
crate_version = env!("CARGO_PKG_VERSION"),
99+
os = env::consts::OS,
100+
arch = env::consts::ARCH,
101+
rust_log = %env_value("RUST_LOG"),
102+
aqbot_log_file = %env_value(LOG_FILE_ENV),
103+
xdg_session_type = %env_value("XDG_SESSION_TYPE"),
104+
wayland_display = %env_value("WAYLAND_DISPLAY"),
105+
display = %env_value("DISPLAY"),
106+
gdk_backend = %env_value("GDK_BACKEND"),
107+
"AQBot process startup diagnostics"
108+
);
109+
}
110+
111+
fn init_stderr_tracing() {
112+
if let Err(err) = tracing_subscriber::fmt()
113+
.with_env_filter(env_filter())
114+
.try_init()
115+
{
116+
eprintln!("failed to initialize AQBot stderr logging: {err}");
117+
}
118+
}
119+
120+
fn env_filter() -> tracing_subscriber::EnvFilter {
121+
tracing_subscriber::EnvFilter::try_from_default_env()
122+
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"))
123+
}
124+
125+
fn env_value(key: &str) -> String {
126+
env::var(key).unwrap_or_else(|_| "<unset>".to_string())
127+
}
128+
129+
fn log_file_path_from_value(value: Option<OsString>) -> Option<PathBuf> {
130+
let value = value?;
131+
if value.to_string_lossy().trim().is_empty() {
132+
return None;
133+
}
134+
Some(PathBuf::from(value))
135+
}
136+
137+
fn open_log_file(path: &Path) -> io::Result<File> {
138+
if let Some(parent) = path
139+
.parent()
140+
.filter(|parent| !parent.as_os_str().is_empty())
141+
{
142+
fs::create_dir_all(parent)?;
143+
}
144+
OpenOptions::new().create(true).append(true).open(path)
145+
}
146+
147+
#[cfg(test)]
148+
mod tests {
149+
use super::{log_file_path_from_value, open_log_file};
150+
use std::ffi::OsString;
151+
use std::io::Write;
152+
153+
#[test]
154+
fn ignores_missing_or_blank_log_file_env() {
155+
assert_eq!(log_file_path_from_value(None), None);
156+
assert_eq!(log_file_path_from_value(Some(OsString::from(" "))), None);
157+
}
158+
159+
#[test]
160+
fn keeps_non_blank_log_file_path() {
161+
assert_eq!(
162+
log_file_path_from_value(Some(OsString::from("/tmp/aqbot.log"))),
163+
Some(std::path::PathBuf::from("/tmp/aqbot.log"))
164+
);
165+
}
166+
167+
#[test]
168+
fn opens_log_file_and_creates_parent_directories() {
169+
let temp_dir = tempfile::tempdir().expect("tempdir");
170+
let log_path = temp_dir.path().join("nested").join("aqbot.log");
171+
172+
{
173+
let mut file = open_log_file(&log_path).expect("open log file");
174+
writeln!(file, "first").expect("write first log line");
175+
}
176+
{
177+
let mut file = open_log_file(&log_path).expect("reopen log file");
178+
writeln!(file, "second").expect("write second log line");
179+
}
180+
181+
let contents = std::fs::read_to_string(log_path).expect("read log file");
182+
assert!(contents.contains("first"));
183+
assert!(contents.contains("second"));
184+
}
185+
}

src-tauri/src/lib.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@ pub struct AppState {
4040

4141
mod commands;
4242
mod context_manager;
43+
mod diagnostics;
4344
mod indexing;
4445
pub mod knowledge_index_scheduler;
46+
#[cfg(any(target_os = "linux", test))]
47+
mod linux_webkit;
4548
mod paths;
4649
mod tray;
4750
mod window_lifecycle;
@@ -53,12 +56,11 @@ mod windows_utils;
5356
#[cfg_attr(mobile, tauri::mobile_entry_point)]
5457
pub fn run() {
5558
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
56-
tracing_subscriber::fmt()
57-
.with_env_filter(
58-
tracing_subscriber::EnvFilter::try_from_default_env()
59-
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
60-
)
61-
.init();
59+
diagnostics::init_tracing();
60+
diagnostics::log_process_startup();
61+
62+
#[cfg(target_os = "linux")]
63+
linux_webkit::apply_startup_workarounds();
6264

6365
#[allow(unused_mut)]
6466
let mut builder = tauri::Builder::default()
@@ -280,6 +282,7 @@ pub fn run() {
280282
commands::desktop::apply_startup_settings,
281283
commands::desktop::test_proxy,
282284
commands::desktop::open_devtools,
285+
commands::desktop::write_diagnostic_log,
283286
commands::desktop::list_system_fonts,
284287
commands::desktop::minimize_window,
285288
commands::desktop::toggle_maximize_window,
@@ -360,12 +363,19 @@ pub fn run() {
360363
// %USERPROFILE%\.aqbot\ on Windows).
361364
let app_dir = paths::aqbot_home();
362365
std::fs::create_dir_all(&app_dir).expect("failed to create AQBot home dir");
366+
tracing::info!(
367+
app_dir = %app_dir.display(),
368+
version = %app.package_info().version,
369+
"AQBot setup started"
370+
);
363371

364372
// Ensure ~/Documents/aqbot/{images,files,backups}/ exist
365373
aqbot_core::storage_paths::ensure_documents_dirs()
366374
.expect("failed to create documents storage dirs");
375+
tracing::info!("AQBot documents directories ensured");
367376

368377
let db_path = format!("sqlite:{}/aqbot.db", app_dir.display());
378+
tracing::info!(db_path = %db_path, "AQBot database path resolved");
369379

370380
// Load or generate master key BEFORE opening the database.
371381
// db::create_pool uses SQLite create mode, which would create aqbot.db
@@ -384,6 +394,7 @@ pub fn run() {
384394
key.copy_from_slice(&bytes);
385395
// Securely clear the temporary buffer
386396
bytes.iter_mut().for_each(|b| *b = 0);
397+
tracing::info!("AQBot master key loaded");
387398
key
388399
} else {
389400
// Safety guard: refuse to generate a new key when an existing database is
@@ -414,6 +425,7 @@ pub fn run() {
414425
std::fs::set_permissions(&key_path, perms)
415426
.expect("failed to set master.key permissions");
416427
}
428+
tracing::info!("AQBot master key generated");
417429
key
418430
};
419431

@@ -449,6 +461,7 @@ pub fn run() {
449461
std::process::exit(1);
450462
}
451463
};
464+
tracing::info!("AQBot database pool initialized");
452465

453466
// Initialize vector store (shares the sea-orm SQLite connection)
454467
let vector_store =
@@ -505,7 +518,11 @@ pub fn run() {
505518
}
506519

507520
if let Some(main_window) = app.get_webview_window("main") {
521+
tracing::info!("AQBot main window found during setup");
508522
window_lifecycle::configure_main_window(app.handle(), &main_window);
523+
tracing::info!("AQBot main window configured");
524+
} else {
525+
tracing::warn!("AQBot main window was not found during setup");
509526
}
510527

511528
// Initialize auto-backup scheduler if enabled

0 commit comments

Comments
 (0)