1use super::*;
4
5const GROUP_DEVICES_MEMO_CAPACITY: u64 = 64;
8
9const DM_DEVICES_MEMO_CAPACITY: u64 = 512;
13
14pub(crate) const NO_AUTHENTICATED_GENERATION: u64 = u64::MAX;
20
21impl Drop for Client {
22 fn drop(&mut self) {
23 self.signal_shutdown_sync();
24 }
25}
26
27impl Client {
28 pub(crate) const STABLE_CONNECTION_RESET_MS: i64 = 30_000;
31
32 pub fn builder() -> ClientBuilder {
34 ClientBuilder::new()
35 }
36
37 pub fn shutdown_signal(&self) -> wacore::runtime::ShutdownSignal {
38 self.shutdown_notifier.subscribe()
39 }
40
41 pub fn signal_shutdown_sync(&self) {
48 self.expected_disconnect.store(true, Ordering::Relaxed);
49 self.is_running.store(false, Ordering::Relaxed);
50 self.shutdown_notifier.notify();
51 self.notify_session_state();
52 #[cfg(feature = "client-lifecycle")]
53 if let Some(lifecycle) = &self.lifecycle {
54 lifecycle.signal_shutdown_sync();
55 }
56 self.notify_connection_shutdown();
57 }
58
59 pub(crate) fn connection_shutdown_signal(&self) -> wacore::runtime::ShutdownSignal {
60 self.connection_shutdown
61 .lock()
62 .unwrap_or_else(|p| p.into_inner())
63 .subscribe()
64 }
65
66 pub(crate) fn notify_connection_shutdown(&self) {
69 self.connection_shutdown
70 .lock()
71 .unwrap_or_else(|p| p.into_inner())
72 .notify();
73 self.notify_session_state();
85 }
86
87 pub(crate) fn reset_connection_shutdown(&self) {
92 *self
93 .connection_shutdown
94 .lock()
95 .unwrap_or_else(|p| p.into_inner()) = wacore::runtime::ShutdownNotifier::new();
96 }
97
98 pub(crate) fn is_shutting_down(&self) -> bool {
99 self.expected_disconnect.load(Ordering::Relaxed) || !self.is_running.load(Ordering::Relaxed)
100 }
101
102 pub(crate) fn is_terminal(&self) -> bool {
127 if self.shutdown_signal().is_fired() {
128 return true;
129 }
130 !self.enable_auto_reconnect.load(Ordering::Relaxed)
146 && (self.expected_disconnect.load(Ordering::Relaxed)
147 || (!self.is_running.load(Ordering::Relaxed) && !self.is_connected()))
148 }
149
150 pub(crate) fn notify_session_state(&self) {
157 self.session_state_notifier.notify(usize::MAX);
158 }
159
160 pub(crate) fn stop_supervision_loop(&self) {
169 self.is_running.store(false, Ordering::Relaxed);
170 self.notify_session_state();
171 }
172
173 fn is_fully_ready(&self) -> bool {
177 self.is_connected() && self.is_logged_in() && self.is_ready.load(Ordering::Relaxed)
178 }
179
180 pub(crate) async fn dispatch_connected(&self, expected_generation: u64) {
182 #[cfg(feature = "client-lifecycle")]
183 {
184 if let Some(lifecycle) = &self.lifecycle {
185 if !lifecycle.ready(expected_generation).await {
186 debug!(
187 "Skipping Connected dispatch for retired generation {expected_generation}"
188 );
189 return;
190 }
191
192 let _login_transition = self
195 .login_transition
196 .lock()
197 .unwrap_or_else(|poisoned| poisoned.into_inner());
198 if self.connection_generation.load(Ordering::SeqCst) != expected_generation
199 || self.expected_disconnect.load(Ordering::Acquire)
200 {
201 debug!(
202 "Skipping Connected dispatch after generation {expected_generation} retired"
203 );
204 return;
205 }
206 if !lifecycle.publish_ready(expected_generation, || self.publish_connected()) {
207 debug!("Skipping Connected dispatch after lifecycle cancellation");
208 }
209 return;
210 }
211 }
212
213 #[cfg(feature = "client-lifecycle")]
214 let _login_transition = self
215 .login_transition
216 .lock()
217 .unwrap_or_else(|poisoned| poisoned.into_inner());
218 if self.connection_generation.load(Ordering::SeqCst) != expected_generation
219 || self.expected_disconnect.load(Ordering::Acquire)
220 {
221 debug!("Skipping Connected dispatch after its connection retired");
222 return;
223 }
224 self.publish_connected();
225 }
226
227 fn publish_connected(&self) {
228 self.is_ready.store(true, Ordering::Relaxed);
229 wacore::telemetry::set_connected(true);
230 self.core.event_bus.dispatch(Event::Connected(
231 crate::types::events::Connected::builder().build(),
232 ));
233 self.connected_notifier.notify(usize::MAX);
234 }
235
236 #[cfg(feature = "client-lifecycle")]
237 pub(super) async fn shutdown_lifecycle(&self) {
238 if let Some(lifecycle) = &self.lifecycle {
239 lifecycle.shutdown().await;
240 }
241 }
242
243 #[cfg(feature = "client-lifecycle")]
244 fn request_lifecycle_shutdown(&self) {
245 if let Some(lifecycle) = &self.lifecycle {
246 lifecycle.request_shutdown();
247 }
248 }
249
250 pub async fn new(
255 runtime: Arc<dyn Runtime>,
256 persistence_manager: Arc<PersistenceManager>,
257 transport_factory: Arc<dyn crate::transport::TransportFactory>,
258 http_client: Arc<dyn crate::http::HttpClient>,
259 override_version: Option<(u32, u32, u32)>,
260 ) -> (Arc<Self>, async_channel::Receiver<MajorSyncTask>) {
261 ClientBuilder::build_required(
262 runtime,
263 persistence_manager,
264 transport_factory,
265 http_client,
266 override_version,
267 CacheConfig::default(),
268 )
269 .await
270 .into_parts()
271 }
272
273 pub async fn new_with_cache_config(
275 runtime: Arc<dyn Runtime>,
276 persistence_manager: Arc<PersistenceManager>,
277 transport_factory: Arc<dyn crate::transport::TransportFactory>,
278 http_client: Arc<dyn crate::http::HttpClient>,
279 override_version: Option<(u32, u32, u32)>,
280 cache_config: CacheConfig,
281 ) -> (Arc<Self>, async_channel::Receiver<MajorSyncTask>) {
282 ClientBuilder::build_required(
283 runtime,
284 persistence_manager,
285 transport_factory,
286 http_client,
287 override_version,
288 cache_config,
289 )
290 .await
291 .into_parts()
292 }
293
294 pub(super) fn assemble(
295 runtime: Arc<dyn Runtime>,
296 persistence_manager: Arc<PersistenceManager>,
297 transport_factory: Arc<dyn crate::transport::TransportFactory>,
298 http_client: Arc<dyn crate::http::HttpClient>,
299 override_version: Option<(u32, u32, u32)>,
300 cache_config: CacheConfig,
301 extensions: ClientExtensions,
302 ) -> ClientAssembly {
303 let ClientExtensions {
304 #[cfg(feature = "client-lifecycle")]
305 lifecycle,
306 #[cfg(feature = "plugins")]
307 plugin_host,
308 } = extensions;
309 let mut unique_id_bytes = [0u8; 2];
310 rand::make_rng::<rand::rngs::StdRng>().fill_bytes(&mut unique_id_bytes);
311
312 let device_snapshot = persistence_manager.get_device_snapshot();
313 let core = wacore::client::CoreClient::new(device_snapshot.core.clone());
314
315 let (tx, rx) = async_channel::bounded(32);
316
317 let device_topology = device_topology::DeviceTopology::new();
318 let this = Self {
319 runtime: runtime.clone(),
320 core,
321 msg_secret_buffer: crate::msg_secret_buffer::MsgSecretWriteBuffer::new(
322 persistence_manager.backend(),
323 runtime.clone(),
324 ),
325 persistence_manager: persistence_manager.clone(),
326 media_conn: Arc::new(RwLock::new(None)),
327 is_logged_in: Arc::new(AtomicBool::new(false)),
328 #[cfg(feature = "client-lifecycle")]
329 login_transition: std::sync::Mutex::new(()),
330 is_connecting: Arc::new(AtomicBool::new(false)),
331 is_running: Arc::new(AtomicBool::new(false)),
332 is_connected: Arc::new(AtomicBool::new(false)),
333 send_active_receipts: AtomicU32::new(0),
334 ik_handshake_failures: Arc::new(AtomicU32::new(0)),
335 shutdown_notifier: wacore::runtime::ShutdownNotifier::new(),
336 connection_shutdown: std::sync::Mutex::new(wacore::runtime::ShutdownNotifier::new()),
337 #[cfg(feature = "client-lifecycle")]
338 lifecycle,
339 #[cfg(feature = "plugins")]
340 plugin_host,
341 stats: Arc::new(wacore::stats::SessionStats::new()),
342
343 transport: Arc::new(Mutex::new(None)),
344 transport_events: Arc::new(Mutex::new(None)),
345 transport_factory,
346 noise_socket: Arc::new(Mutex::new(None)),
347
348 response_waiters: Arc::new(std::sync::Mutex::new(ResponseWaiterMap::default())),
349 node_waiters: std::sync::Mutex::new(Vec::new()),
350 node_waiter_count: AtomicUsize::new(0),
351 sent_node_waiters: std::sync::Mutex::new(Vec::new()),
352 sent_node_waiter_count: AtomicUsize::new(0),
353 unique_id: format!("{}.{}", unique_id_bytes[0], unique_id_bytes[1]),
354 id_counter: Arc::new(AtomicU64::new(0)),
355 unified_session: crate::unified_session::UnifiedSessionManager::new(),
356
357 signal_cache: Arc::new(crate::store::signal_cache::SignalStoreCache::new()),
358 message_processing_semaphore: std::sync::Mutex::new(Arc::new(
359 async_lock::Semaphore::new(1),
360 )),
361 message_semaphore_generation: Arc::new(AtomicU64::new(0)),
362 session_locks: Cache::builder()
369 .max_capacity(cache_config.session_locks_capacity.max(1))
370 .evict_guard(|m| Arc::strong_count(m) <= 1)
371 .build(),
372 chat_lanes: Cache::builder()
373 .max_capacity(cache_config.chat_lanes_capacity.max(1))
374 .evict_guard(|lane: &ChatLane| Arc::strong_count(&lane.enqueue_lock) <= 1)
375 .build(),
376 lid_pn_cache: Arc::new(LidPnCache::with_config(
377 &cache_config.lid_pn_cache,
378 cache_config.cache_stores.lid_pn_cache.clone(),
379 )),
380 ab_props: Arc::new(wacore::store::ab_props::AbPropsCache::new()),
381 group_cache: Mutex::new(None),
382
383 expected_disconnect: Arc::new(AtomicBool::new(false)),
384 intentional_reconnect: AtomicBool::new(false),
385 connection_generation: Arc::new(AtomicU64::new(0)),
386
387 recent_messages: cache_config.recent_messages.build_with_ttl(),
388
389 sender_key_device_cache: crate::sender_key_device_cache::SenderKeyDeviceCache::new(
390 &cache_config.sender_key_devices_cache,
391 ),
392
393 pending_device_sync: crate::pending_device_sync::PendingDeviceSync::new(),
394
395 pending_retries: Arc::new(std::sync::Mutex::new(HashSet::new())),
396
397 message_retry_counts: cache_config.message_retry_counts.build_with_ttl(),
398
399 session_recreate_history: cache_config.session_recreate_history.build_with_ttl(),
400
401 resend_rate_limiter: crate::resend_rate_limiter::ResendRateLimiter::new(
402 cache_config.resend_rate_limiter_capacity,
403 crate::resend_rate_limiter::DEFAULT_RESEND_BURST,
404 crate::resend_rate_limiter::DEFAULT_RESEND_REFILL_PER_MIN,
405 ),
406
407 undecryptable_dispatched: cache_config.undecryptable_dispatched.build_with_ttl(),
408
409 offline_sync_metrics: Arc::new(OfflineSyncMetrics {
410 active: AtomicBool::new(false),
411 total_messages: AtomicUsize::new(0),
412 processed_messages: AtomicUsize::new(0),
413 start_time: std::sync::Mutex::new(None),
414 }),
415 offline_batch: Arc::new(offline_resume::OfflineBatchCoordinator::new()),
416
417 enable_auto_reconnect: Arc::new(AtomicBool::new(true)),
418 auto_reconnect_errors: Arc::new(AtomicU32::new(0)),
419 connected_at_ms: Arc::new(AtomicI64::new(0)),
420 backoff_reset_suppressed: Arc::new(AtomicBool::new(false)),
421
422 needs_initial_full_sync: Arc::new(app_state::BootstrapGate::new(false)),
423
424 app_state_processor: Mutex::new(None),
425 app_state_key_requests: Arc::new(Mutex::new(HashMap::new())),
426 app_state_syncing: app_state::SyncInFlight::new(),
427 app_state_send_lock: Arc::new(Mutex::new(())),
428 initial_keys_synced_notifier: Arc::new(event_listener::Event::new()),
429 initial_app_state_keys_received: Arc::new(AtomicBool::new(false)),
430 prekey_upload_lock: Arc::new(Mutex::new(())),
431 signed_pre_key_rotation_lock: Arc::new(Mutex::new(())),
432 offline_sync_notifier: Arc::new(event_listener::Event::new()),
433 offline_sync_completed: Arc::new(AtomicBool::new(false)),
434 offline_sync_finish_started: Arc::new(AtomicBool::new(false)),
435 offline_receipt_buffer: std::sync::Mutex::new(Vec::new()),
436 inbound_commit_batch: Default::default(),
437 history_sync_activity: Arc::new(crate::sync_task::HistorySyncActivity::new()),
438 outbound_flush: Arc::new(crate::flush_scope::FlushScope::new()),
439 delivery_receipt_queue: std::sync::OnceLock::new(),
440 transport_ack_queue: std::sync::OnceLock::new(),
441 presence_subscriptions: Arc::new(Mutex::new(HashSet::new())),
442 socket_ready_notifier: Arc::new(event_listener::Event::new()),
443 is_ready: Arc::new(AtomicBool::new(false)),
444 connected_notifier: Arc::new(event_listener::Event::new()),
445 authenticated_generation: Arc::new(AtomicU64::new(NO_AUTHENTICATED_GENERATION)),
446 session_state_notifier: Arc::new(event_listener::Event::new()),
447 major_sync_task_sender: tx,
448 pairing_cancellation_tx: Arc::new(Mutex::new(None)),
449 pairing_qr_refresh_tx: Arc::new(Mutex::new(None)),
450 pair_code_state: Arc::new(Mutex::new(wacore::pair_code::PairCodeState::default())),
451 passkey_state: Arc::new(Mutex::new(crate::passkey::flow::PasskeyFlowState::default())),
452 passkey_opening: AtomicBool::new(false),
453 signal_flush_state: AtomicU64::new(0),
454 signal_flush_lifecycle: Mutex::new(()),
455 #[cfg(test)]
456 signal_flush_test_failures: AtomicU32::new(0),
457 #[cfg(test)]
458 signal_flush_test_block: AtomicBool::new(false),
459 #[cfg(test)]
460 signal_flush_test_in_attempt: AtomicU32::new(0),
461 #[cfg(test)]
462 app_state_key_share_prepare_test_failures: AtomicU32::new(0),
463 custom_enc_handlers: std::sync::OnceLock::new(),
464 inbound_durability_hook: std::sync::OnceLock::new(),
465 retry_admission: std::sync::OnceLock::new(),
466 chatstate_handlers: Arc::new(RwLock::new(Vec::new())),
467 pdo_pending_requests: cache_config.pdo_pending_requests.build_with_ttl(),
468 pdo_requested: cache_config.pdo_requested.build_with_ttl(),
469 device_registry_cache: device_topology::DeviceRegistryCache::new(
470 cache_config.device_registry_cache.build_typed_ttl(
471 cache_config.cache_stores.device_registry_cache.clone(),
472 "device_registry",
473 ),
474 Arc::clone(&device_topology),
475 ),
476 device_topology,
477 device_memos_enabled: cache_config.cache_stores.device_registry_cache.is_none()
478 && cache_config.cache_stores.lid_pn_cache.is_none(),
479 group_devices_memo: Cache::builder()
480 .max_capacity(GROUP_DEVICES_MEMO_CAPACITY)
481 .build(),
482 dm_devices_memo: Cache::builder()
483 .max_capacity(DM_DEVICES_MEMO_CAPACITY)
484 .build(),
485 #[cfg(test)]
486 dm_devices_memo_recomputes: AtomicU64::new(0),
487 group_distribution_locks: Cache::builder()
489 .max_capacity(cache_config.group_distribution_locks_capacity.max(1))
490 .evict_guard(|m| Arc::strong_count(m) <= 1)
491 .build(),
492 skdm_warm_memo: Cache::builder()
493 .max_capacity(GROUP_DEVICES_MEMO_CAPACITY)
494 .build(),
495 stanza_router: Self::create_stanza_router(),
496 synchronous_ack: false,
497 http_client,
498 override_version,
499 skip_history_sync: AtomicBool::new(false),
500 wanted_pre_key_count: AtomicUsize::new(crate::prekeys::DEFAULT_WANTED_PRE_KEY_COUNT),
501 cache_config,
502 self_weak: std::sync::OnceLock::new(),
503 saver_handle: std::sync::OnceLock::new(),
504 alloc_meter: std::sync::OnceLock::new(),
505 raw_node_forwarding: AtomicUsize::new(0),
506 #[cfg(feature = "voip-runtime")]
507 call_registry: Arc::new(wacore::voip::CallRegistry::new()),
508 #[cfg(feature = "voip-runtime")]
509 pending_call_link_joins: Arc::new(std::sync::Mutex::new(
510 voip::PendingCallLinkJoins::default(),
511 )),
512 #[cfg(feature = "voip-runtime")]
513 pending_call_link_join_lane: Arc::new(Mutex::new(())),
514 #[cfg(feature = "voip-runtime")]
515 answer_transition_locks: std::array::from_fn(|_| Arc::new(Mutex::new(()))),
516 #[cfg(feature = "voip-runtime")]
517 pending_outgoing_calls: Arc::new(std::sync::Mutex::new(HashMap::new())),
518 };
519
520 let arc = Arc::new(this);
521 arc.lid_pn_cache
524 .attach_topology(Arc::clone(&arc.device_topology));
525 let _ = arc.self_weak.set(Arc::downgrade(&arc));
526
527 ClientAssembly::new(arc, rx)
528 }
529
530 pub(super) fn start_services(self: &Arc<Self>) {
531 let warm_up_arc = self.clone();
532 self.runtime
533 .spawn(Box::pin(async move {
534 if let Err(e) = warm_up_arc.warm_up_lid_pn_cache().await {
535 warn!("Failed to warm up LID-PN cache: {e}");
536 }
537 }))
538 .detach();
539 }
540
541 pub async fn run(self: &Arc<Self>) {
546 #[cfg(feature = "client-lifecycle")]
547 if let Some(lifecycle) = &self.lifecycle
548 && !lifecycle.wait_until_active().await
549 {
550 warn!("Client `run` rejected before construction completed.");
551 return;
552 }
553 let shutdown = self.shutdown_signal();
554 if shutdown.is_fired() {
555 warn!("Client `run` called after shutdown.");
556 return;
557 }
558 if self.is_running.swap(true, Ordering::SeqCst) {
559 warn!("Client `run` method called while already running.");
560 return;
561 }
562 if shutdown.is_fired() {
563 self.is_running.store(false, Ordering::SeqCst);
564 return;
565 }
566 let mut first_connect = true;
571 while self.is_running.load(Ordering::Relaxed) {
572 if !first_connect {
573 self.stats.record_reconnect();
574 }
575 first_connect = false;
576 self.expected_disconnect.store(false, Ordering::Relaxed);
577
578 if let Err(connect_err) = self.connect().await {
579 wacore::telemetry::connect("fail");
580 let is_transient = matches!(
581 &connect_err,
582 ConnectError::Handshake(e) if e.is_transient()
583 );
584 if is_transient {
585 debug!("Transient connect failure, will retry: {connect_err:#}");
586 } else {
587 error!("Failed to connect: {connect_err:#}. Will retry...");
588 }
589 } else {
590 wacore::telemetry::connect("ok");
591 let loop_result = self.read_messages_loop().await;
592 let intentional = self.intentional_reconnect.swap(false, Ordering::Relaxed);
598 let unexpected_disconnect = match loop_result {
602 Ok(node_io::ReadLoopExit::Expected) => {
603 debug!("Message loop exited gracefully (expected disconnect).");
604 None
605 }
606 Ok(node_io::ReadLoopExit::ServerRecycle(reason)) => {
607 if self.expected_disconnect.load(Ordering::Relaxed) || intentional {
608 debug!("Message loop exited during expected disconnect.");
609 None
610 } else {
611 Some(reason)
614 }
615 }
616 Err(e) => {
617 if self.expected_disconnect.load(Ordering::Relaxed) || intentional {
618 debug!("Message loop exited during expected disconnect.");
619 None
620 } else {
621 debug!("Message loop exited, will reconnect if enabled: {e:#}");
624 Some(e.into_reason())
625 }
626 }
627 };
628
629 self.cleanup_connection_state().await;
630
631 if let Some(reason) = unexpected_disconnect {
633 self.core.event_bus.dispatch(Event::Disconnected(
634 crate::types::events::Disconnected::builder()
635 .reason(reason)
636 .build(),
637 ));
638 }
639 }
640
641 if !self.enable_auto_reconnect.load(Ordering::Relaxed) {
642 info!("Auto-reconnect disabled, shutting down.");
643 self.stop_supervision_loop();
644 break;
645 }
646
647 if self.expected_disconnect.load(Ordering::Relaxed) {
649 self.auto_reconnect_errors.store(0, Ordering::Relaxed);
650 self.connected_at_ms.store(0, Ordering::Relaxed);
653 info!("Expected disconnect (e.g., 515), reconnecting immediately...");
654 continue;
655 }
656
657 let connected_at = self.connected_at_ms.swap(0, Ordering::Relaxed);
661 let penalty = self.backoff_reset_suppressed.load(Ordering::Relaxed);
662 if should_reset_backoff(connected_at, wacore::time::now_millis(), penalty) {
663 self.auto_reconnect_errors.store(0, Ordering::Relaxed);
664 }
665
666 let error_count = self.auto_reconnect_errors.fetch_add(1, Ordering::SeqCst);
667 let delay = fibonacci_backoff(error_count);
671 info!(
672 "Will attempt to reconnect in {:?} (attempt {})",
673 delay,
674 error_count + 1
675 );
676 let shutdown_fired = wacore::runtime::wait_for_shutdown(&shutdown);
688 futures::select! {
689 _ = self.runtime.sleep(delay).fuse() => {}
690 _ = shutdown_fired.fuse() => {
691 debug!("Shutdown signalled during reconnect backoff, exiting run loop.");
692 }
693 }
694 }
695 #[cfg(feature = "client-lifecycle")]
696 self.shutdown_lifecycle().await;
697 info!("Client run loop has shut down.");
698 }
699
700 pub async fn connect(self: &Arc<Self>) -> Result<(), ConnectError> {
704 #[cfg(feature = "client-lifecycle")]
705 if let Some(lifecycle) = &self.lifecycle
706 && !lifecycle.wait_until_active().await
707 {
708 return Err(ConnectError::NotActivated);
709 }
710 self.connect_boxed().await
711 }
712
713 #[inline(never)]
714 fn connect_boxed(self: &Arc<Self>) -> wacore::runtime::BoxFuture<'_, Result<(), ConnectError>> {
715 Box::pin(self.connect_graph())
716 }
717
718 #[cfg_attr(
724 feature = "tracing",
725 tracing::instrument(
726 name = "wa.conn.connect",
727 level = "info",
728 skip_all,
729 fields(lid = tracing::field::Empty, pn = tracing::field::Empty),
730 err(level = "warn", Debug)
731 )
732 )]
733 async fn connect_graph(self: &Arc<Self>) -> Result<(), ConnectError> {
734 #[cfg(feature = "tracing")]
735 self.record_identity_on_span(&tracing::Span::current());
736
737 if self.is_connecting.swap(true, Ordering::SeqCst) {
738 return Err(ConnectError::AlreadyConnected);
739 }
740
741 let _guard = scopeguard::guard((), |_| {
742 self.is_connecting.store(false, Ordering::Relaxed);
743 });
744
745 if self.is_connected() {
746 return Err(ConnectError::AlreadyConnected);
747 }
748 let _t = wacore::telemetry::timer(wacore::telemetry::CONNECT_DURATION);
749
750 self.is_logged_in.store(false, Ordering::Relaxed);
754 self.is_ready.store(false, Ordering::Relaxed);
755 self.is_connected.store(false, Ordering::Relaxed);
756 self.offline_sync_completed.store(false, Ordering::Relaxed);
757 self.offline_sync_finish_started
758 .store(false, Ordering::Relaxed);
759 self.clear_offline_receipt_buffer();
760 if self.inbound_commit_batch.reset() {
770 log::warn!(
771 "connect: dropping unflushed Signal state along with late uncommitted drain entries"
772 );
773 self.signal_cache.clear().await;
774 }
775 self.offline_batch.reset();
776 self.outbound_flush.reopen();
777
778 let version_future = rt_timeout(
782 &*self.runtime,
783 TRANSPORT_CONNECT_TIMEOUT,
784 crate::version::resolve_and_update_version(
785 &self.persistence_manager,
786 &self.http_client,
787 self.override_version,
788 ),
789 );
790 let transport_future = rt_timeout(
791 &*self.runtime,
792 TRANSPORT_CONNECT_TIMEOUT,
793 self.transport_factory.create_transport(),
794 );
795
796 debug!("Connecting WebSocket and fetching latest client version in parallel...");
797 let (version_result, transport_result) = futures::join!(version_future, transport_future);
798
799 version_result
800 .map_err(|_| ConnectError::Timeout {
801 stage: ConnectStage::VersionFetch,
802 timeout: TRANSPORT_CONNECT_TIMEOUT,
803 })?
804 .map_err(ConnectError::Version)?;
805 let (transport, mut transport_events) = transport_result
806 .map_err(|_| ConnectError::Timeout {
807 stage: ConnectStage::Transport,
808 timeout: TRANSPORT_CONNECT_TIMEOUT,
809 })?
810 .map_err(ConnectError::Transport)?;
811 debug!("Version fetch and transport connection established.");
812
813 let noise_socket = match handshake::do_handshake(
814 self.runtime.clone(),
815 &self.persistence_manager,
816 &self.ik_handshake_failures,
817 transport.clone(),
818 &mut transport_events,
819 Some(self.stats.clone()),
820 )
821 .await
822 {
823 Ok(socket) => socket,
824 Err(e) => {
825 transport.disconnect().await;
826 return Err(e.into());
827 }
828 };
829
830 self.reset_connection_shutdown();
834
835 self.authenticated_generation
843 .store(NO_AUTHENTICATED_GENERATION, Ordering::SeqCst);
844
845 *self.transport.lock().await = Some(transport);
846 *self.transport_events.lock().await = Some(transport_events);
847 *self.noise_socket.lock().await = Some(noise_socket);
848 self.is_connected.store(true, Ordering::Release);
849
850 self.socket_ready_notifier.notify(usize::MAX);
852
853 let client_clone = self.clone();
854 self.runtime
855 .spawn(Box::pin(async move { client_clone.keepalive_loop().await }))
856 .detach();
857
858 Ok(())
859 }
860
861 #[cfg_attr(
868 feature = "tracing",
869 tracing::instrument(name = "wa.conn.logout", level = "info", skip_all)
870 )]
871 pub async fn logout(self: &Arc<Self>) {
872 use wacore::iq::devices::RemoveCompanionDeviceSpec;
873
874 self.enable_auto_reconnect.store(false, Ordering::Relaxed);
875
876 if self.is_connected()
877 && let Ok(jid) = self.require_pn()
878 && let Err(e) = self.execute(RemoveCompanionDeviceSpec::new(&jid)).await
879 {
880 warn!("Failed to send logout IQ: {e}");
881 }
882
883 self.core.event_bus.dispatch(Event::LoggedOut(
884 crate::types::events::LoggedOut::builder()
885 .on_connect(false)
886 .reason(ConnectFailureReason::LoggedOut)
887 .build(),
888 ));
889
890 self.disconnect().await;
891 }
892
893 #[cfg_attr(
894 feature = "tracing",
895 tracing::instrument(name = "wa.conn.disconnect", level = "info", skip_all)
896 )]
897 pub async fn disconnect(self: &Arc<Self>) {
898 info!("Disconnecting client intentionally.");
899 wacore::telemetry::set_connected(false);
900 self.expected_disconnect.store(true, Ordering::Relaxed);
901 self.is_running.store(false, Ordering::Relaxed);
902 self.shutdown_notifier.notify();
903 self.notify_session_state();
904 #[cfg(feature = "client-lifecycle")]
905 self.request_lifecycle_shutdown();
906
907 if self
923 .flush_inbound_commits_bounded(Duration::from_secs(5))
924 .await
925 {
926 self.flush_offline_receipts();
927 }
928 self.outbound_flush.close();
930 self.outbound_flush
931 .flush(&*self.runtime, Duration::from_secs(5))
932 .await;
933 self.notify_connection_shutdown();
934
935 if let Err(e) = self.persistence_manager.flush().await {
936 log::error!("Failed to flush device state during disconnect: {e}");
937 }
938
939 if let Some(transport) = self.transport.lock().await.as_ref() {
941 transport.disconnect().await;
942 }
943 self.cleanup_connection_state().await;
944
945 self.msg_secret_buffer.seal();
951 self.msg_secret_buffer.flush().await;
952 #[cfg(feature = "client-lifecycle")]
953 self.shutdown_lifecycle().await;
954 }
955
956 pub const RECONNECT_BACKOFF_STEP: u32 = 4;
964
965 #[cfg_attr(
977 feature = "tracing",
978 tracing::instrument(name = "wa.conn.reconnect", level = "info", skip_all)
979 )]
980 pub async fn reconnect(self: &Arc<Self>) {
981 info!("Reconnecting: dropping transport for auto-reconnect.");
982 #[cfg(feature = "client-lifecycle")]
983 if let Some(lifecycle) = &self.lifecycle {
984 lifecycle.cancel_active_scope();
985 }
986 wacore::telemetry::reconnect();
987 self.intentional_reconnect.store(true, Ordering::Relaxed);
988 self.auto_reconnect_errors
989 .store(Self::RECONNECT_BACKOFF_STEP, Ordering::Relaxed);
990 self.backoff_reset_suppressed.store(true, Ordering::Relaxed);
992
993 if self
995 .flush_inbound_commits_bounded(Duration::from_secs(2))
996 .await
997 {
998 self.flush_offline_receipts();
999 }
1000 self.outbound_flush.close();
1001 self.outbound_flush
1002 .flush(&*self.runtime, Duration::from_secs(2))
1003 .await;
1004 self.notify_connection_shutdown();
1005
1006 if let Some(transport) = self.transport.lock().await.as_ref() {
1007 transport.disconnect().await;
1008 }
1009 }
1010
1011 #[cfg_attr(
1017 feature = "tracing",
1018 tracing::instrument(name = "wa.conn.reconnect_immediately", level = "info", skip_all)
1019 )]
1020 pub async fn reconnect_immediately(self: &Arc<Self>) {
1021 info!("Reconnecting immediately (expected disconnect).");
1022 #[cfg(feature = "client-lifecycle")]
1023 if let Some(lifecycle) = &self.lifecycle {
1024 lifecycle.cancel_active_scope();
1025 }
1026 self.expected_disconnect.store(true, Ordering::Relaxed);
1027
1028 if self
1030 .flush_inbound_commits_bounded(Duration::from_secs(2))
1031 .await
1032 {
1033 self.flush_offline_receipts();
1034 }
1035 self.outbound_flush.close();
1036 self.outbound_flush
1037 .flush(&*self.runtime, Duration::from_secs(2))
1038 .await;
1039 self.notify_connection_shutdown();
1040
1041 if let Some(transport) = self.transport.lock().await.as_ref() {
1042 transport.disconnect().await;
1043 }
1044 }
1045
1046 #[cfg_attr(
1047 feature = "tracing",
1048 tracing::instrument(name = "wa.conn.cleanup", level = "debug", skip_all)
1049 )]
1050 #[cfg(not(feature = "client-lifecycle"))]
1051 pub(crate) async fn cleanup_connection_state(self: &Arc<Self>) {
1052 self.cleanup_connection_state_inner().await;
1053 self.clear_connection_scoped_pair_code().await;
1054 }
1055
1056 async fn clear_connection_scoped_pair_code(self: &Arc<Self>) {
1066 *self.pair_code_state.lock().await = wacore::pair_code::PairCodeState::Idle;
1067 }
1068
1069 #[cfg_attr(
1070 feature = "tracing",
1071 tracing::instrument(name = "wa.conn.cleanup", level = "debug", skip_all)
1072 )]
1073 #[cfg(feature = "client-lifecycle")]
1074 pub(crate) async fn cleanup_connection_state(self: &Arc<Self>) {
1075 if self.lifecycle.is_none() {
1076 self.cleanup_connection_state_inner().await;
1077 self.clear_connection_scoped_pair_code().await;
1078 return;
1079 }
1080
1081 let (completed, completion) = futures::channel::oneshot::channel();
1083 let client = Arc::clone(self);
1084 self.runtime
1085 .spawn(Box::pin(async move {
1086 let result = std::panic::AssertUnwindSafe(client.cleanup_connection_state_inner())
1087 .catch_unwind()
1088 .await;
1089 let _ = completed.send(result);
1090 }))
1091 .detach();
1092 match completion.await {
1093 Ok(Ok(())) => {}
1094 Ok(Err(panic)) => std::panic::resume_unwind(panic),
1095 Err(_) => error!("Detached connection cleanup stopped before completion"),
1096 }
1097 self.clear_connection_scoped_pair_code().await;
1098 }
1099
1100 async fn cleanup_connection_state_inner(&self) {
1101 #[cfg(feature = "client-lifecycle")]
1102 let login_transition = self
1103 .login_transition
1104 .lock()
1105 .unwrap_or_else(|poisoned| poisoned.into_inner());
1106 #[cfg(feature = "client-lifecycle")]
1115 let closed_generation = self.connection_generation.fetch_add(1, Ordering::SeqCst);
1116 #[cfg(not(feature = "client-lifecycle"))]
1117 self.connection_generation.fetch_add(1, Ordering::SeqCst);
1118 #[cfg(feature = "client-lifecycle")]
1119 let scope_close = self.lifecycle.as_ref().map(|lifecycle| {
1120 let lifecycle = Arc::clone(lifecycle);
1121 scopeguard::guard((lifecycle, closed_generation), |(lifecycle, generation)| {
1122 if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1123 lifecycle.close_scope(generation);
1124 }))
1125 .is_err()
1126 {
1127 error!("Client lifecycle scope closure panicked");
1128 }
1129 })
1130 });
1131 #[cfg(feature = "client-lifecycle")]
1132 if let Some(lifecycle) = &self.lifecycle {
1133 lifecycle.cancel_scope(closed_generation);
1134 }
1135 self.notify_connection_shutdown();
1136 self.clear_sent_node_waiters();
1145 self.is_logged_in.store(false, Ordering::Relaxed);
1146 #[cfg(feature = "client-lifecycle")]
1147 drop(login_transition);
1148 self.is_ready.store(false, Ordering::Relaxed);
1149 self.is_connected.store(false, Ordering::Release);
1154 #[cfg(feature = "voip-runtime")]
1157 {
1158 self.call_registry.abort_all();
1159 crate::voip::facade::drain_pending_outgoing_on_disconnect(self);
1162 }
1163 if let Some(transport) = self.transport.lock().await.take() {
1169 transport.disconnect().await;
1170 }
1171 *self.transport_events.lock().await = None;
1172 *self.noise_socket.lock().await = None;
1173 wacore::telemetry::set_connected(false);
1178 let _ =
1181 self.send_active_receipts
1182 .compare_exchange(1, 0, Ordering::AcqRel, Ordering::Acquire);
1183 self.chat_lanes.clear().await;
1187 self.pending_retries
1190 .lock()
1191 .unwrap_or_else(|p| p.into_inner())
1192 .clear();
1193 let flush_gate = self.signal_flush_lifecycle.lock().await;
1209 if let Some(client) = self.self_weak.get().and_then(|w| w.upgrade()) {
1210 client
1211 .teardown_inbound_commits_bounded(Duration::from_secs(5))
1212 .await;
1213 } else {
1214 log::error!(
1218 "cleanup_connection_state: self_weak upgrade failed; dropping uncommitted drain entries and their unflushed Signal state"
1219 );
1220 self.signal_cache.clear().await;
1221 }
1222 self.swap_message_semaphore(1);
1224 self.stats.reset_connection_activity();
1227 self.pending_device_sync.clear().await;
1228 self.offline_sync_completed.store(false, Ordering::Relaxed);
1230 self.offline_sync_finish_started
1231 .store(false, Ordering::Relaxed);
1232 self.clear_offline_receipt_buffer();
1233 if self.inbound_commit_batch.reset() {
1238 log::warn!(
1239 "cleanup_connection_state: dropping unflushed Signal state along with late uncommitted drain entries"
1240 );
1241 self.signal_cache.clear().await;
1242 }
1243 drop(flush_gate);
1245 self.offline_batch.reset();
1246 self.offline_sync_metrics
1247 .active
1248 .store(false, Ordering::Release);
1249 self.offline_sync_metrics
1250 .total_messages
1251 .store(0, Ordering::Release);
1252 self.offline_sync_metrics
1253 .processed_messages
1254 .store(0, Ordering::Release);
1255 match self.offline_sync_metrics.start_time.lock() {
1256 Ok(mut guard) => *guard = None,
1257 Err(poison) => *poison.into_inner() = None,
1258 }
1259 self.history_sync_activity.reset();
1260 let waiter_count = {
1265 let mut waiters_map = self.response_waiters_guard();
1266 let count = waiters_map.len();
1267 waiters_map.clear();
1271 count
1272 };
1273 if waiter_count > 0 {
1274 debug!(
1275 "Dropping {} orphaned IQ response waiter(s) on disconnect",
1276 waiter_count
1277 );
1278 }
1279
1280 *self.app_state_key_requests.lock().await = HashMap::new();
1283 self.app_state_syncing.clear();
1284
1285 *self.media_conn.write().await = None;
1287
1288 if let Some(proc) = self.app_state_processor.lock().await.as_ref() {
1290 proc.clear_key_cache().await;
1291 }
1292 #[cfg(feature = "client-lifecycle")]
1293 drop(scope_close);
1294 }
1295
1296 pub async fn wait_for_socket(&self, timeout: Duration) -> Result<(), ConnectError> {
1304 if self.is_connected() {
1306 return Ok(());
1307 }
1308
1309 let notified = self.socket_ready_notifier.listen();
1312 if self.is_connected() {
1313 return Ok(());
1314 }
1315
1316 rt_timeout(&*self.runtime, timeout, notified)
1317 .await
1318 .map_err(|_| ConnectError::Timeout {
1319 stage: ConnectStage::Socket,
1320 timeout,
1321 })
1322 }
1323
1324 pub async fn wait_for_connected(&self, timeout: Duration) -> Result<(), ConnectError> {
1332 if self.is_fully_ready() {
1334 return Ok(());
1335 }
1336
1337 let notified = self.connected_notifier.listen();
1340 if self.is_fully_ready() {
1341 return Ok(());
1342 }
1343
1344 rt_timeout(&*self.runtime, timeout, notified)
1345 .await
1346 .map_err(|_| ConnectError::Timeout {
1347 stage: ConnectStage::Ready,
1348 timeout,
1349 })
1350 }
1351
1352 pub fn is_connected(&self) -> bool {
1353 self.is_connected.load(Ordering::Acquire)
1354 }
1355
1356 #[cfg(test)]
1358 pub(crate) fn set_connected_for_test(&self, connected: bool) {
1359 self.is_connected.store(connected, Ordering::Release);
1360 }
1361
1362 pub fn is_logged_in(&self) -> bool {
1363 self.is_logged_in.load(Ordering::Relaxed)
1364 }
1365
1366 pub(crate) fn can_reach_server(&self) -> bool {
1381 self.is_connected()
1382 && self.is_logged_in()
1383 && self.authenticated_generation.load(Ordering::SeqCst)
1384 == self.connection_generation.load(Ordering::SeqCst)
1385 && self.is_running.load(Ordering::Relaxed)
1386 && !self.expected_disconnect.load(Ordering::Relaxed)
1392 }
1393}
1394
1395#[cfg(test)]
1396mod tests {
1397 use super::*;
1398 use std::time::Duration;
1399
1400 #[tokio::test]
1401 async fn wait_for_socket_resolves_immediately_once_connected() {
1402 let client = crate::test_utils::create_test_client().await;
1403 client.set_connected_for_test(true);
1404
1405 client
1406 .wait_for_socket(Duration::from_millis(50))
1407 .await
1408 .expect("an already connected client must not wait");
1409 }
1410
1411 #[tokio::test]
1412 async fn wait_for_socket_times_out_at_the_socket_stage() {
1413 let client = crate::test_utils::create_test_client().await;
1414
1415 let timeout = Duration::from_millis(50);
1416 let error = client
1417 .wait_for_socket(timeout)
1418 .await
1419 .expect_err("a disconnected client must time out");
1420 assert!(matches!(
1421 error,
1422 ConnectError::Timeout {
1423 stage: ConnectStage::Socket,
1424 timeout: waited,
1425 } if waited == timeout
1426 ));
1427 }
1428
1429 #[tokio::test]
1430 async fn wait_for_connected_resolves_immediately_once_fully_ready() {
1431 let client = crate::test_utils::create_test_client().await;
1432 client.set_connected_for_test(true);
1433 client.is_logged_in.store(true, Ordering::Relaxed);
1434 client.is_ready.store(true, Ordering::Relaxed);
1435
1436 client
1437 .wait_for_connected(Duration::from_millis(50))
1438 .await
1439 .expect("a fully ready client must not wait");
1440 }
1441
1442 #[tokio::test]
1443 async fn wait_for_connected_times_out_at_the_ready_stage() {
1444 let client = crate::test_utils::create_test_client().await;
1445 client.set_connected_for_test(true);
1447
1448 let timeout = Duration::from_millis(50);
1449 let error = client
1450 .wait_for_connected(timeout)
1451 .await
1452 .expect_err("a client that never logged in must time out");
1453 assert!(matches!(
1454 error,
1455 ConnectError::Timeout {
1456 stage: ConnectStage::Ready,
1457 timeout: waited,
1458 } if waited == timeout
1459 ));
1460 }
1461
1462 #[tokio::test]
1463 async fn logout_tears_down_an_offline_client_without_sending_the_iq() {
1464 let client = crate::test_utils::create_test_client().await;
1465
1466 tokio::time::timeout(Duration::from_secs(5), client.logout())
1467 .await
1468 .expect("logout must not block on an offline client");
1469
1470 assert!(!client.enable_auto_reconnect.load(Ordering::Relaxed));
1471 assert!(!client.is_connected());
1472 }
1473
1474 #[tokio::test]
1475 async fn logout_still_tears_down_when_the_deregistration_iq_fails() {
1476 let client = crate::test_utils::create_test_client().await;
1477 client.set_connected_for_test(true);
1479
1480 tokio::time::timeout(Duration::from_secs(5), client.logout())
1481 .await
1482 .expect("a failed deregistration IQ must not block logout");
1483
1484 assert!(!client.enable_auto_reconnect.load(Ordering::Relaxed));
1485 assert!(!client.is_connected());
1486 }
1487
1488 #[tokio::test]
1489 async fn connect_rejects_an_already_connected_client() {
1490 let client = crate::test_utils::create_test_client().await;
1491 client.set_connected_for_test(true);
1492
1493 let error = client
1494 .connect()
1495 .await
1496 .expect_err("connecting twice must be refused");
1497 assert!(matches!(error, ConnectError::AlreadyConnected));
1498 }
1499
1500 const CAPPED_BACKOFF_ATTEMPTS: u32 = 40;
1505
1506 async fn run_until_parked_in_backoff(client: &Arc<Client>) -> tokio::task::JoinHandle<()> {
1511 client
1512 .auto_reconnect_errors
1513 .store(CAPPED_BACKOFF_ATTEMPTS, Ordering::Relaxed);
1514
1515 let runner = client.clone();
1516 let run = tokio::spawn(async move { runner.run().await });
1517
1518 crate::test_utils::poll_until("the run loop to reach its reconnect backoff", || {
1519 client.auto_reconnect_errors.load(Ordering::Relaxed) > CAPPED_BACKOFF_ATTEMPTS
1520 })
1521 .await;
1522
1523 run
1524 }
1525
1526 #[tokio::test]
1533 async fn disconnect_interrupts_the_reconnect_backoff() {
1534 let client = crate::test_utils::create_test_client().await;
1535 let run = run_until_parked_in_backoff(&client).await;
1536
1537 client.disconnect().await;
1538
1539 tokio::time::timeout(Duration::from_secs(10), run)
1540 .await
1541 .expect("run() must return when disconnect() fires, not after the 900s backoff")
1542 .expect("the run task must not panic");
1543 }
1544
1545 #[tokio::test]
1550 async fn signal_shutdown_sync_interrupts_the_reconnect_backoff() {
1551 let client = crate::test_utils::create_test_client().await;
1552 let run = run_until_parked_in_backoff(&client).await;
1553
1554 client.signal_shutdown_sync();
1555
1556 tokio::time::timeout(Duration::from_secs(10), run)
1557 .await
1558 .expect("run() must return when signal_shutdown_sync() fires")
1559 .expect("the run task must not panic");
1560 }
1561
1562 #[tokio::test]
1568 async fn a_connection_level_shutdown_does_not_cut_the_backoff_short() {
1569 let client = crate::test_utils::create_test_client().await;
1570 let run = run_until_parked_in_backoff(&client).await;
1571
1572 client.notify_connection_shutdown();
1573
1574 tokio::time::sleep(Duration::from_millis(200)).await;
1577 assert_eq!(
1578 client.auto_reconnect_errors.load(Ordering::Relaxed),
1579 CAPPED_BACKOFF_ATTEMPTS + 1,
1580 "a per-connection shutdown must not release the reconnect backoff"
1581 );
1582
1583 client.disconnect().await;
1584 tokio::time::timeout(Duration::from_secs(10), run)
1585 .await
1586 .expect("run() must still return on a terminal shutdown")
1587 .expect("the run task must not panic");
1588 }
1589}