mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-04 02:52:55 +08:00
- 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.
283 lines
9.7 KiB
Rust
283 lines
9.7 KiB
Rust
use std::{
|
|
process::{Command, ExitCode},
|
|
sync::{
|
|
atomic::{AtomicBool, Ordering},
|
|
Mutex,
|
|
},
|
|
time::Duration,
|
|
};
|
|
|
|
use axum::{
|
|
extract::{Extension, Json},
|
|
http::StatusCode,
|
|
routing::post,
|
|
Router,
|
|
};
|
|
use tauri::{
|
|
async_runtime::JoinHandle, webview::Color, AppHandle, Manager, RunEvent, WebviewUrl,
|
|
WebviewWindow, WebviewWindowBuilder,
|
|
};
|
|
use tauri_plugin_opener::OpenerExt;
|
|
use tokio_util::sync::CancellationToken;
|
|
|
|
#[cfg(dev)]
|
|
use cursor_server::config::ConsoleSource;
|
|
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";
|
|
const AUTOSTART_ARG: &str = "--autostart";
|
|
|
|
struct DesktopRuntime {
|
|
shutdown: CancellationToken,
|
|
server: Mutex<Option<JoinHandle<Result<()>>>>,
|
|
exiting: AtomicBool,
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn open_terminal_with_command(command: String) -> tauri::Result<()> {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let _ = command;
|
|
Command::new("open").args(["-a", "Terminal"]).status()?;
|
|
Ok(())
|
|
}
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
Command::new("cmd")
|
|
.args(["/C", "start", "cmd", "/K", &command])
|
|
.spawn()?;
|
|
Ok(())
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
const TERMINALS: &[(&str, &[&str])] = &[
|
|
("x-terminal-emulator", &["-e"]),
|
|
("gnome-terminal", &["--"]),
|
|
("konsole", &["-e"]),
|
|
("xfce4-terminal", &["--execute"]),
|
|
("alacritty", &["-e"]),
|
|
("kitty", &[]),
|
|
];
|
|
let script = format!("{command}; exec bash");
|
|
for (terminal, separator) in TERMINALS {
|
|
let mut process = Command::new(terminal);
|
|
process.args(*separator);
|
|
process.arg("bash").arg("-c").arg(&script);
|
|
match process.spawn() {
|
|
Ok(_) => return Ok(()),
|
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
|
|
Err(error) => return Err(error.into()),
|
|
}
|
|
}
|
|
Err(tauri::Error::from(std::io::Error::new(
|
|
std::io::ErrorKind::NotFound,
|
|
"no supported terminal emulator found",
|
|
)))
|
|
}
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct OpenExternalUrlRequest {
|
|
url: String,
|
|
}
|
|
|
|
fn open_external_url(app: &AppHandle, url: &str) -> std::result::Result<(), String> {
|
|
let parsed = url::Url::parse(url).map_err(|error| format!("invalid URL: {error}"))?;
|
|
if !matches!(parsed.scheme(), "http" | "https") || parsed.host_str().is_none() {
|
|
return Err("only absolute HTTP and HTTPS URLs are allowed".into());
|
|
}
|
|
|
|
app.opener()
|
|
.open_url(parsed.as_str(), None::<&str>)
|
|
.map_err(|error| format!("failed to open URL: {error}"))
|
|
}
|
|
|
|
async fn open_external_url_handler(
|
|
Extension(app): Extension<AppHandle>,
|
|
Json(request): Json<OpenExternalUrlRequest>,
|
|
) -> std::result::Result<StatusCode, (StatusCode, String)> {
|
|
open_external_url(&app, &request.url)
|
|
.map(|_| StatusCode::NO_CONTENT)
|
|
.map_err(|error| (StatusCode::BAD_REQUEST, error))
|
|
}
|
|
|
|
fn desktop_api_router(app: AppHandle) -> Router {
|
|
Router::new()
|
|
.route(
|
|
"/__byok-api__/api/desktop/open-external-url",
|
|
post(open_external_url_handler),
|
|
)
|
|
.layer(Extension(app))
|
|
}
|
|
|
|
fn create_main_window(
|
|
app: &AppHandle,
|
|
address: std::net::SocketAddr,
|
|
) -> tauri::Result<WebviewWindow> {
|
|
let url = format!("http://{address}/__byok-api__/")
|
|
.parse()
|
|
.expect("local frontend URL");
|
|
let builder = WebviewWindowBuilder::new(app, MAIN_WINDOW_LABEL, WebviewUrl::External(url))
|
|
.title("Cursor BYOK")
|
|
.inner_size(820.0, 558.0)
|
|
.min_inner_size(820.0, 558.0)
|
|
.center()
|
|
.background_color(Color(20, 20, 20, 255))
|
|
.decorations(cfg!(target_os = "macos"))
|
|
.shadow(true)
|
|
.resizable(true)
|
|
.visible(false);
|
|
|
|
#[cfg(target_os = "macos")]
|
|
let builder = builder
|
|
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
|
.hidden_title(true);
|
|
|
|
builder.build()
|
|
}
|
|
|
|
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"
|
|
);
|
|
|
|
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])
|
|
.plugin(tauri_plugin_single_instance::init(|app, args, _| {
|
|
if !args.iter().any(|arg| arg == AUTOSTART_ARG) {
|
|
tray::show_main_window(app);
|
|
}
|
|
}))
|
|
.plugin(tauri_plugin_clipboard_manager::init())
|
|
.plugin(tauri_plugin_opener::init())
|
|
.plugin(tauri_plugin_process::init())
|
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
|
.setup(move |app| {
|
|
app.handle().plugin(tauri_plugin_autostart::init(
|
|
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
|
|
Some(vec![AUTOSTART_ARG]),
|
|
))?;
|
|
let config = Config::desktop()?;
|
|
#[cfg(dev)]
|
|
let config = {
|
|
let mut config = config;
|
|
config.console = Some(ConsoleSource::Proxy(
|
|
"http://127.0.0.1:1420"
|
|
.parse()
|
|
.expect("Vite development URL"),
|
|
));
|
|
config
|
|
};
|
|
let server = tauri::async_runtime::block_on(App::new(config))?
|
|
.merge_router(desktop_api_router(app.handle().clone()));
|
|
#[cfg(not(dev))]
|
|
let server = server.merge_router(frontend::router(app.handle().clone()));
|
|
let listener = tauri::async_runtime::block_on(server.bind())?;
|
|
let address = listener.local_addr()?;
|
|
tauri::async_runtime::block_on(server.harness().cleanup_stale_settings())?;
|
|
let desktop_settings =
|
|
tauri::async_runtime::block_on(server.store().desktop_settings())
|
|
.unwrap_or_default();
|
|
#[cfg(target_os = "macos")]
|
|
app.handle()
|
|
.set_dock_visibility(desktop_settings.show_dock_icon)?;
|
|
let shutdown = CancellationToken::new();
|
|
let server_shutdown = shutdown.clone();
|
|
let app_handle = app.handle().clone();
|
|
let task = tauri::async_runtime::spawn(async move {
|
|
let result = server.serve_on(listener, server_shutdown).await;
|
|
if let Err(error) = &result {
|
|
tracing::error!(%error, "desktop server stopped unexpectedly");
|
|
app_handle.exit(1);
|
|
}
|
|
result
|
|
});
|
|
app.manage(DesktopRuntime {
|
|
shutdown,
|
|
server: Mutex::new(Some(task)),
|
|
exiting: AtomicBool::new(false),
|
|
});
|
|
let window = create_main_window(app.handle(), address)?;
|
|
if desktop_settings.silent_start && started_by_autostart {
|
|
tracing::info!("silent autostart enabled; keeping the main window hidden");
|
|
} else {
|
|
window.show()?;
|
|
window.set_focus()?;
|
|
}
|
|
tray::create(app)?;
|
|
Ok(())
|
|
})
|
|
.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 {
|
|
label,
|
|
event: tauri::WindowEvent::CloseRequested { api, .. },
|
|
..
|
|
} if label == MAIN_WINDOW_LABEL => {
|
|
let runtime = app.state::<DesktopRuntime>();
|
|
if !runtime.exiting.load(Ordering::Acquire) {
|
|
api.prevent_close();
|
|
if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) {
|
|
let _ = window.hide();
|
|
}
|
|
}
|
|
}
|
|
RunEvent::ExitRequested { api, .. } => {
|
|
let runtime = app.state::<DesktopRuntime>();
|
|
if !runtime.exiting.swap(true, Ordering::AcqRel) {
|
|
api.prevent_exit();
|
|
runtime.shutdown.cancel();
|
|
let server = runtime.server.lock().expect("server lock poisoned").take();
|
|
let app = app.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
if let Some(server) = server {
|
|
match tokio::time::timeout(Duration::from_secs(11), server).await {
|
|
Ok(Ok(Ok(()))) => {}
|
|
Ok(Ok(Err(error))) => {
|
|
tracing::error!(%error, "desktop server shutdown failed")
|
|
}
|
|
Ok(Err(error)) => tracing::error!(%error, "desktop server task failed"),
|
|
Err(_) => tracing::warn!("desktop server shutdown timed out"),
|
|
}
|
|
}
|
|
app.exit(0);
|
|
});
|
|
}
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
RunEvent::Reopen {
|
|
has_visible_windows: false,
|
|
..
|
|
} => tray::show_main_window(app),
|
|
_ => {}
|
|
});
|
|
|
|
ExitCode::SUCCESS
|
|
}
|