minor refinements
This commit is contained in:
@@ -318,7 +318,7 @@ pub fn clear_auth_pass() -> Result<(), OAuthError> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use crate::core::services::auth::logout;
|
||||
/// use crate::services::auth::logout;
|
||||
///
|
||||
/// logout().expect("Failed to logout");
|
||||
/// ```
|
||||
@@ -342,7 +342,7 @@ pub fn logout() -> Result<(), OAuthError> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use crate::core::services::auth::with_auth;
|
||||
/// use crate::services::auth::with_auth;
|
||||
///
|
||||
/// let client = reqwest::Client::new();
|
||||
/// let request = client.get("https://api.example.com/user");
|
||||
@@ -370,6 +370,7 @@ pub async fn with_auth(request: reqwest::RequestBuilder) -> reqwest::RequestBuil
|
||||
///
|
||||
/// Returns `OAuthError` if the exchange fails or the server returns an error.
|
||||
pub async fn exchange_code_for_auth_pass(
|
||||
redirect_uri: &str,
|
||||
callback_params: OAuthCallbackParams,
|
||||
code_verifier: &str,
|
||||
) -> Result<AuthPass, OAuthError> {
|
||||
@@ -393,7 +394,7 @@ pub async fn exchange_code_for_auth_pass(
|
||||
let body = form_urlencoded::Serializer::new(String::new())
|
||||
.append_pair("client_id", &app_config.auth.audience)
|
||||
.append_pair("grant_type", "authorization_code")
|
||||
.append_pair("redirect_uri", &app_config.auth.redirect_uri)
|
||||
.append_pair("redirect_uri", redirect_uri)
|
||||
.append_pair("code", &callback_params.code)
|
||||
.append_pair("code_verifier", code_verifier)
|
||||
.finish();
|
||||
@@ -469,7 +470,7 @@ pub async fn exchange_code_for_auth_pass(
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use crate::core::services::auth::init_auth_code_retrieval;
|
||||
/// use crate::services::auth::init_auth_code_retrieval;
|
||||
///
|
||||
/// init_auth_code_retrieval();
|
||||
/// // User will be prompted to login in their browser
|
||||
@@ -511,24 +512,19 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
url.query_pairs_mut()
|
||||
.append_pair("client_id", &app_config.auth.audience)
|
||||
.append_pair("response_type", "code")
|
||||
.append_pair("redirect_uri", &app_config.auth.redirect_uri)
|
||||
.append_pair("scope", "openid email profile")
|
||||
.append_pair("state", &state)
|
||||
.append_pair("code_challenge", &code_challenge)
|
||||
.append_pair("code_challenge_method", "S256");
|
||||
|
||||
info!("Initiating OAuth flow");
|
||||
|
||||
// Bind the server FIRST to ensure port is open
|
||||
// We bind synchronously using std::net::TcpListener then convert to tokio::net::TcpListener
|
||||
// to ensure the port is bound before we open the browser.
|
||||
info!("Attempting to bind to: {}", app_config.auth.redirect_host);
|
||||
let std_listener = match std::net::TcpListener::bind(&app_config.auth.redirect_host) {
|
||||
|
||||
// Bind to port 0 (ephemeral port),
|
||||
// The OS will assign an available port.
|
||||
let bind_addr = "localhost:0";
|
||||
|
||||
info!("Attempting to bind to: {}", bind_addr);
|
||||
let std_listener = match std::net::TcpListener::bind(&bind_addr) {
|
||||
Ok(s) => {
|
||||
info!("Successfully bound to {}", app_config.auth.redirect_host);
|
||||
s.set_nonblocking(true).unwrap();
|
||||
s
|
||||
}
|
||||
@@ -538,68 +534,75 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
"Listening on {} for /callback",
|
||||
app_config.auth.redirect_host
|
||||
);
|
||||
// Get the actual port assigned by the OS
|
||||
let local_addr = std_listener
|
||||
.local_addr()
|
||||
.map_err(|e| OAuthError::ServerBindError(e.to_string()))?;
|
||||
let port = local_addr.port();
|
||||
info!("Successfully bound to {}", local_addr);
|
||||
info!("Listening on port {} for /callback", port);
|
||||
|
||||
let redirect_uri = format!("http://localhost:{}/callback", port);
|
||||
|
||||
url.query_pairs_mut()
|
||||
.append_pair("client_id", &app_config.auth.audience)
|
||||
.append_pair("response_type", "code")
|
||||
.append_pair("redirect_uri", &redirect_uri)
|
||||
.append_pair("scope", "openid email profile")
|
||||
.append_pair("state", &state)
|
||||
.append_pair("code_challenge", &code_challenge)
|
||||
.append_pair("code_challenge_method", "S256");
|
||||
let redirect_uri_clone = redirect_uri.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
info!("Starting callback listener task");
|
||||
let listener = match TcpListener::from_std(std_listener) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
error!("Failed to create async listener: {}", e);
|
||||
error!("Failed to convert listener: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match listen_for_callback(listener).await {
|
||||
Ok(callback_params) => {
|
||||
// Validate state
|
||||
let stored_state = lock_r!(FDOLL).oauth_flow.state.clone();
|
||||
Ok(params) => {
|
||||
let (stored_state, stored_verifier) = {
|
||||
let guard = lock_r!(FDOLL);
|
||||
(
|
||||
guard.oauth_flow.state.clone(),
|
||||
guard.oauth_flow.code_verifier.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
if stored_state.as_ref() != Some(&callback_params.state) {
|
||||
error!("State mismatch - possible CSRF attack!");
|
||||
if stored_state.as_deref() != Some(params.state.as_str()) {
|
||||
error!("State mismatch");
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve code_verifier
|
||||
let code_verifier = match lock_r!(FDOLL).oauth_flow.code_verifier.clone() {
|
||||
Some(cv) => cv,
|
||||
None => {
|
||||
error!("Code verifier not found in state");
|
||||
return;
|
||||
}
|
||||
let Some(code_verifier) = stored_verifier else {
|
||||
error!("Code verifier missing");
|
||||
return;
|
||||
};
|
||||
|
||||
// Clear OAuth flow state after successful callback
|
||||
lock_w!(FDOLL).oauth_flow = Default::default();
|
||||
|
||||
match exchange_code_for_auth_pass(callback_params, &code_verifier).await {
|
||||
match exchange_code_for_auth_pass(&redirect_uri_clone, params, &code_verifier).await
|
||||
{
|
||||
Ok(auth_pass) => {
|
||||
lock_w!(FDOLL).auth_pass = Some(auth_pass.clone());
|
||||
{
|
||||
let mut guard = lock_w!(FDOLL);
|
||||
guard.auth_pass = Some(auth_pass.clone());
|
||||
guard.oauth_flow = Default::default();
|
||||
}
|
||||
if let Err(e) = save_auth_pass(&auth_pass) {
|
||||
error!("Failed to save auth pass: {}", e);
|
||||
} else {
|
||||
info!("Authentication successful!");
|
||||
crate::services::ws::init_ws_client().await;
|
||||
on_success();
|
||||
return;
|
||||
}
|
||||
on_success();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to exchange code for tokens: {}", e);
|
||||
}
|
||||
Err(e) => error!("Token exchange failed: {}", e),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to receive callback: {}", e);
|
||||
// Clear OAuth flow state on error
|
||||
lock_w!(FDOLL).oauth_flow = Default::default();
|
||||
}
|
||||
Err(e) => error!("Callback listener error: {}", e),
|
||||
}
|
||||
});
|
||||
|
||||
info!("Opening auth URL: {}", url);
|
||||
if let Err(e) = app_handle.opener().open_url(url, None::<&str>) {
|
||||
error!("Failed to open auth portal: {}", e);
|
||||
return Err(OAuthError::OpenPortalFailed(e));
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use device_query::{DeviceEvents, DeviceEventsHandler};
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tauri::Emitter;
|
||||
use tracing::{error, info};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::{get_app_handle, lock_r, state::FDOLL};
|
||||
@@ -14,8 +13,8 @@ use crate::{get_app_handle, lock_r, state::FDOLL};
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct CursorPosition {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, TS)]
|
||||
@@ -28,29 +27,27 @@ pub struct CursorPositions {
|
||||
|
||||
static CURSOR_TRACKER: OnceCell<()> = OnceCell::new();
|
||||
|
||||
/// Convert absolute screen coordinates to grid coordinates
|
||||
pub fn absolute_position_to_grid(pos: &CursorPosition) -> CursorPosition {
|
||||
/// Convert absolute screen coordinates to normalized coordinates (0.0 - 1.0)
|
||||
pub fn absolute_to_normalized(pos: &CursorPosition) -> CursorPosition {
|
||||
let guard = lock_r!(FDOLL);
|
||||
let grid_size = guard.app_data.scene.grid_size;
|
||||
let screen_w = guard.app_data.scene.display.screen_width;
|
||||
let screen_h = guard.app_data.scene.display.screen_height;
|
||||
let screen_w = guard.app_data.scene.display.screen_width as f64;
|
||||
let screen_h = guard.app_data.scene.display.screen_height as f64;
|
||||
|
||||
CursorPosition {
|
||||
x: pos.x * grid_size / screen_w,
|
||||
y: pos.y * grid_size / screen_h,
|
||||
x: (pos.x / screen_w).clamp(0.0, 1.0),
|
||||
y: (pos.y / screen_h).clamp(0.0, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert grid coordinates to absolute screen coordinates
|
||||
pub fn grid_to_absolute_position(grid: &CursorPosition) -> CursorPosition {
|
||||
/// Convert normalized coordinates to absolute screen coordinates
|
||||
pub fn normalized_to_absolute(normalized: &CursorPosition) -> CursorPosition {
|
||||
let guard = lock_r!(FDOLL);
|
||||
let grid_size = guard.app_data.scene.grid_size;
|
||||
let screen_w = guard.app_data.scene.display.screen_width;
|
||||
let screen_h = guard.app_data.scene.display.screen_height;
|
||||
let screen_w = guard.app_data.scene.display.screen_width as f64;
|
||||
let screen_h = guard.app_data.scene.display.screen_height as f64;
|
||||
|
||||
CursorPosition {
|
||||
x: (grid.x * screen_w + grid_size / 2) / grid_size,
|
||||
y: (grid.y * screen_h + grid_size / 2) / grid_size,
|
||||
x: (normalized.x * screen_w).round(),
|
||||
y: (normalized.y * screen_h).round(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,19 +73,40 @@ pub async fn start_cursor_tracking() -> Result<(), String> {
|
||||
|
||||
async fn init_cursor_tracking() -> Result<(), String> {
|
||||
info!("Initializing cursor tracking...");
|
||||
let app_handle = get_app_handle();
|
||||
|
||||
// Create a channel to decouple event generation (producer) from processing (consumer).
|
||||
// Capacity 100 is plenty for 500ms polling (2Hz).
|
||||
let (tx, mut rx) = mpsc::channel::<CursorPositions>(100);
|
||||
|
||||
// Spawn the consumer task
|
||||
// This task handles WebSocket reporting and local broadcasting.
|
||||
// It runs independently of the device event loop.
|
||||
tauri::async_runtime::spawn(async move {
|
||||
info!("Cursor event consumer started");
|
||||
let app_handle = get_app_handle();
|
||||
|
||||
while let Some(positions) = rx.recv().await {
|
||||
let mapped_for_ws = positions.mapped.clone();
|
||||
|
||||
// 1. WebSocket reporting
|
||||
crate::services::ws::report_cursor_data(mapped_for_ws).await;
|
||||
|
||||
// 2. Broadcast to local windows
|
||||
if let Err(e) = app_handle.emit("cursor-position", &positions) {
|
||||
error!("Failed to emit cursor position event: {:?}", e);
|
||||
}
|
||||
}
|
||||
warn!("Cursor event consumer stopped (channel closed)");
|
||||
});
|
||||
|
||||
// Try to initialize the device event handler
|
||||
// Using 500ms sleep as requested by user to reduce CPU usage
|
||||
let device_state = DeviceEventsHandler::new(Duration::from_millis(500))
|
||||
.ok_or("Failed to create device event handler (already running?)")?;
|
||||
|
||||
info!("Device event handler created successfully");
|
||||
info!("Setting up mouse move handler for event broadcasting...");
|
||||
|
||||
let send_count = Arc::new(AtomicU64::new(0));
|
||||
let send_count_clone = Arc::clone(&send_count);
|
||||
let app_handle_clone = app_handle.clone();
|
||||
|
||||
// Get scale factor from global state
|
||||
#[cfg(target_os = "windows")]
|
||||
let scale_factor = {
|
||||
@@ -96,8 +114,11 @@ async fn init_cursor_tracking() -> Result<(), String> {
|
||||
guard.app_data.scene.display.monitor_scale_factor
|
||||
};
|
||||
|
||||
// The producer closure moves `tx` into it.
|
||||
// device_query runs this closure on its own thread.
|
||||
// Explicitly clone tx to ensure clear capture semantics
|
||||
let tx_clone = tx.clone();
|
||||
let _guard = device_state.on_mouse_move(move |position: &(i32, i32)| {
|
||||
|
||||
// `device_query` crate appears to behave
|
||||
// differently on Windows vs other platforms.
|
||||
//
|
||||
@@ -105,46 +126,33 @@ async fn init_cursor_tracking() -> Result<(), String> {
|
||||
// factor on Windows, so we handle it manually.
|
||||
#[cfg(target_os = "windows")]
|
||||
let raw = CursorPosition {
|
||||
x: (position.0 as f64 / scale_factor) as i32,
|
||||
y: (position.1 as f64 / scale_factor) as i32,
|
||||
x: position.0 as f64 / scale_factor,
|
||||
y: position.1 as f64 / scale_factor,
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let raw = CursorPosition {
|
||||
x: position.0,
|
||||
y: position.1,
|
||||
x: position.0 as f64,
|
||||
y: position.1 as f64,
|
||||
};
|
||||
|
||||
let mapped = absolute_position_to_grid(&raw);
|
||||
let mapped = absolute_to_normalized(&raw);
|
||||
|
||||
let positions = CursorPositions {
|
||||
raw,
|
||||
mapped: mapped.clone(),
|
||||
mapped,
|
||||
};
|
||||
|
||||
// Report to server (existing functionality)
|
||||
let mapped_for_ws = mapped.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
crate::services::ws::report_cursor_data(mapped_for_ws).await;
|
||||
});
|
||||
|
||||
// Broadcast to ALL windows using events
|
||||
match app_handle_clone.emit("cursor-position", &positions) {
|
||||
Ok(_) => {
|
||||
let count = send_count_clone.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
if count % 100 == 0 {
|
||||
info!("Broadcast {} cursor position updates to all windows. Latest: raw({}, {}), mapped({}, {})",
|
||||
count, positions.raw.x, positions.raw.y, positions.mapped.x, positions.mapped.y);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to emit cursor position event: {:?}", e);
|
||||
}
|
||||
// Send to consumer channel (non-blocking)
|
||||
if let Err(e) = tx_clone.try_send(positions) {
|
||||
debug!("Failed to send cursor position to channel: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
info!("Mouse move handler registered - now broadcasting cursor events to all windows");
|
||||
|
||||
// Keep the handler alive forever
|
||||
// This loop is necessary to keep `_guard` and `device_state` in scope.
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(3600)).await;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ use tauri::{async_runtime, Emitter};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
get_app_handle, lock_r, lock_w, models::app_config::AppConfig,
|
||||
services::cursor::{grid_to_absolute_position, CursorPosition, CursorPositions},
|
||||
get_app_handle, lock_r, lock_w,
|
||||
models::app_config::AppConfig,
|
||||
services::cursor::{normalized_to_absolute, CursorPosition, CursorPositions},
|
||||
state::FDOLL,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -41,9 +42,9 @@ fn on_friend_request_received(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(str) => {
|
||||
println!("Received friend request: {:?}", str);
|
||||
get_app_handle()
|
||||
.emit(WS_EVENT::FRIEND_REQUEST_RECEIVED, str)
|
||||
.unwrap();
|
||||
if let Err(e) = get_app_handle().emit(WS_EVENT::FRIEND_REQUEST_RECEIVED, str) {
|
||||
error!("Failed to emit friend request received event: {:?}", e);
|
||||
}
|
||||
}
|
||||
_ => error!("Received unexpected payload format for friend request received"),
|
||||
}
|
||||
@@ -53,9 +54,9 @@ fn on_friend_request_accepted(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(str) => {
|
||||
println!("Received friend request accepted: {:?}", str);
|
||||
get_app_handle()
|
||||
.emit(WS_EVENT::FRIEND_REQUEST_ACCEPTED, str)
|
||||
.unwrap();
|
||||
if let Err(e) = get_app_handle().emit(WS_EVENT::FRIEND_REQUEST_ACCEPTED, str) {
|
||||
error!("Failed to emit friend request accepted event: {:?}", e);
|
||||
}
|
||||
}
|
||||
_ => error!("Received unexpected payload format for friend request accepted"),
|
||||
}
|
||||
@@ -65,9 +66,9 @@ fn on_friend_request_denied(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(str) => {
|
||||
println!("Received friend request denied: {:?}", str);
|
||||
get_app_handle()
|
||||
.emit(WS_EVENT::FRIEND_REQUEST_DENIED, str)
|
||||
.unwrap();
|
||||
if let Err(e) = get_app_handle().emit(WS_EVENT::FRIEND_REQUEST_DENIED, str) {
|
||||
error!("Failed to emit friend request denied event: {:?}", e);
|
||||
}
|
||||
}
|
||||
_ => error!("Received unexpected payload format for friend request denied"),
|
||||
}
|
||||
@@ -77,7 +78,9 @@ fn on_unfriended(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(str) => {
|
||||
println!("Received unfriended: {:?}", str);
|
||||
get_app_handle().emit(WS_EVENT::UNFRIENDED, str).unwrap();
|
||||
if let Err(e) = get_app_handle().emit(WS_EVENT::UNFRIENDED, str) {
|
||||
error!("Failed to emit unfriended event: {:?}", e);
|
||||
}
|
||||
}
|
||||
_ => error!("Received unexpected payload format for unfriended"),
|
||||
}
|
||||
@@ -88,15 +91,16 @@ fn on_friend_cursor_position(payload: Payload, _socket: RawClient) {
|
||||
Payload::Text(values) => {
|
||||
// values is Vec<serde_json::Value>
|
||||
if let Some(first_value) = values.first() {
|
||||
let incoming_data: Result<IncomingFriendCursorPayload, _> = serde_json::from_value(first_value.clone());
|
||||
let incoming_data: Result<IncomingFriendCursorPayload, _> =
|
||||
serde_json::from_value(first_value.clone());
|
||||
|
||||
match incoming_data {
|
||||
match incoming_data {
|
||||
Ok(friend_data) => {
|
||||
// We received grid coordinates (mapped)
|
||||
// We received normalized coordinates (mapped)
|
||||
let mapped_pos = &friend_data.position;
|
||||
|
||||
// Convert grid coordinates back to absolute screen coordinates (raw)
|
||||
let raw_pos = grid_to_absolute_position(mapped_pos);
|
||||
// Convert normalized coordinates back to absolute screen coordinates (raw)
|
||||
let raw_pos = normalized_to_absolute(mapped_pos);
|
||||
|
||||
let outgoing_payload = OutgoingFriendCursorPayload {
|
||||
user_id: friend_data.user_id.clone(),
|
||||
@@ -106,14 +110,16 @@ fn on_friend_cursor_position(payload: Payload, _socket: RawClient) {
|
||||
},
|
||||
};
|
||||
|
||||
get_app_handle()
|
||||
if let Err(e) = get_app_handle()
|
||||
.emit(WS_EVENT::FRIEND_CURSOR_POSITION, outgoing_payload)
|
||||
.unwrap();
|
||||
{
|
||||
error!("Failed to emit friend cursor position event: {:?}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to parse friend cursor position data: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Received empty text payload for friend cursor position");
|
||||
}
|
||||
@@ -126,38 +132,43 @@ fn on_friend_disconnected(payload: Payload, _socket: RawClient) {
|
||||
match payload {
|
||||
Payload::Text(str) => {
|
||||
println!("Received friend disconnected: {:?}", str);
|
||||
get_app_handle()
|
||||
.emit(WS_EVENT::FRIEND_DISCONNECTED, str)
|
||||
.unwrap();
|
||||
if let Err(e) = get_app_handle().emit(WS_EVENT::FRIEND_DISCONNECTED, str) {
|
||||
error!("Failed to emit friend disconnected event: {:?}", e);
|
||||
}
|
||||
}
|
||||
_ => error!("Received unexpected payload format for friend disconnected"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn report_cursor_data(cursor_position: CursorPosition) {
|
||||
let client = {
|
||||
// Only attempt to get clients if lock_r succeeds (it should, but safety first)
|
||||
// and if clients are actually initialized.
|
||||
let client_opt = {
|
||||
let guard = lock_r!(FDOLL);
|
||||
guard
|
||||
.clients
|
||||
.as_ref()
|
||||
.expect("Clients are initialized")
|
||||
.ws_client
|
||||
.as_ref()
|
||||
.expect("WebSocket client is initialized")
|
||||
.clone()
|
||||
.and_then(|c| c.ws_client.as_ref())
|
||||
.cloned()
|
||||
};
|
||||
|
||||
match async_runtime::spawn_blocking(move || {
|
||||
client.emit(
|
||||
WS_EVENT::CURSOR_REPORT_POSITION,
|
||||
Payload::Text(vec![json!(cursor_position)]),
|
||||
)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => (),
|
||||
Ok(Err(e)) => error!("Failed to emit ping: {}", e),
|
||||
Err(e) => error!("Failed to execute blocking task: {}", e),
|
||||
if let Some(client) = client_opt {
|
||||
match async_runtime::spawn_blocking(move || {
|
||||
client.emit(
|
||||
WS_EVENT::CURSOR_REPORT_POSITION,
|
||||
Payload::Text(vec![json!(cursor_position)]),
|
||||
)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => (),
|
||||
Ok(Err(e)) => error!("Failed to emit cursor report: {}", e),
|
||||
Err(e) => error!("Failed to execute blocking task for cursor report: {}", e),
|
||||
}
|
||||
} else {
|
||||
// Quietly fail if client is not ready (e.g. during startup/shutdown)
|
||||
// or debug log it.
|
||||
// debug!("WebSocket client not ready to report cursor data");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,28 +178,39 @@ pub async fn init_ws_client() {
|
||||
guard.app_config.clone()
|
||||
};
|
||||
|
||||
let ws_client = build_ws_client(&app_config).await;
|
||||
|
||||
let mut guard = lock_w!(FDOLL);
|
||||
if let Some(clients) = guard.clients.as_mut() {
|
||||
clients.ws_client = Some(ws_client);
|
||||
match build_ws_client(&app_config).await {
|
||||
Ok(ws_client) => {
|
||||
let mut guard = lock_w!(FDOLL);
|
||||
if let Some(clients) = guard.clients.as_mut() {
|
||||
clients.ws_client = Some(ws_client);
|
||||
}
|
||||
info!("WebSocket client initialized after authentication");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to initialize WebSocket client: {}", e);
|
||||
// We should probably inform the user or retry here, but for now we just log it.
|
||||
}
|
||||
}
|
||||
info!("WebSocket client initialized after authentication");
|
||||
}
|
||||
|
||||
pub async fn build_ws_client(app_config: &AppConfig) -> rust_socketio::client::Client {
|
||||
let token = crate::services::auth::get_access_token()
|
||||
.await
|
||||
.expect("No access token available for WebSocket connection");
|
||||
pub async fn build_ws_client(
|
||||
app_config: &AppConfig,
|
||||
) -> Result<rust_socketio::client::Client, String> {
|
||||
let token_result = crate::services::auth::get_access_token().await;
|
||||
|
||||
let token = match token_result {
|
||||
Some(t) => t,
|
||||
None => return Err("No access token available for WebSocket connection".to_string()),
|
||||
};
|
||||
|
||||
info!("Building WebSocket client with authentication");
|
||||
|
||||
let api_base_url = app_config
|
||||
.api_base_url
|
||||
.clone()
|
||||
.expect("Missing API base URL");
|
||||
.ok_or("Missing API base URL")?;
|
||||
|
||||
let client = async_runtime::spawn_blocking(move || {
|
||||
let client_result = async_runtime::spawn_blocking(move || {
|
||||
ClientBuilder::new(api_base_url)
|
||||
.namespace("/")
|
||||
.on(
|
||||
@@ -206,20 +228,24 @@ pub async fn build_ws_client(app_config: &AppConfig) -> rust_socketio::client::C
|
||||
.auth(json!({ "token": token }))
|
||||
.connect()
|
||||
})
|
||||
.await
|
||||
.expect("Failed to spawn blocking task");
|
||||
.await;
|
||||
|
||||
match client {
|
||||
Ok(c) => {
|
||||
info!("WebSocket client connected successfully");
|
||||
c
|
||||
}
|
||||
match client_result {
|
||||
Ok(connect_result) => match connect_result {
|
||||
Ok(c) => {
|
||||
info!("WebSocket client connected successfully");
|
||||
Ok(c)
|
||||
}
|
||||
Err(e) => {
|
||||
let err_msg = format!("Failed to connect WebSocket: {:?}", e);
|
||||
error!("{}", err_msg);
|
||||
Err(err_msg)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to connect WebSocket: {:?}", e);
|
||||
panic!(
|
||||
"TODO: better error handling for WebSocket connection - {}",
|
||||
e
|
||||
);
|
||||
let err_msg = format!("Failed to spawn blocking task: {:?}", e);
|
||||
error!("{}", err_msg);
|
||||
Err(err_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user