1use crate::client::Client;
2use crate::client::ClientError;
3use crate::client::ResponseWaiter;
4use crate::socket::error::{EncryptSendError, SocketError};
5use futures::FutureExt;
6use std::num::NonZeroU64;
7use std::sync::Arc;
8use std::sync::atomic::Ordering;
9use std::time::Duration;
10use thiserror::Error;
11use wacore::runtime::timeout as rt_timeout;
12use wacore_binary::Node;
13
14pub use wacore::request::{InfoQuery, InfoQueryType, RequestUtils};
15
16pub(crate) const DEFAULT_IQ_TIMEOUT: Duration = Duration::from_secs(75);
21const IQ_ID_ATTR: &str = "id";
22const IQ_TAG: &str = "iq";
23
24#[cfg(not(target_arch = "wasm32"))]
29type IqSendFuture<'a> =
30 std::pin::Pin<Box<dyn Future<Output = Result<(), ClientError>> + Send + 'a>>;
31#[cfg(target_arch = "wasm32")]
32type IqSendFuture<'a> =
33 std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), ClientError>> + 'a>>;
34
35#[cfg(not(target_arch = "wasm32"))]
43type IqOnSent<'a> = Box<dyn FnOnce() + Send + 'a>;
44#[cfg(target_arch = "wasm32")]
45type IqOnSent<'a> = Box<dyn FnOnce() + 'a>;
46
47pub(crate) struct ResponseWaiterGuard {
57 waiters: Arc<std::sync::Mutex<crate::client::ResponseWaiterMap>>,
58 req_id: String,
59 cleanup_generation: NonZeroU64,
60}
61
62impl ResponseWaiterGuard {
63 pub(crate) fn new(
64 waiters: Arc<std::sync::Mutex<crate::client::ResponseWaiterMap>>,
65 req_id: String,
66 cleanup_generation: NonZeroU64,
67 ) -> Self {
68 Self {
69 waiters,
70 req_id,
71 cleanup_generation,
72 }
73 }
74}
75
76impl Drop for ResponseWaiterGuard {
77 fn drop(&mut self) {
78 self.waiters
79 .lock()
80 .unwrap_or_else(|p| p.into_inner())
81 .remove_guarded(&self.req_id, self.cleanup_generation);
82 }
83}
84
85enum PreparedIq {
88 Encoded(Vec<u8>),
90 Query(Box<InfoQuery<'static>>),
94}
95
96#[derive(Debug, Error)]
97#[non_exhaustive]
98pub enum IqError {
99 #[error("IQ request timed out")]
100 Timeout,
101 #[error("client is not connected")]
102 NotConnected,
103 #[error("socket error")]
104 Socket(#[from] SocketError),
105 #[error("encrypted send pipeline failed")]
106 EncryptSend(#[from] EncryptSendError),
107 #[error("client state prevented send")]
110 ClientState(#[source] Box<ClientError>),
111 #[error("received disconnect node during IQ wait: {0:?}")]
112 Disconnected(Box<Node>),
113 #[error("received a server error response: code={code}, text='{text}'")]
114 ServerError {
115 code: u16,
116 text: String,
117 error_type: Option<String>,
119 backoff: Option<u32>,
121 },
122 #[error("received unexpected IQ response type: {got:?}")]
123 UnexpectedResponseType { got: Option<String> },
124 #[error("internal channel closed unexpectedly")]
125 InternalChannelClosed,
126 #[error("IQ request ID is already in flight: {0}")]
127 DuplicateRequestId(String),
128 #[error("failed to encode IQ request")]
129 EncodeError(#[source] anyhow::Error),
130 #[error("failed to parse IQ response")]
131 ParseError(#[from] anyhow::Error),
132}
133
134impl IqError {
135 pub(crate) fn is_transport_unavailable(&self) -> bool {
136 match self {
137 IqError::NotConnected | IqError::Disconnected(_) | IqError::InternalChannelClosed => {
138 true
139 }
140 IqError::EncryptSend(error) => error.is_transport_unavailable(),
141 IqError::ClientState(client) => client.is_transport_unavailable(),
142 _ => false,
143 }
144 }
145
146 pub(crate) fn is_timeout(&self) -> bool {
151 match self {
152 IqError::Timeout => true,
153 IqError::NotConnected
154 | IqError::Socket(_)
155 | IqError::EncryptSend(_)
156 | IqError::ClientState(_)
157 | IqError::Disconnected(_)
158 | IqError::ServerError { .. }
159 | IqError::UnexpectedResponseType { .. }
160 | IqError::InternalChannelClosed
161 | IqError::DuplicateRequestId(_)
162 | IqError::EncodeError(_)
163 | IqError::ParseError(_) => false,
164 }
165 }
166}
167
168impl From<wacore::request::IqError> for IqError {
169 fn from(err: wacore::request::IqError) -> Self {
170 match err {
171 wacore::request::IqError::Timeout => Self::Timeout,
172 wacore::request::IqError::NotConnected => Self::NotConnected,
173 wacore::request::IqError::Disconnected(node) => Self::Disconnected(node),
174 wacore::request::IqError::ServerError {
175 code,
176 text,
177 error_type,
178 backoff,
179 } => Self::ServerError {
180 code,
181 text,
182 error_type,
183 backoff,
184 },
185 wacore::request::IqError::UnexpectedResponseType { got } => {
186 Self::UnexpectedResponseType { got }
187 }
188 wacore::request::IqError::InternalChannelClosed => Self::InternalChannelClosed,
189 _ => Self::InternalChannelClosed,
192 }
193 }
194}
195
196impl Client {
197 pub(crate) fn generate_request_id(&self) -> String {
198 self.get_request_utils().generate_request_id()
199 }
200
201 pub fn generate_message_id(&self) -> String {
217 self.generate_message_id_at(wacore::time::now_secs_u64())
218 }
219
220 pub(crate) fn generate_message_id_at(&self, unix_secs: u64) -> String {
223 let device_snapshot = self.persistence_manager.get_device_snapshot();
224 RequestUtils::message_id_at(device_snapshot.pn.as_ref(), unix_secs)
227 }
228
229 fn get_request_utils(&self) -> RequestUtils {
230 RequestUtils::with_counter(self.unique_id.clone(), self.id_counter.clone())
231 }
232
233 #[cfg_attr(
287 feature = "tracing",
288 tracing::instrument(
289 name = "wa.iq",
290 level = "debug",
291 skip_all,
292 fields(
293 ns = %query.namespace,
294 kind = ?query.query_type,
295 lid = tracing::field::Empty,
296 pn = tracing::field::Empty
297 ),
298 err(Debug)
299 )
300 )]
301 pub async fn send_iq(
302 &self,
303 query: InfoQuery<'_>,
304 ) -> Result<Arc<wacore_binary::OwnedNodeRef>, IqError> {
305 #[cfg(feature = "tracing")]
306 self.record_identity_on_span(&tracing::Span::current());
307
308 let iq_timeout = query.timeout.unwrap_or(DEFAULT_IQ_TIMEOUT);
309 let req_id = query
310 .id
311 .clone()
312 .unwrap_or_else(|| self.generate_request_id());
313
314 let request_utils = self.get_request_utils();
315 let node = request_utils.build_iq_node(query, Some(req_id.clone()));
316
317 self.send_and_wait_iq(
318 req_id,
319 iq_timeout,
320 Box::pin(async { self.send_node(node).await }),
321 None,
322 )
323 .await
324 }
325
326 pub async fn send_iq_node(
332 &self,
333 node: Node,
334 timeout: Option<Duration>,
335 ) -> Result<Arc<wacore_binary::OwnedNodeRef>, IqError> {
336 self.send_iq_node_then(node, timeout, None).await
337 }
338
339 #[cfg_attr(
342 feature = "tracing",
343 tracing::instrument(name = "wa.iq.node", level = "debug", skip_all, err(Debug))
344 )]
345 pub(crate) async fn send_iq_node_then(
346 &self,
347 mut node: Node,
348 timeout: Option<Duration>,
349 on_sent: Option<IqOnSent<'_>>,
350 ) -> Result<Arc<wacore_binary::OwnedNodeRef>, IqError> {
351 #[cfg(feature = "tracing")]
352 self.record_identity_on_span(&tracing::Span::current());
353
354 if node.tag.as_ref() != IQ_TAG {
355 return Err(IqError::ParseError(anyhow::anyhow!(
356 "expected an <iq> stanza, got <{}>",
357 node.tag
358 )));
359 }
360
361 let req_id = node
362 .attrs
363 .get(IQ_ID_ATTR)
364 .map(|value| value.as_str().into_owned())
365 .filter(|value| !value.is_empty())
366 .unwrap_or_else(|| self.generate_request_id());
367 node.attrs.insert(IQ_ID_ATTR, req_id.clone());
368
369 self.send_and_wait_iq(
370 req_id,
371 timeout.unwrap_or(DEFAULT_IQ_TIMEOUT),
372 Box::pin(async { self.send_node(node).await }),
373 on_sent,
374 )
375 .await
376 }
377
378 pub async fn execute<S>(&self, spec: S) -> Result<S::Response, IqError>
392 where
393 S: wacore::iq::spec::IqSpec,
394 {
395 let req_id = self.generate_request_id();
399 let mut buf = Vec::new();
400 let prepared = match spec.encode_iq_direct(&req_id, &mut buf) {
401 Ok(true) => PreparedIq::Encoded(buf),
402 Ok(false) => PreparedIq::Query(Box::new(spec.build_iq())),
403 Err(e) => return Err(IqError::EncodeError(e)),
404 };
405
406 let response = self.execute_prepared(req_id, prepared).await?;
407 spec.parse_response(response.get())
408 .map_err(IqError::ParseError)
409 }
410
411 async fn execute_prepared(
414 &self,
415 req_id: String,
416 prepared: PreparedIq,
417 ) -> Result<Arc<wacore_binary::OwnedNodeRef>, IqError> {
418 match prepared {
419 PreparedIq::Encoded(buf) => {
423 self.send_and_wait_iq(
424 req_id,
425 DEFAULT_IQ_TIMEOUT,
426 Box::pin(async { self.send_raw_bytes(buf).await }),
427 None,
428 )
429 .await
430 }
431 PreparedIq::Query(iq) => {
432 let mut iq = *iq;
433 if iq.id.is_none() {
436 iq.id = Some(req_id);
437 }
438 self.send_iq(iq).await
439 }
440 }
441 }
442
443 async fn send_and_wait_iq(
452 &self,
453 req_id: String,
454 timeout: Duration,
455 send_fn: IqSendFuture<'_>,
456 on_sent: Option<IqOnSent<'_>>,
457 ) -> Result<Arc<wacore_binary::OwnedNodeRef>, IqError> {
458 let _t = wacore::telemetry::timer(wacore::telemetry::IQ_DURATION);
459 if !self.is_running.load(Ordering::Relaxed) {
460 wacore::telemetry::iq("error");
461 return Err(IqError::NotConnected);
462 }
463
464 let (tx, rx) = futures::channel::oneshot::channel();
465 let cleanup_generation = {
466 let mut waiters = self.response_waiters_guard();
467 let Some(cleanup_generation) =
471 waiters.try_insert_guarded(req_id.clone(), ResponseWaiter::Iq(tx))
472 else {
473 wacore::telemetry::iq("error");
474 return Err(IqError::DuplicateRequestId(req_id));
475 };
476 cleanup_generation
477 };
478 let _waiter_guard =
483 ResponseWaiterGuard::new(self.response_waiters.clone(), req_id, cleanup_generation);
484
485 let shutdown = wacore::runtime::wait_for_shutdown(&self.connection_shutdown_signal());
488
489 if !self.is_running.load(Ordering::Acquire) {
490 wacore::telemetry::iq("error");
491 return Err(IqError::NotConnected);
492 }
493
494 if let Err(e) = send_fn.await {
495 wacore::telemetry::iq("error");
496 return match e {
497 ClientError::Socket(s_err) => Err(IqError::Socket(s_err)),
498 ClientError::EncryptSend(es_err) => Err(IqError::EncryptSend(es_err)),
499 ClientError::NotConnected => Err(IqError::NotConnected),
500 other => Err(IqError::ClientState(Box::new(other))),
504 };
505 }
506
507 if let Some(on_sent) = on_sent {
508 on_sent();
509 }
510
511 let request_utils = self.get_request_utils();
512 let result = futures::select! {
513 result = rt_timeout(&*self.runtime, timeout, rx).fuse() => {
514 match result {
515 Ok(Ok(response_node)) => match request_utils.parse_iq_response(response_node.get()) {
516 Ok(()) => Ok(response_node),
517 Err(e) => Err(e.into()),
518 },
519 Ok(Err(_)) => Err(IqError::InternalChannelClosed),
520 Err(_) => Err(IqError::Timeout),
521 }
522 }
523 _ = shutdown.fuse() => Err(IqError::NotConnected),
524 };
525 wacore::telemetry::iq(match &result {
526 Ok(_) => "ok",
527 Err(IqError::Timeout) => "timeout",
528 Err(_) => "error",
529 });
530 result
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::{IQ_ID_ATTR, IQ_TAG, IqError, ResponseWaiterGuard};
537 use crate::client::{ResponseWaiter, ResponseWaiterMap};
538 use std::sync::atomic::Ordering;
539 use std::sync::{Arc, Mutex};
540 use wacore_binary::builder::NodeBuilder;
541
542 #[tokio::test]
543 async fn send_iq_node_rejects_non_iq_stanzas() {
544 let client = crate::test_utils::create_test_client_with_name("invalid_iq_node").await;
545 let error = client
546 .send_iq_node(NodeBuilder::new("message").build(), None)
547 .await
548 .expect_err("a non-IQ stanza must be rejected before transport");
549 assert!(matches!(error, IqError::ParseError(_)));
550 }
551
552 #[tokio::test]
553 async fn send_iq_node_rejects_duplicate_in_flight_id() {
554 let client = crate::test_utils::create_test_client_with_name("duplicate_iq_id").await;
555 client.is_running.store(true, Ordering::Release);
556 let request_id = "duplicate-request";
557 let (tx, _rx) = futures::channel::oneshot::channel();
558 client
559 .response_waiters_guard()
560 .insert(request_id.to_owned(), ResponseWaiter::Iq(tx));
561
562 let error = client
563 .send_iq_node(
564 NodeBuilder::new(IQ_TAG)
565 .attr(IQ_ID_ATTR, request_id)
566 .build(),
567 None,
568 )
569 .await
570 .expect_err("a duplicate ID must not replace an existing waiter");
571 assert!(matches!(error, IqError::DuplicateRequestId(id) if id == request_id));
572 assert!(client.response_waiters_guard().contains_key(request_id));
573
574 client.response_waiters_guard().remove(request_id);
575 client.is_running.store(false, Ordering::Release);
576 }
577
578 #[test]
579 fn converts_unexpected_response_type() {
580 let err = IqError::from(wacore::request::IqError::UnexpectedResponseType {
581 got: Some("get".to_string()),
582 });
583
584 match err {
585 IqError::UnexpectedResponseType { got } => assert_eq!(got.as_deref(), Some("get")),
586 other => panic!("expected UnexpectedResponseType, got {other:?}"),
587 }
588 }
589
590 #[test]
594 fn waiter_guard_removes_pending_entry_on_drop() {
595 let waiters: Arc<Mutex<ResponseWaiterMap>> =
596 Arc::new(Mutex::new(ResponseWaiterMap::default()));
597 let (tx, _rx) = futures::channel::oneshot::channel();
598 let cleanup_generation = waiters
599 .lock()
600 .unwrap()
601 .try_insert_guarded("req-1".to_string(), ResponseWaiter::Iq(tx))
602 .expect("unique request ID");
603 assert!(waiters.lock().unwrap().contains_key("req-1"));
604
605 {
606 let _guard = ResponseWaiterGuard {
607 waiters: waiters.clone(),
608 req_id: "req-1".to_string(),
609 cleanup_generation,
610 };
611 }
612 assert!(
613 !waiters.lock().unwrap().contains_key("req-1"),
614 "dropping the guard must remove the pending waiter"
615 );
616 }
617
618 #[test]
621 fn waiter_guard_drop_is_noop_when_already_resolved() {
622 let waiters: Arc<Mutex<ResponseWaiterMap>> =
623 Arc::new(Mutex::new(ResponseWaiterMap::default()));
624 let (tx, _rx) = futures::channel::oneshot::channel();
625 let cleanup_generation = waiters
626 .lock()
627 .unwrap()
628 .try_insert_guarded("req-1".to_string(), ResponseWaiter::Iq(tx))
629 .expect("unique request ID");
630 waiters.lock().unwrap().remove("req-1");
632 {
633 let _guard = ResponseWaiterGuard {
634 waiters: waiters.clone(),
635 req_id: "req-1".to_string(),
636 cleanup_generation,
637 };
638 }
639 assert!(waiters.lock().unwrap().is_empty());
640 }
641
642 #[test]
643 fn stale_waiter_guard_preserves_a_reused_request_id() {
644 let waiters = Arc::new(Mutex::new(ResponseWaiterMap::default()));
645 let (old_tx, _old_rx) = futures::channel::oneshot::channel();
646 let old_generation = waiters
647 .lock()
648 .unwrap()
649 .try_insert_guarded("reused-id".to_string(), ResponseWaiter::Iq(old_tx))
650 .expect("initial request ID");
651 let old_guard = ResponseWaiterGuard {
652 waiters: waiters.clone(),
653 req_id: "reused-id".to_string(),
654 cleanup_generation: old_generation,
655 };
656
657 waiters.lock().unwrap().remove("reused-id");
660 let (new_tx, _new_rx) = futures::channel::oneshot::channel();
661 waiters
662 .lock()
663 .unwrap()
664 .try_insert_guarded("reused-id".to_string(), ResponseWaiter::Iq(new_tx))
665 .expect("reused request ID");
666
667 drop(old_guard);
668 assert!(
669 waiters.lock().unwrap().contains_key("reused-id"),
670 "an old guard must not remove the newer registration"
671 );
672 }
673
674 #[test]
675 fn disconnected_waiter_guard_preserves_a_reused_request_id() {
676 let waiters = Arc::new(Mutex::new(ResponseWaiterMap::default()));
677 let (old_tx, _old_rx) = futures::channel::oneshot::channel();
678 let old_generation = waiters
679 .lock()
680 .unwrap()
681 .try_insert_guarded("reused-id".to_string(), ResponseWaiter::Iq(old_tx))
682 .expect("initial request ID");
683 let old_guard = ResponseWaiterGuard {
684 waiters: waiters.clone(),
685 req_id: "reused-id".to_string(),
686 cleanup_generation: old_generation,
687 };
688
689 waiters.lock().unwrap().clear();
692 let (new_tx, _new_rx) = futures::channel::oneshot::channel();
693 waiters
694 .lock()
695 .unwrap()
696 .try_insert_guarded("reused-id".to_string(), ResponseWaiter::Iq(new_tx))
697 .expect("reused request ID");
698
699 drop(old_guard);
700 assert!(
701 waiters.lock().unwrap().contains_key("reused-id"),
702 "a pre-disconnect guard must not remove the post-reconnect waiter"
703 );
704 }
705}