refactored app startup flow
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
use tauri;
|
use tauri;
|
||||||
use tracing;
|
use tracing;
|
||||||
|
|
||||||
|
use crate::init::lifecycle;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn logout_and_restart() -> Result<(), String> {
|
pub async fn logout_and_restart() -> Result<(), String> {
|
||||||
crate::services::auth::logout_and_restart()
|
crate::services::auth::logout_and_restart()
|
||||||
@@ -18,7 +20,7 @@ pub fn start_auth_flow() -> Result<(), String> {
|
|||||||
// Close welcome window if it's still open
|
// Close welcome window if it's still open
|
||||||
crate::services::welcome::close_welcome_window();
|
crate::services::welcome::close_welcome_window();
|
||||||
tauri::async_runtime::spawn(async {
|
tauri::async_runtime::spawn(async {
|
||||||
crate::startup::bootstrap().await;
|
lifecycle::handle_authentication_flow().await;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
|
|||||||
@@ -1,77 +1,59 @@
|
|||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::time::{sleep, Instant};
|
use tokio::time::sleep;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
init::startup::{initialize_app_data_and_connections, transition_to_main_interface},
|
||||||
|
lock_w,
|
||||||
models::health::HealthError,
|
models::health::HealthError,
|
||||||
remotes::health::HealthRemote,
|
remotes::health::HealthRemote,
|
||||||
services::{
|
services::{
|
||||||
auth::{get_access_token, get_tokens},
|
active_app::init_active_app_changes_listener,
|
||||||
scene::{close_splash_window, open_scene_window, open_splash_window},
|
auth::get_tokens,
|
||||||
|
health_manager::show_health_manager_with_error,
|
||||||
|
scene::close_splash_window,
|
||||||
welcome::open_welcome_window,
|
welcome::open_welcome_window,
|
||||||
ws::init_ws_client,
|
|
||||||
},
|
},
|
||||||
state::init_app_data,
|
state::FDOLL,
|
||||||
system_tray::update_system_tray,
|
system_tray::{init_system_tray, update_system_tray},
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn init_ws_after_auth() {
|
/// Initializes and starts the core app lifecycle after initial setup.
|
||||||
const MAX_ATTEMPTS: u8 = 5;
|
///
|
||||||
const BACKOFF: Duration = Duration::from_millis(300);
|
/// This function handles:
|
||||||
|
/// - System tray initialization and storage in app state
|
||||||
for _attempt in 1..=MAX_ATTEMPTS {
|
/// - Active app change listener setup
|
||||||
if get_access_token().await.is_some() {
|
/// - Startup sequence execution with error handling
|
||||||
init_ws_client().await;
|
///
|
||||||
return;
|
/// # Errors
|
||||||
}
|
/// If the startup sequence fails, displays a health manager dialog
|
||||||
|
/// with the error details.
|
||||||
sleep(BACKOFF).await;
|
///
|
||||||
}
|
/// # Example
|
||||||
}
|
/// ```
|
||||||
|
/// // Called automatically during app setup in initialize_app_environment()
|
||||||
async fn construct_app() {
|
/// lifecycle::launch_core_services().await;
|
||||||
open_splash_window();
|
/// ```
|
||||||
|
pub async fn launch_core_services() {
|
||||||
// Record start time for minimum splash duration
|
let tray = init_system_tray();
|
||||||
let start = Instant::now();
|
{
|
||||||
|
let mut guard = lock_w!(FDOLL);
|
||||||
// Initialize app data first so we only start WebSocket after auth is fully available
|
guard.tray = Some(tray);
|
||||||
init_app_data().await;
|
|
||||||
|
|
||||||
// Initialize WebSocket client after we know auth is present
|
|
||||||
init_ws_after_auth().await;
|
|
||||||
|
|
||||||
// Ensure splash stays visible for at least 3 seconds
|
|
||||||
let elapsed = start.elapsed();
|
|
||||||
if elapsed < Duration::from_secs(3) {
|
|
||||||
sleep(Duration::from_secs(3) - elapsed).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close splash and open main scene
|
// Begin listening for foreground app changes
|
||||||
close_splash_window();
|
init_active_app_changes_listener();
|
||||||
open_scene_window();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn bootstrap() {
|
if let Err(err) = validate_environment_and_start_app().await {
|
||||||
match get_tokens().await {
|
tracing::warn!("Startup sequence encountered an error: {}", err);
|
||||||
Some(_tokens) => {
|
show_health_manager_with_error(Some(err.to_string()));
|
||||||
info!("Tokens found in keyring - restoring user session");
|
|
||||||
construct_app().await;
|
|
||||||
update_system_tray(true);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
info!("No active session found - showing welcome first");
|
|
||||||
open_welcome_window();
|
|
||||||
close_splash_window();
|
|
||||||
update_system_tray(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform checks for environment, network condition
|
/// Perform checks for environment, network condition
|
||||||
/// and handle situations where startup would not be appropriate.
|
/// and handle situations where startup would not be appropriate.
|
||||||
pub async fn init_startup_sequence() -> Result<(), HealthError> {
|
pub async fn validate_environment_and_start_app() -> Result<(), HealthError> {
|
||||||
let health_remote = HealthRemote::try_new()?;
|
let health_remote = HealthRemote::try_new()?;
|
||||||
|
|
||||||
// simple retry loop to smooth transient network issues
|
// simple retry loop to smooth transient network issues
|
||||||
@@ -81,7 +63,7 @@ pub async fn init_startup_sequence() -> Result<(), HealthError> {
|
|||||||
for attempt in 1..=MAX_ATTEMPTS {
|
for attempt in 1..=MAX_ATTEMPTS {
|
||||||
match health_remote.get_health().await {
|
match health_remote.get_health().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
bootstrap().await;
|
handle_authentication_flow().await;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(HealthError::NonOkStatus(status)) => {
|
Err(HealthError::NonOkStatus(status)) => {
|
||||||
@@ -109,3 +91,21 @@ pub async fn init_startup_sequence() -> Result<(), HealthError> {
|
|||||||
StatusCode::SERVICE_UNAVAILABLE,
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles authentication flow: checks for tokens and either restores session or shows welcome.
|
||||||
|
pub async fn handle_authentication_flow() {
|
||||||
|
match get_tokens().await {
|
||||||
|
Some(_tokens) => {
|
||||||
|
info!("Tokens found in keyring - restoring user session");
|
||||||
|
let start = initialize_app_data_and_connections().await;
|
||||||
|
transition_to_main_interface(start).await;
|
||||||
|
update_system_tray(true);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
info!("No active session found - showing welcome first");
|
||||||
|
open_welcome_window();
|
||||||
|
close_splash_window();
|
||||||
|
update_system_tray(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src-tauri/src/init/mod.rs
Normal file
3
src-tauri/src/init/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod lifecycle;
|
||||||
|
pub mod startup;
|
||||||
|
pub mod tracing;
|
||||||
52
src-tauri/src/init/startup.rs
Normal file
52
src-tauri/src/init/startup.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
use tokio::time::{sleep, Instant};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
services::{
|
||||||
|
auth::get_access_token,
|
||||||
|
scene::{close_splash_window, open_scene_window, open_splash_window},
|
||||||
|
ws::init_ws_client,
|
||||||
|
},
|
||||||
|
state::init_app_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
async fn establish_websocket_connection() {
|
||||||
|
const MAX_ATTEMPTS: u8 = 5;
|
||||||
|
const BACKOFF: Duration = Duration::from_millis(300);
|
||||||
|
|
||||||
|
for _attempt in 1..=MAX_ATTEMPTS {
|
||||||
|
if get_access_token().await.is_some() {
|
||||||
|
init_ws_client().await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(BACKOFF).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn initialize_app_data_and_connections() -> Instant {
|
||||||
|
open_splash_window();
|
||||||
|
|
||||||
|
// Record start time for minimum splash duration
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
// Initialize app data first so we only start WebSocket after auth is fully available
|
||||||
|
init_app_data().await;
|
||||||
|
|
||||||
|
// Initialize WebSocket client after we know auth is present
|
||||||
|
establish_websocket_connection().await;
|
||||||
|
|
||||||
|
start
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn transition_to_main_interface(start: Instant) {
|
||||||
|
// Ensure splash stays visible for at least 3 seconds
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
if elapsed < Duration::from_secs(3) {
|
||||||
|
sleep(Duration::from_secs(3) - elapsed).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close splash and open main scene
|
||||||
|
close_splash_window();
|
||||||
|
open_scene_window();
|
||||||
|
}
|
||||||
49
src-tauri/src/init/tracing.rs
Normal file
49
src-tauri/src/init/tracing.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use tauri::Manager;
|
||||||
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
|
|
||||||
|
use crate::get_app_handle;
|
||||||
|
|
||||||
|
/// Initialize `tracing_subscriber` for logging to file & console
|
||||||
|
pub fn setup_logging() {
|
||||||
|
// Set up file appender
|
||||||
|
let app_handle = get_app_handle();
|
||||||
|
let app_log_dir = app_handle
|
||||||
|
.path()
|
||||||
|
.app_log_dir()
|
||||||
|
.expect("Could not determine app log dir");
|
||||||
|
|
||||||
|
// Create the directory if it doesn't exist
|
||||||
|
if let Err(e) = std::fs::create_dir_all(&app_log_dir) {
|
||||||
|
eprintln!("Failed to create log directory: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_appender = tracing_appender::rolling::daily(&app_log_dir, "friendolls.log");
|
||||||
|
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
|
||||||
|
|
||||||
|
// Create a filter - adjust the level as needed (trace, debug, info, warn, error)
|
||||||
|
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
|
||||||
|
|
||||||
|
// Create a layer that writes to the file
|
||||||
|
let file_layer = tracing_subscriber::fmt::layer()
|
||||||
|
.with_target(false)
|
||||||
|
.with_thread_ids(false)
|
||||||
|
.with_file(true)
|
||||||
|
.with_line_number(true)
|
||||||
|
.with_writer(non_blocking);
|
||||||
|
|
||||||
|
// Create a layer that writes to stdout (console)
|
||||||
|
let console_layer = tracing_subscriber::fmt::layer()
|
||||||
|
.with_target(false)
|
||||||
|
.with_thread_ids(false)
|
||||||
|
.with_file(true)
|
||||||
|
.with_line_number(true);
|
||||||
|
|
||||||
|
// Combine both layers with filter
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(filter)
|
||||||
|
.with(file_layer)
|
||||||
|
.with(console_layer)
|
||||||
|
.init();
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
use crate::services::{
|
use crate::{
|
||||||
cursor::start_cursor_tracking,
|
init::tracing::setup_logging,
|
||||||
doll_editor::open_doll_editor_window,
|
services::{
|
||||||
scene::{open_splash_window, set_pet_menu_state, set_scene_interactive},
|
cursor::start_cursor_tracking,
|
||||||
|
doll_editor::open_doll_editor_window,
|
||||||
|
scene::{open_splash_window, set_pet_menu_state, set_scene_interactive},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use commands::app::{quit_app, restart_app};
|
use commands::app::{quit_app, restart_app};
|
||||||
use commands::app_data::{get_app_data, refresh_app_data};
|
use commands::app_data::{get_app_data, refresh_app_data};
|
||||||
@@ -18,22 +21,18 @@ use commands::interaction::send_interaction_cmd;
|
|||||||
use commands::sprite::recolor_gif_base64;
|
use commands::sprite::recolor_gif_base64;
|
||||||
use commands::user_status::send_user_status_cmd;
|
use commands::user_status::send_user_status_cmd;
|
||||||
use tauri::async_runtime;
|
use tauri::async_runtime;
|
||||||
use tauri::Manager;
|
|
||||||
use tracing_subscriber::{self, util::SubscriberInitExt};
|
|
||||||
|
|
||||||
static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
|
static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
mod commands;
|
mod commands;
|
||||||
mod lifecycle;
|
mod init;
|
||||||
mod models;
|
mod models;
|
||||||
mod remotes;
|
mod remotes;
|
||||||
mod services;
|
mod services;
|
||||||
mod startup;
|
|
||||||
mod state;
|
mod state;
|
||||||
mod system_tray;
|
mod system_tray;
|
||||||
mod utilities;
|
mod utilities;
|
||||||
|
|
||||||
|
|
||||||
/// Tauri app handle
|
/// Tauri app handle
|
||||||
pub fn get_app_handle<'a>() -> &'a tauri::AppHandle<tauri::Wry> {
|
pub fn get_app_handle<'a>() -> &'a tauri::AppHandle<tauri::Wry> {
|
||||||
APP_HANDLE
|
APP_HANDLE
|
||||||
@@ -41,55 +40,11 @@ pub fn get_app_handle<'a>() -> &'a tauri::AppHandle<tauri::Wry> {
|
|||||||
.expect("get_app_handle called but app is still not initialized")
|
.expect("get_app_handle called but app is still not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_fdoll() -> Result<(), tauri::Error> {
|
fn initialize_app_environment() -> Result<(), tauri::Error> {
|
||||||
// Initialize tracing subscriber for logging
|
setup_logging();
|
||||||
|
|
||||||
// Set up file appender
|
|
||||||
let app_handle = get_app_handle();
|
|
||||||
let app_log_dir = app_handle
|
|
||||||
.path()
|
|
||||||
.app_log_dir()
|
|
||||||
.expect("Could not determine app log dir");
|
|
||||||
|
|
||||||
// Create the directory if it doesn't exist
|
|
||||||
if let Err(e) = std::fs::create_dir_all(&app_log_dir) {
|
|
||||||
eprintln!("Failed to create log directory: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
let file_appender = tracing_appender::rolling::daily(&app_log_dir, "friendolls.log");
|
|
||||||
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
|
|
||||||
|
|
||||||
// Create a filter - adjust the level as needed (trace, debug, info, warn, error)
|
|
||||||
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
|
||||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
|
|
||||||
|
|
||||||
// Create a layer that writes to the file
|
|
||||||
let file_layer = tracing_subscriber::fmt::layer()
|
|
||||||
.with_target(false)
|
|
||||||
.with_thread_ids(false)
|
|
||||||
.with_file(true)
|
|
||||||
.with_line_number(true)
|
|
||||||
.with_writer(non_blocking);
|
|
||||||
|
|
||||||
// Create a layer that writes to stdout (console)
|
|
||||||
let console_layer = tracing_subscriber::fmt::layer()
|
|
||||||
.with_target(false)
|
|
||||||
.with_thread_ids(false)
|
|
||||||
.with_file(true)
|
|
||||||
.with_line_number(true);
|
|
||||||
|
|
||||||
// Combine both layers with filter
|
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
|
||||||
tracing_subscriber::registry()
|
|
||||||
.with(filter)
|
|
||||||
.with(file_layer)
|
|
||||||
.with(console_layer)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
open_splash_window();
|
open_splash_window();
|
||||||
|
state::init_fdoll_state();
|
||||||
state::init_fdoll_state(Some(_guard));
|
async_runtime::spawn(async move { init::lifecycle::launch_core_services().await });
|
||||||
async_runtime::spawn(async move { lifecycle::start_fdoll().await });
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +102,7 @@ pub fn run() {
|
|||||||
APP_HANDLE
|
APP_HANDLE
|
||||||
.set(app.handle().to_owned())
|
.set(app.handle().to_owned())
|
||||||
.expect("Failed to init app handle.");
|
.expect("Failed to init app handle.");
|
||||||
setup_fdoll().expect("Failed to setup app.");
|
initialize_app_environment().expect("Failed to setup app.");
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
lock_w,
|
|
||||||
services::{
|
|
||||||
active_app::init_active_app_changes_listener,
|
|
||||||
health_manager::show_health_manager_with_error,
|
|
||||||
},
|
|
||||||
startup::init_startup_sequence,
|
|
||||||
state::FDOLL,
|
|
||||||
system_tray::init_system_tray,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Initializes and starts the core app lifecycle after initial setup.
|
|
||||||
///
|
|
||||||
/// This function handles:
|
|
||||||
/// - System tray initialization and storage in app state
|
|
||||||
/// - Active app change listener setup
|
|
||||||
/// - Startup sequence execution with error handling
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// If the startup sequence fails, displays a health manager dialog
|
|
||||||
/// with the error details.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```
|
|
||||||
/// // Called automatically during app setup in setup_fdoll()
|
|
||||||
/// lifecycle::start_fdoll().await;
|
|
||||||
/// ```
|
|
||||||
pub async fn start_fdoll() {
|
|
||||||
let tray = init_system_tray();
|
|
||||||
{
|
|
||||||
let mut guard = lock_w!(FDOLL);
|
|
||||||
guard.tray = Some(tray);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Begin listening for foreground app changes
|
|
||||||
init_active_app_changes_listener();
|
|
||||||
|
|
||||||
if let Err(err) = init_startup_sequence().await {
|
|
||||||
tracing::warn!("Startup sequence encountered an error: {}", err);
|
|
||||||
show_health_manager_with_error(Some(err.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
// in app-core/src/state.rs
|
// in app-core/src/state.rs
|
||||||
use crate::{lock_w};
|
use crate::lock_w;
|
||||||
use std::{
|
use std::sync::{Arc, LazyLock, RwLock};
|
||||||
sync::{Arc, LazyLock, RwLock},
|
|
||||||
};
|
|
||||||
use tauri::tray::TrayIcon;
|
use tauri::tray::TrayIcon;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
mod network;
|
|
||||||
mod auth;
|
mod auth;
|
||||||
|
mod network;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
pub use network::*;
|
|
||||||
pub use auth::*;
|
pub use auth::*;
|
||||||
|
pub use network::*;
|
||||||
pub use ui::*;
|
pub use ui::*;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -20,7 +18,6 @@ pub struct AppState {
|
|||||||
pub network: NetworkState,
|
pub network: NetworkState,
|
||||||
pub auth: AuthState,
|
pub auth: AuthState,
|
||||||
pub ui: UiState,
|
pub ui: UiState,
|
||||||
pub tracing_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
|
|
||||||
pub tray: Option<TrayIcon>,
|
pub tray: Option<TrayIcon>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,11 +26,10 @@ pub struct AppState {
|
|||||||
pub static FDOLL: LazyLock<Arc<RwLock<AppState>>> =
|
pub static FDOLL: LazyLock<Arc<RwLock<AppState>>> =
|
||||||
LazyLock::new(|| Arc::new(RwLock::new(AppState::default())));
|
LazyLock::new(|| Arc::new(RwLock::new(AppState::default())));
|
||||||
|
|
||||||
pub fn init_fdoll_state(tracing_guard: Option<tracing_appender::non_blocking::WorkerGuard>) {
|
pub fn init_fdoll_state() {
|
||||||
{
|
{
|
||||||
let mut guard = lock_w!(FDOLL);
|
let mut guard = lock_w!(FDOLL);
|
||||||
dotenvy::dotenv().ok();
|
dotenvy::dotenv().ok();
|
||||||
guard.tracing_guard = tracing_guard;
|
|
||||||
guard.app_config = crate::services::client_config_manager::load_app_config();
|
guard.app_config = crate::services::client_config_manager::load_app_config();
|
||||||
guard.network = init_network_state();
|
guard.network = init_network_state();
|
||||||
guard.auth = init_auth_state();
|
guard.auth = init_auth_state();
|
||||||
|
|||||||
Reference in New Issue
Block a user