mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 20:42:26 +00:00
859 lines
35 KiB
Rust
Vendored
859 lines
35 KiB
Rust
Vendored
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
|
|
pub mod tauri_handlers;
|
|
pub mod uninstall;
|
|
pub mod utils;
|
|
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
use tauri::State;
|
|
use tauri::menu::{CheckMenuItemBuilder, Menu, MenuItemBuilder};
|
|
use tauri::tray::TrayIconBuilder;
|
|
use tauri::{AppHandle, Manager, RESTART_EXIT_CODE, Runtime};
|
|
use tauri_plugin_dialog::DialogExt;
|
|
|
|
#[cfg(target_os = "windows")]
|
|
extern crate winapi;
|
|
|
|
use crate::tauri_handlers::startup::{
|
|
abort_installation, create_default_backend_services, get_installation_status, install_conda,
|
|
install_to_directory, setup_python_environment,
|
|
};
|
|
|
|
use crate::tauri_handlers::environments::{
|
|
create_environment, create_environment_from_requirements, execute_in_environment,
|
|
get_environment_extensions, install_extensions, list_conda_environments, remove_environment,
|
|
remove_extension, select_requirements_file, update_environment, update_extension,
|
|
update_installation_error,
|
|
};
|
|
|
|
use crate::tauri_handlers::jupyter::{
|
|
check_jupyter_server, list_jupyter_servers, open_jupyter_logs_window, start_jupyter_server,
|
|
stop_all_jupyter_servers, stop_jupyter_server, update_jupyter_status,
|
|
};
|
|
|
|
use crate::tauri_handlers::credentials::{
|
|
get_user_credentials, open_credentials_file, update_user_credentials,
|
|
};
|
|
|
|
use crate::tauri_handlers::backends::{
|
|
create_backend_service, delete_backend_service, initialize_backends, list_backend_services,
|
|
open_backend_logs_window, start_backend_service, stop_backend_service, update_backend_service,
|
|
};
|
|
|
|
use crate::utils::certs::generate_self_signed_cert;
|
|
|
|
use crate::tauri_handlers::helpers::{
|
|
check_directory_exists, check_file_exists, get_home_directory, get_installation_directory,
|
|
get_or_create_app_id, get_settings_directory, get_userdata_directory, get_working_directory,
|
|
open_url_in_window, open_workspace_in_browser, save_working_directory, select_directory,
|
|
select_file, toggle_theme, update_openbb_settings,
|
|
};
|
|
|
|
use tauri_plugin_updater::UpdaterExt;
|
|
|
|
use crate::utils::process_monitor::{
|
|
GetProcessLogsRequest, LogEntry, LogStorage, RunningProcesses, clear_process_logs,
|
|
get_log_storage, get_process_logs, init_process_monitoring, register_process,
|
|
unregister_process,
|
|
};
|
|
|
|
use crate::uninstall::uninstall_application;
|
|
|
|
#[derive(Clone, serde::Serialize)]
|
|
struct InstallationState {
|
|
is_installed: bool,
|
|
installation_directory: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct ProcessLogState(LogStorage);
|
|
|
|
#[tauri::command]
|
|
fn register_process_monitoring(state: State<ProcessLogState>, process_id: String) -> bool {
|
|
register_process(&state.0, &process_id)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn unregister_process_monitoring(state: State<ProcessLogState>, process_id: String) -> bool {
|
|
unregister_process(&state.0, &process_id)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn get_process_logs_history(
|
|
state: State<ProcessLogState>,
|
|
process_id: String,
|
|
count: Option<usize>,
|
|
) -> Vec<LogEntry> {
|
|
let request = GetProcessLogsRequest { process_id, count };
|
|
get_process_logs(&state.0.clone(), request)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn clear_process_logs_history(state: State<ProcessLogState>, process_id: String) -> bool {
|
|
clear_process_logs(&state.0, &process_id)
|
|
}
|
|
|
|
async fn check_and_apply_update(app: AppHandle, always_prompt: bool) {
|
|
let show_error = |app: &AppHandle, title: &str, message: String| {
|
|
app.dialog()
|
|
.message(message)
|
|
.title(title)
|
|
.kind(tauri_plugin_dialog::MessageDialogKind::Error)
|
|
.show(|_| {});
|
|
};
|
|
let update_url = "https://github.com/OpenBB-finance/OpenBB/releases/download/ODP/latest.json";
|
|
|
|
let ap_id = get_or_create_app_id();
|
|
|
|
let headers = {
|
|
let mut headers = reqwest::header::HeaderMap::new();
|
|
|
|
match reqwest::header::HeaderValue::from_str("ODP-Updater") {
|
|
Ok(user_agent) => {
|
|
headers.insert(reqwest::header::USER_AGENT, user_agent);
|
|
}
|
|
Err(e) => {
|
|
log::error!("Failed to create User-Agent header: {}", e);
|
|
if always_prompt {
|
|
show_error(
|
|
&app,
|
|
"Update Check Failed",
|
|
format!("Failed to create User-Agent header: {}", e),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
match reqwest::header::HeaderValue::from_str(&ap_id) {
|
|
Ok(app_id) => {
|
|
headers.insert(reqwest::header::HeaderName::from_static("x-app-id"), app_id);
|
|
}
|
|
Err(e) => {
|
|
log::error!("Failed to create X-App-ID header: {}", e);
|
|
if always_prompt {
|
|
show_error(
|
|
&app,
|
|
"Update Check Failed",
|
|
format!("Failed to create X-App-ID header: {}", e),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
headers
|
|
};
|
|
|
|
let url = match update_url.parse() {
|
|
Ok(url) => url,
|
|
Err(e) => {
|
|
let err_msg = format!("Failed to parse update URL: {}", e);
|
|
log::error!("{}", err_msg);
|
|
if always_prompt {
|
|
show_error(&app, "Update Check Failed", err_msg);
|
|
}
|
|
return;
|
|
}
|
|
};
|
|
|
|
let updater_res = app
|
|
.updater_builder()
|
|
.headers(headers)
|
|
.endpoints(vec![url])
|
|
.and_then(|builder| builder.build());
|
|
|
|
match updater_res {
|
|
Ok(updater) => {
|
|
match updater.check().await {
|
|
Ok(Some(update)) => {
|
|
let app_clone = app.clone();
|
|
app.dialog()
|
|
.message(format!(
|
|
"A new version ({}) is available. Would you like to install it now? The update will close the application and restart.",
|
|
update.version
|
|
))
|
|
.title("Update Available")
|
|
.kind(tauri_plugin_dialog::MessageDialogKind::Info)
|
|
.buttons(tauri_plugin_dialog::MessageDialogButtons::YesNo)
|
|
.show(move |install| {
|
|
if install {
|
|
let app_clone_inner = app_clone.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
if let Some(window) = app_clone_inner.get_webview_window("main") {
|
|
let _ = window.show();
|
|
let _ = window.set_focus();
|
|
}
|
|
|
|
if let Err(e) = update.download_and_install(|_, _| {}, || {}).await
|
|
{
|
|
log::error!("Failed to install update: {}", e);
|
|
show_error(
|
|
&app_clone_inner,
|
|
"Update Failed",
|
|
format!("Failed to install update: {}", e),
|
|
);
|
|
} else {
|
|
log::info!("Update installed successfully, restarting...");
|
|
|
|
// SET THE FLAG TO SHOW WINDOW AFTER RESTART
|
|
if let Ok(home_dir) = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")) {
|
|
let flag_path = std::path::Path::new(&home_dir).join(".openbb_platform").join(".show_on_restart");
|
|
let _ = std::fs::write(flag_path, "1");
|
|
log::info!("Set flag to show window on restart");
|
|
}
|
|
|
|
if let Some(window) = app_clone_inner.get_webview_window("main") {
|
|
let _ = window.show();
|
|
}
|
|
|
|
app_clone_inner.request_restart();
|
|
|
|
/* #[cfg(not(target_os = "macos"))]
|
|
{
|
|
use std::process::Command;
|
|
|
|
if let Ok(exe_path) = std::env::current_exe() {
|
|
log::debug!("Relaunching from: {}", exe_path.display());
|
|
let _ = Command::new(exe_path).spawn();
|
|
std::process::exit(0);
|
|
} else {
|
|
log::error!("Could not determine executable path for restart");
|
|
app_clone_inner.restart();
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
Ok(None) => {
|
|
log::debug!("No updates available");
|
|
if always_prompt {
|
|
app.dialog()
|
|
.message("You are already running the latest version.")
|
|
.title("No Updates Available")
|
|
.kind(tauri_plugin_dialog::MessageDialogKind::Info)
|
|
.show(|_| {});
|
|
}
|
|
}
|
|
Err(e) => {
|
|
let err_msg = format!("Failed to check for updates: {}", e);
|
|
log::error!("{}", err_msg);
|
|
if always_prompt {
|
|
show_error(&app, "Update Check Failed", err_msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
let err_msg = format!("Failed to build updater: {}", e);
|
|
log::error!("{}", err_msg);
|
|
if always_prompt {
|
|
show_error(&app, "Update Check Failed", err_msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn trigger_update_dialog(app: AppHandle) {
|
|
check_and_apply_update(app, true).await;
|
|
}
|
|
|
|
async fn background_update_check(app: AppHandle) {
|
|
check_and_apply_update(app, false).await;
|
|
}
|
|
|
|
fn check_installation_on_startup() -> InstallationState {
|
|
log::debug!("STARTUP: Checking installation status");
|
|
|
|
let home_dir = match std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")) {
|
|
Ok(dir) => dir,
|
|
Err(e) => {
|
|
log::debug!("STARTUP: Failed to get home directory: {e}");
|
|
return InstallationState {
|
|
is_installed: false,
|
|
installation_directory: None,
|
|
};
|
|
}
|
|
};
|
|
|
|
let platform_dir = Path::new(&home_dir).join(".openbb_platform");
|
|
let system_settings_path = platform_dir.join("system_settings.json");
|
|
|
|
log::debug!(
|
|
"STARTUP: Looking for system settings at: {}",
|
|
system_settings_path.display()
|
|
);
|
|
|
|
if !system_settings_path.exists() {
|
|
log::debug!("STARTUP: System settings file not found");
|
|
return InstallationState {
|
|
is_installed: false,
|
|
installation_directory: None,
|
|
};
|
|
}
|
|
|
|
log::debug!("STARTUP: System settings file found");
|
|
|
|
let settings_content = match std::fs::read_to_string(&system_settings_path) {
|
|
Ok(content) => content,
|
|
Err(e) => {
|
|
log::debug!("STARTUP: Failed to read settings file: {e}");
|
|
return InstallationState {
|
|
is_installed: false,
|
|
installation_directory: None,
|
|
};
|
|
}
|
|
};
|
|
|
|
let settings: serde_json::Value = match serde_json::from_str(&settings_content) {
|
|
Ok(json) => json,
|
|
Err(e) => {
|
|
log::debug!("STARTUP: Failed to parse settings JSON: {e}");
|
|
return InstallationState {
|
|
is_installed: false,
|
|
installation_directory: None,
|
|
};
|
|
}
|
|
};
|
|
|
|
let mut install_dir = None;
|
|
|
|
if let Some(install_settings) = settings.get("install_settings")
|
|
&& let Some(dir) = install_settings.get("installation_directory")
|
|
&& let Some(dir_str) = dir.as_str()
|
|
{
|
|
log::debug!("STARTUP: Found installation directory in install_settings: {dir_str}");
|
|
let conda_dir = Path::new(dir_str).join("conda");
|
|
|
|
let conda_exe = if cfg!(target_os = "windows") {
|
|
conda_dir.join("Scripts").join("conda.exe")
|
|
} else {
|
|
conda_dir.join("bin").join("conda")
|
|
};
|
|
|
|
if conda_exe.exists() {
|
|
log::debug!("STARTUP: Conda executable found at {}", conda_exe.display());
|
|
install_dir = Some(dir_str.to_string());
|
|
} else {
|
|
log::debug!(
|
|
"STARTUP: Conda executable not found at {}",
|
|
conda_exe.display()
|
|
);
|
|
}
|
|
}
|
|
|
|
if install_dir.is_none()
|
|
&& let Some(dir) = settings.get("installation_directory")
|
|
&& let Some(dir_str) = dir.as_str()
|
|
{
|
|
log::debug!("STARTUP: Found installation directory at root level: {dir_str}");
|
|
let conda_dir = Path::new(dir_str).join("conda");
|
|
|
|
let conda_exe = if cfg!(target_os = "windows") {
|
|
conda_dir.join("Scripts").join("conda.exe")
|
|
} else {
|
|
conda_dir.join("bin").join("conda")
|
|
};
|
|
|
|
if conda_exe.exists() {
|
|
log::debug!("STARTUP: Conda executable found at {}", conda_exe.display());
|
|
install_dir = Some(dir_str.to_string());
|
|
} else {
|
|
log::debug!(
|
|
"STARTUP: Conda executable not found at {}",
|
|
conda_exe.display()
|
|
);
|
|
}
|
|
}
|
|
|
|
let is_installed = install_dir.is_some();
|
|
log::debug!(
|
|
"STARTUP: Installation status: {}",
|
|
if is_installed { "VALID" } else { "INVALID" }
|
|
);
|
|
|
|
InstallationState {
|
|
is_installed,
|
|
installation_directory: install_dir,
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn get_installation_state(state: tauri::State<InstallationState>) -> InstallationState {
|
|
state.inner().clone()
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn navigate_to_page<R: Runtime>(app_handle: AppHandle<R>, page: &str) {
|
|
if let Some(window) = app_handle.get_webview_window("main") {
|
|
let _ = window.show();
|
|
let _ = window.set_focus();
|
|
|
|
let js = format!(
|
|
r#"
|
|
if (localStorage.getItem('environments-first-load-done') === 'true') {{
|
|
window.location.href = '{page}';
|
|
}} else {{
|
|
console.log('Navigation prevented: environments-first-load-done not set');
|
|
}}
|
|
"#
|
|
);
|
|
let _ = window.eval(&js);
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn quit_application(app_handle: AppHandle) {
|
|
log::debug!("Quit application command received, running cleanup...");
|
|
cleanup_all_processes(app_handle.clone()).await;
|
|
app_handle.exit(0);
|
|
}
|
|
|
|
async fn cleanup_all_processes(app_handle: AppHandle) {
|
|
use crate::tauri_handlers::helpers::{RealEnvSystem, RealFileExtTrait, RealFileSystem};
|
|
log::debug!("Running complete application cleanup");
|
|
|
|
let cleanup_timeout = std::time::Duration::from_secs(10);
|
|
|
|
let cleanup_result = tokio::time::timeout(cleanup_timeout, async {
|
|
match tokio::time::timeout(
|
|
std::time::Duration::from_secs(3),
|
|
tauri_handlers::jupyter::stop_all_jupyter_servers(app_handle.clone()),
|
|
)
|
|
.await
|
|
{
|
|
Ok(Ok(_)) => log::debug!("Successfully stopped all Jupyter servers"),
|
|
Ok(Err(e)) => log::error!("Error stopping Jupyter servers: {e}"),
|
|
Err(_) => log::warn!("Jupyter servers shutdown timed out"),
|
|
}
|
|
|
|
match tokio::time::timeout(
|
|
std::time::Duration::from_secs(3),
|
|
tauri_handlers::backends::stop_all_backend_services(
|
|
app_handle,
|
|
&RealFileSystem,
|
|
&RealEnvSystem,
|
|
&RealFileExtTrait,
|
|
),
|
|
)
|
|
.await
|
|
{
|
|
Ok(Ok(_)) => log::debug!("Successfully stopped all backend services"),
|
|
Ok(Err(e)) => log::error!("Error stopping backend services: {e}"),
|
|
Err(_) => log::warn!("Backend services shutdown timed out"),
|
|
}
|
|
})
|
|
.await;
|
|
|
|
match cleanup_result {
|
|
Ok(_) => log::debug!("All cleanup completed successfully"),
|
|
Err(_) => log::warn!("Cleanup process timed out after 10 seconds"),
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
log::debug!("Waiting for Windows to clean up UI resources...");
|
|
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
|
}
|
|
|
|
log::debug!("Cleanup complete");
|
|
}
|
|
|
|
fn main() {
|
|
let _ = fix_path_env::fix();
|
|
init_process_monitoring();
|
|
|
|
tauri::Builder::default()
|
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
|
.plugin(tauri_plugin_opener::init())
|
|
.plugin(tauri_plugin_single_instance::init(|app, _, _| {
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let _ = window.show();
|
|
let _ = window.set_focus();
|
|
}
|
|
}))
|
|
.plugin(tauri_plugin_fs::init())
|
|
.plugin(tauri_plugin_shell::init())
|
|
.plugin(tauri_plugin_persisted_scope::init())
|
|
.plugin(tauri_plugin_log::Builder::new().clear_targets().target(tauri_plugin_log::Target::new(
|
|
tauri_plugin_log::TargetKind::Stdout,
|
|
)).target(tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stderr)).build())
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.manage(ProcessLogState(get_log_storage()))
|
|
.manage(RunningProcesses::new())
|
|
.manage(check_installation_on_startup())
|
|
.invoke_handler(tauri::generate_handler![
|
|
toggle_theme,
|
|
navigate_to_page,
|
|
save_working_directory,
|
|
get_working_directory,
|
|
get_home_directory,
|
|
select_directory,
|
|
get_installation_directory,
|
|
get_userdata_directory,
|
|
get_settings_directory,
|
|
select_file,
|
|
install_to_directory,
|
|
check_directory_exists,
|
|
check_file_exists,
|
|
install_conda,
|
|
abort_installation,
|
|
get_installation_status,
|
|
get_installation_state,
|
|
setup_python_environment,
|
|
create_environment,
|
|
list_conda_environments,
|
|
get_environment_extensions,
|
|
install_extensions,
|
|
update_extension,
|
|
update_environment,
|
|
update_installation_error,
|
|
remove_extension,
|
|
remove_environment,
|
|
create_environment_from_requirements,
|
|
select_requirements_file,
|
|
execute_in_environment,
|
|
start_jupyter_server,
|
|
stop_jupyter_server,
|
|
stop_all_jupyter_servers,
|
|
check_jupyter_server,
|
|
list_jupyter_servers,
|
|
get_user_credentials,
|
|
open_credentials_file,
|
|
update_user_credentials,
|
|
open_url_in_window,
|
|
register_process_monitoring,
|
|
unregister_process_monitoring,
|
|
get_process_logs_history,
|
|
clear_process_logs_history,
|
|
open_jupyter_logs_window,
|
|
update_jupyter_status,
|
|
open_backend_logs_window,
|
|
start_backend_service,
|
|
stop_backend_service,
|
|
update_backend_service,
|
|
create_backend_service,
|
|
delete_backend_service,
|
|
list_backend_services,
|
|
uninstall_application,
|
|
quit_application,
|
|
generate_self_signed_cert,
|
|
update_openbb_settings,
|
|
create_default_backend_services
|
|
])
|
|
.setup(|app_handle| {
|
|
let install_state = check_installation_on_startup();
|
|
|
|
let show_after_update = {
|
|
if let Ok(home_dir) = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")) {
|
|
let flag_path = std::path::Path::new(&home_dir).join(".openbb_platform").join(".show_on_restart");
|
|
if flag_path.exists() {
|
|
log::info!("Found update restart flag - will show window");
|
|
let _ = std::fs::remove_file(&flag_path);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
} else {
|
|
false
|
|
}
|
|
};
|
|
|
|
if install_state.is_installed {
|
|
let backend_handle = app_handle.handle().clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
use crate::tauri_handlers::helpers::{RealFileExtTrait, RealFileSystem, RealEnvSystem};
|
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
|
log::debug!("Initializing backends after state setup delay");
|
|
if let Err(e) = initialize_backends(&backend_handle, RealFileSystem, RealEnvSystem, RealFileExtTrait).await {
|
|
log::error!("Failed to initialize backends: {e}");
|
|
}
|
|
});
|
|
}
|
|
|
|
if let Some(window) = app_handle.get_webview_window("main") {
|
|
window.set_menu(Menu::new(app_handle.handle())?)?;
|
|
}
|
|
|
|
let autostart_enabled = {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
utils::autostart::macos_autostart::is_autostart_enabled(app_handle.handle()).unwrap_or(true)
|
|
}
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
utils::autostart::windows_autostart::is_autostart_enabled(app_handle.handle()).unwrap_or(true)
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
utils::autostart::linux_autostart::is_autostart_enabled(&app_handle.handle()).unwrap_or(true)
|
|
}
|
|
};
|
|
log::debug!("Autostart is currently: {}", if autostart_enabled { "enabled" } else { "disabled" });
|
|
|
|
let handle = app_handle.handle().clone();
|
|
let open_item = MenuItemBuilder::new("Open Window").id("open").build(&handle)?;
|
|
let open_workspace = MenuItemBuilder::new("Go to Workspace").id("open_workspace").build(&handle)?;
|
|
let separator1 = tauri::menu::PredefinedMenuItem::separator(&handle)?;
|
|
let open_environments_item = MenuItemBuilder::new("Environments").id("open_environments").build(&handle)?;
|
|
let open_api_keys_item = MenuItemBuilder::new("API Keys").id("open_api_keys").build(&handle)?;
|
|
let open_backends_item = MenuItemBuilder::new("Backends").id("open_backends").build(&handle)?;
|
|
let separator2 = tauri::menu::PredefinedMenuItem::separator(&handle)?;
|
|
let start_at_login_item = CheckMenuItemBuilder::new("Start at Login in Background")
|
|
.id("start_at_login")
|
|
.checked(autostart_enabled)
|
|
.build(&handle)?;
|
|
let separator3 = tauri::menu::PredefinedMenuItem::separator(&handle)?;
|
|
let check_updates_item = MenuItemBuilder::new("Check for Updates").id("check_updates").build(&handle)?;
|
|
let uninstall_item = MenuItemBuilder::new("Uninstall").id("uninstall").build(&handle)?;
|
|
let quit_item = MenuItemBuilder::new("Quit").id("quit").build(&handle)?;
|
|
|
|
let menu = Menu::with_items(&handle, &[
|
|
&open_item,
|
|
&open_workspace,
|
|
&separator1,
|
|
&open_backends_item,
|
|
&open_environments_item,
|
|
&open_api_keys_item,
|
|
&separator2,
|
|
&start_at_login_item,
|
|
&separator3,
|
|
&check_updates_item,
|
|
&uninstall_item,
|
|
&quit_item
|
|
])?;
|
|
|
|
let icon = handle.default_window_icon().unwrap().clone();
|
|
let tray_handle = handle.clone();
|
|
let tray = TrayIconBuilder::new()
|
|
.icon(icon)
|
|
.tooltip("Open Data Platform - By OpenBB")
|
|
.menu(&menu)
|
|
.on_menu_event(move |_tray, event| {
|
|
let id_string = event.id().0.as_str();
|
|
match id_string {
|
|
"quit" => {
|
|
log::debug!("Received termination signal, running cleanup...");
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
let cleanup_handle = tray_handle.clone();
|
|
rt.block_on(async {
|
|
cleanup_all_processes(cleanup_handle).await;
|
|
});
|
|
log::debug!("Exiting application");
|
|
tray_handle.exit(0);
|
|
}
|
|
"open" => {
|
|
if let Some(window) = tray_handle.get_webview_window("main") {
|
|
window.show().unwrap();
|
|
window.set_focus().unwrap();
|
|
}
|
|
}
|
|
"open_workspace" => open_workspace_in_browser(),
|
|
"open_environments" => navigate_to_page(tray_handle.clone(), "/environments"),
|
|
"open_api_keys" => navigate_to_page(tray_handle.clone(), "/api-keys"),
|
|
"open_backends" => navigate_to_page(tray_handle.clone(), "/backends"),
|
|
"check_updates" => {
|
|
let update_handle = tray_handle.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
trigger_update_dialog(update_handle).await;
|
|
});
|
|
}
|
|
"uninstall" => {
|
|
if let Some(window) = tray_handle.get_webview_window("main") {
|
|
window.show().unwrap();
|
|
window.set_focus().unwrap();
|
|
let install_state = check_installation_on_startup();
|
|
if !install_state.is_installed {
|
|
tray_handle.dialog().message("The installation appears to be incomplete. To uninstall, quit the application and remove the application from the operating system.").kind(tauri_plugin_dialog::MessageDialogKind::Error).show(|_| {});
|
|
} else {
|
|
window.eval("window.location.href = '/uninstall';").unwrap();
|
|
}
|
|
}
|
|
},
|
|
"start_at_login" => {
|
|
let app_handle = tray_handle.clone();
|
|
let is_enabled = {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
utils::autostart::macos_autostart::is_autostart_enabled(&app_handle).unwrap_or(false)
|
|
}
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
utils::autostart::windows_autostart::is_autostart_enabled(&app_handle).unwrap_or(false)
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
utils::autostart::linux_autostart::is_autostart_enabled(&app_handle).unwrap_or(false)
|
|
}
|
|
};
|
|
log::debug!("Current autostart status: {}", if is_enabled { "enabled" } else { "disabled" });
|
|
let target_state = !is_enabled;
|
|
log::debug!("Attempting to {} autostart", if target_state { "enable" } else { "disable" });
|
|
let result = {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
if target_state {
|
|
utils::autostart::macos_autostart::enable_autostart(&app_handle)
|
|
} else {
|
|
utils::autostart::macos_autostart::disable_autostart(&app_handle)
|
|
}
|
|
}
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
if target_state {
|
|
utils::autostart::windows_autostart::enable_autostart(&app_handle)
|
|
} else {
|
|
utils::autostart::windows_autostart::disable_autostart(&app_handle)
|
|
}
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
if target_state {
|
|
utils::autostart::linux_autostart::enable_autostart(&app_handle)
|
|
} else {
|
|
utils::autostart::linux_autostart::disable_autostart(&app_handle)
|
|
}
|
|
}
|
|
};
|
|
if let Err(e) = result {
|
|
log::error!("Failed to {} autostart: {}", if target_state { "enable" } else { "disable" }, e);
|
|
return;
|
|
}
|
|
if let Err(e) = start_at_login_item.set_checked(target_state) {
|
|
log::error!("Failed to update menu item state: {e}");
|
|
} else {
|
|
log::debug!("Menu item state updated to: {target_state}");
|
|
}
|
|
log::debug!("Successfully {} autostart", if target_state { "enabled" } else { "disabled" });
|
|
}
|
|
_ => {}
|
|
}
|
|
})
|
|
.build(&handle)
|
|
.unwrap();
|
|
|
|
app_handle.manage(tray);
|
|
|
|
if let Some(window) = app_handle.get_webview_window("main") {
|
|
let window_clone = window.clone();
|
|
window.on_window_event(move |event| {
|
|
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
|
window_clone.hide().unwrap();
|
|
api.prevent_close();
|
|
}
|
|
});
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
use objc2_app_kit::{NSColor, NSWindow};
|
|
let ns_window_ptr = window.ns_window().unwrap();
|
|
let ns_window = unsafe { &*(ns_window_ptr as *mut NSWindow) };
|
|
let bg_color = NSColor::colorWithRed_green_blue_alpha(0.0, 0.0, 0.0, 1.0);
|
|
ns_window.setBackgroundColor(Some(&bg_color));
|
|
};
|
|
}
|
|
|
|
let exit_handle = app_handle.handle().clone();
|
|
ctrlc::set_handler(move || {
|
|
println!("Received termination signal, running cleanup...");
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
let cleanup_handle = exit_handle.clone();
|
|
rt.block_on(async {
|
|
cleanup_all_processes(cleanup_handle).await;
|
|
});
|
|
exit_handle.exit(0);
|
|
}).unwrap_or_else(|e| log::error!("Error setting Ctrl-C handler: {e}"));
|
|
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let termination_handle = app_handle.handle().clone();
|
|
utils::app_termination::setup_termination_handler(termination_handle);
|
|
}
|
|
|
|
if !install_state.is_installed {
|
|
log::info!("Installation is INVALID - showing window and navigating to setup");
|
|
if let Some(window) = handle.get_webview_window("main") {
|
|
let _ = window.show();
|
|
let _ = window.set_focus();
|
|
let _ = window.eval("localStorage.clear(); console.log('localStorage cleared due to INVALID installation');");
|
|
let _ = window.eval("window.location.href = '/setup'");
|
|
}
|
|
} else {
|
|
// VALID INSTALLATION
|
|
log::info!("Installation is VALID");
|
|
|
|
let update_handle = handle.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
log::debug!("Starting background update check...");
|
|
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
|
|
background_update_check(update_handle).await;
|
|
});
|
|
|
|
if let Some(window) = handle.get_webview_window("main") {
|
|
let _ = window.eval("localStorage.setItem('environments-first-load-done', 'true');");
|
|
|
|
if show_after_update {
|
|
log::info!("SHOWING WINDOW AFTER UPDATE RESTART");
|
|
let _ = window.show();
|
|
}
|
|
|
|
let _ = window.set_focus();
|
|
}
|
|
}
|
|
Ok(())
|
|
})
|
|
.build(tauri::generate_context!())
|
|
.unwrap_or_else(|e| {
|
|
log::error!("Error while building Tauri application: {e}");
|
|
std::process::exit(1);
|
|
})
|
|
.run(|app_handle, event| {
|
|
let is_restart_requested = Arc::new(AtomicBool::new(false));
|
|
let is_restart_requested_clone = is_restart_requested.clone();
|
|
|
|
if let tauri::RunEvent::ExitRequested { code, ..} = event
|
|
&& code.unwrap() == RESTART_EXIT_CODE {
|
|
is_restart_requested.store(true, Ordering::SeqCst);
|
|
}
|
|
|
|
if let tauri::RunEvent::ExitRequested { ref api, .. } = event {
|
|
log::debug!("Caught applicationWillTerminate event, running cleanup...");
|
|
api.prevent_exit();
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
let cleanup_handle = app_handle.clone();
|
|
rt.block_on(async {
|
|
cleanup_all_processes(cleanup_handle).await;
|
|
});
|
|
if is_restart_requested_clone.load(Ordering::SeqCst) {
|
|
log::debug!("Restart requested, exiting with restart code");
|
|
tauri::process::restart(&app_handle.env());
|
|
} else {
|
|
log::debug!("Exiting application normally");
|
|
std::process::exit(0);
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
if let tauri::RunEvent::Reopen { .. } = event
|
|
&& let Some(window) = app_handle.get_webview_window("main") {
|
|
let _ = window.show();
|
|
let _ = window.set_focus();
|
|
}
|
|
}
|
|
|
|
if let tauri::RunEvent::Exit = event
|
|
&& is_restart_requested_clone.load(Ordering::SeqCst) {
|
|
app_handle.cleanup_before_exit();
|
|
let env = app_handle.env();
|
|
tauri::process::restart(&env);
|
|
}
|
|
});
|
|
}
|