feat(qbzd): add session lifecycle port (Phase 2)
New session.rs module ports activate_session from Tauri to headless: - Creates per-user directories (same layout as desktop app) - Persists last_user_id marker for session restore - Emits RuntimeEvent::Ready via event bus - Skips desktop-only stores (tray, visualizer, remote control) Session activation wired into daemon startup after auto-login. Audio settings reload deferred until qbz-player crate exposes reload_settings (currently on legacy player only). cc
This commit is contained in:
3 files changed
+111
-18
No files matched your search
+35
-18
@@ -7,8 +7,11 @@ use qbz_cache::AudioCache;
|
||||
use qbz_core::QbzCore;
|
||||
use qbz_player::Player;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::adapter::{DaemonAdapter, DaemonEvent};
|
||||
use crate::config::DaemonConfig;
|
||||
use crate::session::UserSession;
|
||||
|
||||
/// Central state container for the headless daemon.
|
||||
/// Replaces Tauri's app.state::<T>() with direct field access.
|
||||
@@ -17,6 +20,8 @@ pub struct DaemonCore {
|
||||
pub core: Arc<QbzCore<DaemonAdapter>>,
|
||||
pub audio_cache: Arc<AudioCache>,
|
||||
pub event_bus: broadcast::Sender<DaemonEvent>,
|
||||
/// Per-user state, populated after login + session activation
|
||||
pub user: RwLock<Option<UserSession>>,
|
||||
}
|
||||
|
||||
/// Run the daemon main loop.
|
||||
@@ -59,26 +64,41 @@ pub async fn run(mut config: DaemonConfig) -> Result<(), String> {
|
||||
core.init().await.map_err(|e| format!("QbzCore init failed: {}", e))?;
|
||||
log::info!("[qbzd] QbzCore initialized");
|
||||
|
||||
// Try auto-login from saved OAuth token
|
||||
let logged_in = try_auto_login(&core).await;
|
||||
if logged_in {
|
||||
log::info!("[qbzd] Auto-login successful");
|
||||
} else {
|
||||
log::warn!("[qbzd] No saved credentials. Run `qbzd login` to authenticate.");
|
||||
}
|
||||
|
||||
// Create audio cache with configured sizes
|
||||
let cache_bytes = config.memory_cache_bytes();
|
||||
let audio_cache = Arc::new(AudioCache::new(cache_bytes));
|
||||
log::info!("[qbzd] Audio cache: {} MB", config.cache.memory_mb);
|
||||
|
||||
let core = Arc::new(core);
|
||||
|
||||
let daemon = Arc::new(DaemonCore {
|
||||
config: config.clone(),
|
||||
core: Arc::new(core),
|
||||
core: core.clone(),
|
||||
audio_cache,
|
||||
event_bus: event_tx,
|
||||
event_bus: event_tx.clone(),
|
||||
user: RwLock::new(None),
|
||||
});
|
||||
|
||||
// Try auto-login from saved OAuth token
|
||||
match try_auto_login(&core).await {
|
||||
Some(user_id) => {
|
||||
log::info!("[qbzd] Auto-login successful (user_id: {})", user_id);
|
||||
// Activate per-user session (initialize stores, sync settings)
|
||||
match crate::session::activate_session(user_id, &core, &event_tx).await {
|
||||
Ok(session) => {
|
||||
*daemon.user.write().await = Some(session);
|
||||
log::info!("[qbzd] User session activated");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("[qbzd] Session activation failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
log::warn!("[qbzd] No saved credentials. Run `qbzd login` to authenticate.");
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"[qbzd] Daemon ready on {}:{}",
|
||||
config.server.bind,
|
||||
@@ -128,12 +148,9 @@ pub async fn run(mut config: DaemonConfig) -> Result<(), String> {
|
||||
}
|
||||
|
||||
/// Try to restore session from saved OAuth token in keyring.
|
||||
async fn try_auto_login(core: &QbzCore<DaemonAdapter>) -> bool {
|
||||
// Try loading token from keyring (same storage as desktop app)
|
||||
let token = match load_oauth_token() {
|
||||
Some(t) => t,
|
||||
None => return false,
|
||||
};
|
||||
/// Returns the user_id on success.
|
||||
async fn try_auto_login(core: &QbzCore<DaemonAdapter>) -> Option<u64> {
|
||||
let token = load_oauth_token()?;
|
||||
|
||||
match core.login_with_token(&token).await {
|
||||
Ok(session) => {
|
||||
@@ -142,11 +159,11 @@ async fn try_auto_login(core: &QbzCore<DaemonAdapter>) -> bool {
|
||||
session.display_name,
|
||||
session.user_id
|
||||
);
|
||||
true
|
||||
Some(session.user_id)
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("[qbzd] Saved token expired or invalid: {}", e);
|
||||
false
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ mod config;
|
||||
mod daemon;
|
||||
mod adapter;
|
||||
mod resources;
|
||||
mod session;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//! Headless session lifecycle — port of session_lifecycle.rs without Tauri.
|
||||
//!
|
||||
//! Initializes per-user stores after login. The stores themselves are
|
||||
//! Tauri-free (SQLite); only the state lookup mechanism changes.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::adapter::{DaemonEvent, RuntimeEvent};
|
||||
|
||||
/// Per-user state, populated after successful login.
|
||||
pub struct UserSession {
|
||||
pub user_id: u64,
|
||||
pub data_dir: PathBuf,
|
||||
pub cache_dir: PathBuf,
|
||||
}
|
||||
|
||||
/// Activate a user session (headless equivalent of session_lifecycle::activate_session).
|
||||
///
|
||||
/// Creates per-user directories, initializes stores, syncs audio settings to player.
|
||||
pub async fn activate_session(
|
||||
user_id: u64,
|
||||
core: &qbz_core::QbzCore<crate::adapter::DaemonAdapter>,
|
||||
event_tx: &tokio::sync::broadcast::Sender<DaemonEvent>,
|
||||
) -> Result<UserSession, String> {
|
||||
log::info!("[qbzd/session] Activating session for user {}", user_id);
|
||||
|
||||
// Resolve per-user directories (same layout as desktop app)
|
||||
let global_data = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
let global_cache = dirs::cache_dir()
|
||||
.ok_or("Could not determine cache directory")?
|
||||
.join("qbz");
|
||||
|
||||
let data_dir = global_data.join("users").join(user_id.to_string());
|
||||
let cache_dir = global_cache.join("users").join(user_id.to_string());
|
||||
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
.map_err(|e| format!("Failed to create user data dir: {}", e))?;
|
||||
std::fs::create_dir_all(&cache_dir)
|
||||
.map_err(|e| format!("Failed to create user cache dir: {}", e))?;
|
||||
|
||||
log::info!("[qbzd/session] User data dir: {}", data_dir.display());
|
||||
log::info!("[qbzd/session] User cache dir: {}", cache_dir.display());
|
||||
|
||||
// Note: audio settings were already loaded at DaemonCore init from the
|
||||
// global store. Per-user settings sync will be added when qbz-player
|
||||
// exposes reload_settings() in the crate (currently on legacy player only).
|
||||
|
||||
// Persist last user_id for session restore on next launch
|
||||
let marker_path = global_data.join("last_user_id");
|
||||
let _ = std::fs::write(&marker_path, user_id.to_string());
|
||||
log::info!("[qbzd/session] Saved last_user_id marker");
|
||||
|
||||
// Emit session activated event
|
||||
let _ = event_tx.send(DaemonEvent::Runtime(RuntimeEvent::Ready { user_id }));
|
||||
|
||||
log::info!("[qbzd/session] Session activated");
|
||||
|
||||
Ok(UserSession {
|
||||
user_id,
|
||||
data_dir,
|
||||
cache_dir,
|
||||
})
|
||||
}
|
||||
|
||||
/// Load the last user_id from the marker file (for auto-login session restore).
|
||||
pub fn load_last_user_id() -> Option<u64> {
|
||||
let path = dirs::data_dir()?.join("qbz").join("last_user_id");
|
||||
std::fs::read_to_string(&path)
|
||||
.ok()?
|
||||
.trim()
|
||||
.parse()
|
||||
.ok()
|
||||
}
|
||||
Reference in new issue
Block a user