feat: enhance desktop application startup diagnostics and logging

- Introduced a new `startup` module to capture and manage startup diagnostics.
- Implemented logging functionality with daily rotation and error reporting for application startup failures.
- Updated the main application run function to return an exit code based on startup success or failure.
- Added new dependencies for logging and diagnostics in `Cargo.toml`.
- Increased request timeout in the control service for improved stability.
- Expanded maximum search results in the federation module for better search capabilities.
This commit is contained in:
leookun
2026-08-30 03:19:01 +08:00
parent 44e2d8057a
commit fd7998e43c
24 changed files with 295 additions and 32 deletions
Generated
+114
View File
@@ -97,6 +97,28 @@ dependencies = [
"x11rb",
]
[[package]]
name = "ashpd"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39"
dependencies = [
"async-fs",
"async-net",
"enumflags2",
"futures-channel",
"futures-util",
"rand 0.9.5",
"raw-window-handle",
"serde",
"serde_repr",
"url",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"zbus",
]
[[package]]
name = "asn1-rs"
version = "0.7.2"
@@ -186,6 +208,17 @@ dependencies = [
"slab",
]
[[package]]
name = "async-fs"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5"
dependencies = [
"async-lock",
"blocking",
"futures-lite",
]
[[package]]
name = "async-io"
version = "2.6.0"
@@ -215,6 +248,17 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "async-net"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
dependencies = [
"async-io",
"blocking",
"futures-lite",
]
[[package]]
name = "async-process"
version = "2.5.0"
@@ -1132,6 +1176,7 @@ version = "0.1.5-beta.1"
dependencies = [
"axum",
"cursor-server",
"rfd",
"serde",
"serde_json",
"tauri",
@@ -1145,6 +1190,7 @@ dependencies = [
"tokio",
"tokio-util",
"tracing",
"tracing-appender",
"tracing-subscriber",
"url",
]
@@ -1520,6 +1566,15 @@ dependencies = [
"syn 3.0.3",
]
[[package]]
name = "dlib"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
dependencies = [
"libloading",
]
[[package]]
name = "dlopen2"
version = "0.8.2"
@@ -4555,6 +4610,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "pollster"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]]
name = "portable-atomic"
version = "1.15.0"
@@ -5196,6 +5257,30 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rfd"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed"
dependencies = [
"ashpd",
"block2",
"dispatch2",
"js-sys",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"pollster",
"raw-window-handle",
"urlencoding",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "ring"
version = "0.17.14"
@@ -6289,6 +6374,12 @@ dependencies = [
"serde_json",
]
[[package]]
name = "symlink"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
[[package]]
name = "syn"
version = "1.0.109"
@@ -7262,6 +7353,19 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-appender"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
dependencies = [
"crossbeam-channel",
"symlink",
"thiserror 2.0.20",
"time",
"tracing-subscriber",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
@@ -7636,6 +7740,12 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "urlpattern"
version = "0.3.0"
@@ -7852,6 +7962,7 @@ dependencies = [
"cc",
"downcast-rs",
"rustix",
"scoped-tls",
"smallvec",
"wayland-sys",
]
@@ -7910,6 +8021,8 @@ version = "0.31.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
dependencies = [
"dlib",
"log",
"pkg-config",
]
@@ -9028,6 +9141,7 @@ dependencies = [
"endi",
"enumflags2",
"serde",
"url",
"winnow 1.0.4",
"zcheapstr",
"zvariant_derive",
+2
View File
@@ -14,6 +14,7 @@ tauri-build = { version = "2", features = [] }
[dependencies]
axum = "0.8"
cursor-server = { path = "../../../server" }
rfd = "0.15"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri = { version = "2", features = ["tray-icon"] }
@@ -26,5 +27,6 @@ tauri-plugin-updater = "2"
tokio = { version = "1", features = ["time"] }
tokio-util = "0.7"
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
url = "2"
+28 -13
View File
@@ -1,5 +1,5 @@
use std::{
process::Command,
process::{Command, ExitCode},
sync::{
atomic::{AtomicBool, Ordering},
Mutex,
@@ -19,7 +19,6 @@ use tauri::{
};
use tauri_plugin_opener::OpenerExt;
use tokio_util::sync::CancellationToken;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[cfg(dev)]
use cursor_server::config::ConsoleSource;
@@ -27,6 +26,7 @@ use cursor_server::{App, Config, Result};
#[cfg(not(dev))]
use crate::frontend;
use crate::startup::{self, StartupDiagnostics};
use crate::tray;
pub(crate) const MAIN_WINDOW_LABEL: &str = "main";
@@ -141,16 +141,23 @@ fn create_main_window(
builder.build()
}
pub fn run() {
let started_by_autostart = std::env::args_os().any(|arg| arg == AUTOSTART_ARG);
pub fn run() -> ExitCode {
let diagnostics = match StartupDiagnostics::initialize() {
Ok(diagnostics) => diagnostics,
Err(error) => {
startup::report_logging_failure(error.as_ref());
return ExitCode::FAILURE;
}
};
tracing::info!(
version = env!("CARGO_PKG_VERSION"),
os = std::env::consts::OS,
architecture = std::env::consts::ARCH,
log_directory = %diagnostics.log_directory().display(),
"desktop starting"
);
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "cursor_server=info".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
let started_by_autostart = std::env::args_os().any(|arg| arg == AUTOSTART_ARG);
let app = tauri::Builder::default()
.invoke_handler(tauri::generate_handler![open_terminal_with_command])
@@ -218,8 +225,14 @@ pub fn run() {
tray::create(app)?;
Ok(())
})
.build(tauri::generate_context!())
.expect("failed to build Cursor BYOK desktop app");
.build(tauri::generate_context!());
let app = match app {
Ok(app) => app,
Err(error) => {
diagnostics.report_fatal(&error);
return ExitCode::FAILURE;
}
};
app.run(|app, event| match event {
RunEvent::WindowEvent {
@@ -264,4 +277,6 @@ pub fn run() {
} => tray::show_main_window(app),
_ => {}
});
ExitCode::SUCCESS
}
+1
View File
@@ -1,6 +1,7 @@
mod desktop;
#[cfg(not(dev))]
mod frontend;
mod startup;
mod tray;
pub use desktop::run;
+2 -2
View File
@@ -1,5 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
cursor_byok_desktop::run();
fn main() -> std::process::ExitCode {
cursor_byok_desktop::run()
}
+139
View File
@@ -0,0 +1,139 @@
//! Captures startup diagnostics before the database and desktop runtime initialize.
use std::{error::Error, path::PathBuf};
use rfd::{MessageButtons, MessageDialog, MessageLevel};
use tracing_appender::{
non_blocking::WorkerGuard,
rolling::{RollingFileAppender, Rotation},
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
const LOG_DIRECTORY_NAME: &str = "logs";
const LOG_FILE_PREFIX: &str = "cursor-byok";
const LOG_FILE_SUFFIX: &str = "log";
const RETAINED_LOG_FILES: usize = 15;
type BoxError = Box<dyn Error + Send + Sync>;
pub(crate) struct StartupDiagnostics {
log_directory: PathBuf,
_writer_guard: WorkerGuard,
}
impl StartupDiagnostics {
pub(crate) fn initialize() -> Result<Self, BoxError> {
let log_directory = cursor_server::config::managed_data_dir()?.join(LOG_DIRECTORY_NAME);
std::fs::create_dir_all(&log_directory)?;
let file_appender = RollingFileAppender::builder()
.rotation(Rotation::DAILY)
.filename_prefix(LOG_FILE_PREFIX)
.filename_suffix(LOG_FILE_SUFFIX)
.max_log_files(RETAINED_LOG_FILES)
.build(&log_directory)?;
let (file_writer, writer_guard) = tracing_appender::non_blocking(file_appender);
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "cursor_byok_desktop=info,cursor_server=info".into());
tracing_subscriber::registry()
.with(filter)
.with(tracing_subscriber::fmt::layer())
.with(
tracing_subscriber::fmt::layer()
.with_ansi(false)
.with_writer(file_writer),
)
.try_init()?;
Ok(Self {
log_directory,
_writer_guard: writer_guard,
})
}
pub(crate) fn log_directory(&self) -> &std::path::Path {
&self.log_directory
}
pub(crate) fn report_fatal(&self, error: &(dyn Error + 'static)) {
let details = error_chain(error);
tracing::error!(
error = %details,
log_directory = %self.log_directory.display(),
"desktop failed to start"
);
show_fatal_dialog(&details, Some(&self.log_directory));
}
}
pub(crate) fn report_logging_failure(error: &(dyn Error + 'static)) {
let details = error_chain(error);
eprintln!("Cursor BYOK failed to initialize logging: {details}");
show_fatal_dialog(&details, None);
}
fn show_fatal_dialog(details: &str, log_directory: Option<&std::path::Path>) {
let log_guidance = match log_directory {
Some(directory) => format!(
"日志目录 / Log directory:\n{}\n\n请将最新的日志文件发送给开发者。\nPlease send the latest log file to the developer.",
directory.display()
),
None => "日志系统也未能启动,因此没有生成日志文件。\nLogging also failed to initialize, so no log file was created."
.to_owned(),
};
let description = format!(
"Cursor BYOK 无法启动 / failed to start.\n\n错误 / Error:\n{details}\n\n{log_guidance}"
);
let _ = MessageDialog::new()
.set_level(MessageLevel::Error)
.set_title("Cursor BYOK 启动失败 / Startup Error")
.set_description(description)
.set_buttons(MessageButtons::Ok)
.show();
}
fn error_chain(error: &(dyn Error + 'static)) -> String {
let mut details = error.to_string();
let mut source = error.source();
while let Some(cause) = source {
details.push_str("\nCaused by: ");
details.push_str(&cause.to_string());
source = cause.source();
}
details
}
#[cfg(test)]
mod tests {
use super::*;
use std::fmt;
#[derive(Debug)]
struct OuterError(std::io::Error);
impl fmt::Display for OuterError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("database initialization failed")
}
}
impl Error for OuterError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.0)
}
}
#[test]
fn fatal_report_includes_the_complete_error_chain() {
let error = OuterError(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"database file is read-only",
));
assert_eq!(
error_chain(&error),
"database initialization failed\nCaused by: database file is read-only"
);
}
}
+5 -1
View File
@@ -12,7 +12,11 @@ if (!existsSync(vite)) run(['ci'], desktopRoot);
run(['run', 'build:demo'], desktopRoot);
function run(args, cwd) {
const result = spawnSync(npm, args, { cwd, stdio: 'inherit' });
const result = spawnSync(npm, args, {
cwd,
stdio: 'inherit',
shell: process.platform === 'win32',
});
if (result.error) throw result.error;
if (result.status !== 0) process.exit(result.status ?? 1);
}
-1
View File
@@ -1,4 +1,3 @@
<!-- Defines stable Cursor prompt content embedded by the server. -->
You are an AI coding assistant, powered by {{FAKE_MODEL_NAME}}. You operate in Cursor.
Your main goal is to follow the USER's instructions, which are denoted by the <user_query> tag.
-1
View File
@@ -1,4 +1,3 @@
<!-- Defines stable Cursor prompt content embedded by the server. -->
{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
You are now in Agent mode. You have EXITED your previous mode. Continue with the task in the new mode.
</system_reminder>
-1
View File
@@ -1,4 +1,3 @@
<!-- Defines stable Cursor prompt content embedded by the server. -->
You are an AI coding assistant, powered by {{FAKE_MODEL_NAME}}. You operate in Cursor.
Your main goal is to follow the USER's instructions, which are denoted by the <user_query> tag.
-1
View File
@@ -1,4 +1,3 @@
<!-- Defines stable Cursor prompt content embedded by the server. -->
{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
You are now in Ask mode. You have EXITED your previous mode. Continue with the task in the new mode.
</system_reminder>
@@ -1,4 +1,3 @@
<!-- Defines stable Cursor prompt content embedded by the server. -->
You are compacting conversation history for future model turns.
Produce a concise plain-text summary that preserves durable context: user goals, constraints, facts, decisions, files, commands, errors, tool outcomes, and pending follow-ups.
Do not address the user. Do not mention compaction, summarization, or token limits.
@@ -1,4 +1,3 @@
<!-- Defines stable Cursor prompt content embedded by the server. -->
{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<timestamp>{{TIMESTAMP}}</timestamp>
<user_query>
{{USER_QUERY}}
-1
View File
@@ -1,4 +1,3 @@
<!-- Defines stable Cursor prompt content embedded by the server. -->
You are an AI coding assistant, powered by {{FAKE_MODEL_NAME}}. You operate in Cursor.
Your main goal is to follow the USER's instructions, which are denoted by the <user_query> tag.
-1
View File
@@ -1,4 +1,3 @@
<!-- Defines stable Cursor prompt content embedded by the server. -->
{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
You are now in Debug mode. You have EXITED your previous mode. Continue with the task in the new mode.
</system_reminder>
-1
View File
@@ -1,4 +1,3 @@
<!-- Defines stable Cursor prompt content embedded by the server. -->
You are an AI coding assistant, powered by Cursor {{FAKE_MODEL_NAME}}. You operate in Cursor.
Your main goal is to follow the USER's instructions, which are denoted by the <user_query> tag.
@@ -1,4 +1,3 @@
<!-- Defines stable Cursor prompt content embedded by the server. -->
{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
You are now in Multitask mode. You have EXITED your previous mode. Continue with the task in the new mode.
</system_reminder>
-1
View File
@@ -1,4 +1,3 @@
<!-- Defines stable Cursor prompt content embedded by the server. -->
You are an AI coding assistant, powered by {{FAKE_MODEL_NAME}}. You operate in Cursor.
Your main goal is to follow the USER's instructions, which are denoted by the <user_query> tag.
-1
View File
@@ -1,4 +1,3 @@
<!-- Defines stable Cursor prompt content embedded by the server. -->
{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
You are now in Plan mode. You have EXITED your previous mode. Continue with the task in the new mode.
</system_reminder>
-1
View File
@@ -1,4 +1,3 @@
<!-- Defines stable Cursor prompt content embedded by the server. -->
You are an AI coding assistant, powered by {{FAKE_MODEL_NAME}}. You operate in Cursor.
Your main goal is to follow the USER's instructions, which are denoted by the <user_query> tag.
-1
View File
@@ -1,4 +1,3 @@
<!-- Defines stable Cursor prompt content embedded by the server. -->
{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
You are currently working inside a Task subagent. Your parent agent has delegated a clearly bounded assignment to you. Complete that assignment directly with the tools available in this session. The Task tool is unavailable inside subagents, so delegation cannot be nested.
</system_reminder>
+1 -1
View File
@@ -169,7 +169,7 @@ impl ControlService {
.header(OS_HEADER, std::env::consts::OS)
.header(APP_VERSION_HEADER, env!("CARGO_PKG_VERSION"))
.header(LANGUAGE_HEADER, language)
.timeout(std::time::Duration::from_secs(5));
.timeout(std::time::Duration::from_secs(60));
if let Some(disabled_ad_ids) = disabled_ad_ids.filter(|value| !value.is_empty()) {
request = request.header(DISABLED_AD_IDS_HEADER, disabled_ad_ids);
}
+1 -1
View File
@@ -8,7 +8,7 @@ use crate::store::Store;
use super::{catalog, SearchEngine, SearchHit};
const RRF_K: f64 = 60.0;
const MAX_RESULTS: usize = 10;
const MAX_RESULTS: usize = 30;
#[derive(Clone)]
pub struct WebSearch {
+2
View File
@@ -28,7 +28,9 @@ impl Store {
.max_connections(8)
.connect_with(options)
.await?;
tracing::info!("running database migrations");
sqlx::migrate!("./migrations").run(&pool).await?;
tracing::info!("database migrations completed");
Ok(Self {
pool,
writes: WriteCoordinator::default(),