1mod client;
2mod connect;
3mod connection;
4mod event_loop;
5mod request_builder;
6mod retry;
7mod services;
8
9#[derive(Debug, Clone)]
12pub struct EndpointInfo {
13 pub endpoint: EndpointDescription,
15 pub user_identity_token: IdentityToken,
17 pub preferred_locales: Vec<String>,
19}
20
21impl From<EndpointDescription> for EndpointInfo {
22 fn from(value: EndpointDescription) -> Self {
23 Self {
24 endpoint: value,
25 user_identity_token: IdentityToken::Anonymous,
26 preferred_locales: Vec::new(),
27 }
28 }
29}
30
31impl From<(EndpointDescription, IdentityToken)> for EndpointInfo {
32 fn from(value: (EndpointDescription, IdentityToken)) -> Self {
33 Self {
34 endpoint: value.0,
35 user_identity_token: value.1,
36 preferred_locales: Vec::new(),
37 }
38 }
39}
40
41use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
42use std::sync::Arc;
43use std::time::{Duration, Instant};
44
45use arc_swap::ArcSwap;
46pub use client::Client;
47pub use connect::SessionConnectMode;
48pub use connection::{
49 ConnectionSource, DirectConnectionSource, ReverseConnectionSource, SessionBuilder,
50};
51pub use event_loop::{SessionActivity, SessionEventLoop, SessionPollResult};
52use opcua_core::handle::AtomicHandle;
53use opcua_core::sync::{Mutex, RwLock};
54pub use request_builder::UARequest;
55pub use retry::{DefaultRetryPolicy, RequestRetryPolicy};
56pub use services::attributes::{
57 HistoryRead, HistoryReadAction, HistoryUpdate, HistoryUpdateAction, Read, Write,
58};
59pub use services::method::Call;
60pub use services::node_management::{AddNodes, AddReferences, DeleteNodes, DeleteReferences};
61pub use services::session::{ActivateSession, Cancel, CloseSession, CreateSession};
62use services::subscriptions::state::SubscriptionState;
63pub use services::subscriptions::{
64 CreateMonitoredItems, CreateSubscription, DataChangeCallback, DeleteMonitoredItems,
65 DeleteSubscriptions, EventCallback, ModifyMonitoredItems, ModifySubscription, MonitoredItem,
66 MonitoredItemMap, OnSubscriptionNotification, OnSubscriptionNotificationCore,
67 PreInsertMonitoredItems, Publish, PublishLimits, Republish, SetMonitoringMode,
68 SetPublishingMode, SetTriggering, Subscription, SubscriptionActivity, SubscriptionCache,
69 SubscriptionCallbacks, SubscriptionEventLoopState, TransferSubscriptions,
70};
71pub use services::view::{
72 Browse, BrowseNext, RegisterNodes, TranslateBrowsePaths, UnregisterNodes,
73};
74use tracing::{error, info};
75
76#[allow(unused)]
77macro_rules! session_warn {
78 ($session: expr, $($arg:tt)*) => {
79 tracing::warn!("session:{} {}", $session.session_id(), format!($($arg)*));
80 }
81}
82#[allow(unused)]
83pub(crate) use session_warn;
84
85#[allow(unused)]
86macro_rules! session_error {
87 ($session: expr, $($arg:tt)*) => {
88 tracing::error!("session:{} {}", $session.session_id(), format!($($arg)*));
89 }
90}
91#[allow(unused)]
92pub(crate) use session_error;
93
94#[allow(unused)]
95macro_rules! session_debug {
96 ($session: expr, $($arg:tt)*) => {
97 tracing::debug!("session:{} {}", $session.session_id(), format!($($arg)*));
98 }
99}
100#[allow(unused)]
101pub(crate) use session_debug;
102
103#[allow(unused)]
104macro_rules! session_trace {
105 ($session: expr, $($arg:tt)*) => {
106 tracing::trace!("session:{} {}", $session.session_id(), format!($($arg)*));
107 }
108}
109#[allow(unused)]
110pub(crate) use session_trace;
111
112use opcua_core::ResponseMessage;
113use opcua_types::{
114 ApplicationDescription, ContextOwned, DecodingOptions, EndpointDescription, Error, IntegerId,
115 NamespaceMap, NodeId, ReadValueId, RequestHeader, ResponseHeader, StatusCode,
116 TimestampsToReturn, TypeLoader, UAString, VariableId, Variant,
117};
118
119use crate::browser::Browser;
120use crate::transport::Connector;
121use crate::{AsyncSecureChannel, ClientConfig, ExponentialBackoff, SessionRetryPolicy};
122
123use super::IdentityToken;
124
125pub(crate) fn process_service_result(response_header: &ResponseHeader) -> Result<(), Error> {
128 if response_header.service_result.is_bad() {
129 info!(
130 "Received a bad service result {} from the request",
131 response_header.service_result
132 );
133 Err(Error::new(
134 response_header.service_result,
135 "Received a bad service result from the server",
136 ))
137 } else {
138 Ok(())
139 }
140}
141
142pub(crate) fn process_unexpected_response(response: ResponseMessage) -> Error {
143 match response {
144 ResponseMessage::ServiceFault(service_fault) => {
145 error!(
146 "Received a service fault of {} for the request",
147 service_fault.response_header.service_result
148 );
149 Error::new(
150 service_fault.response_header.service_result,
151 "Request returned ServiceFault",
152 )
153 }
154 _ => {
155 error!(
156 "Received an unexpected response to the request: {}",
157 response.type_name()
158 );
159 Error::new(
160 StatusCode::BadUnknownResponse,
161 format!(
162 "Received an unexpected response to the request: {}",
163 response.type_name()
164 ),
165 )
166 }
167 }
168}
169
170#[derive(Clone, Copy)]
171pub(super) enum SessionState {
172 Disconnected,
173 Connected,
174 Connecting,
175}
176
177static NEXT_SESSION_ID: AtomicU32 = AtomicU32::new(1);
178
179pub struct Session {
186 pub(super) channel: AsyncSecureChannel,
187 pub(super) state_watch_rx: tokio::sync::watch::Receiver<SessionState>,
188 pub(super) state_watch_tx: tokio::sync::watch::Sender<SessionState>,
189 pub(super) session_id: Arc<ArcSwap<NodeId>>,
190 pub(super) internal_session_id: AtomicU32,
191 pub(super) session_name: UAString,
192 pub(super) application_description: ApplicationDescription,
193 pub(super) request_timeout: Duration,
194 pub(super) publish_timeout: Duration,
195 pub(super) recreate_monitored_items_chunk: usize,
196 pub(super) recreate_subscriptions: bool,
197 pub(super) should_reconnect: AtomicBool,
198 pub(super) session_timeout: f64,
199 pub subscription_state: Mutex<SubscriptionState>,
201 pub(super) publish_limits_watch_rx: tokio::sync::watch::Receiver<PublishLimits>,
202 pub(super) publish_limits_watch_tx: tokio::sync::watch::Sender<PublishLimits>,
203 pub(super) monitored_item_handle: AtomicHandle,
204 pub(super) trigger_publish_tx: tokio::sync::watch::Sender<Instant>,
205 pub(super) session_nonce_length: usize,
206 decoding_options: DecodingOptions,
207 pub(crate) close_tx: tokio::sync::watch::Sender<bool>,
208}
209
210impl Session {
211 #[allow(clippy::too_many_arguments)]
212 pub(crate) fn new<T: Connector + Send + Sync + 'static>(
213 channel: AsyncSecureChannel,
214 session_name: UAString,
215 application_description: ApplicationDescription,
216 session_retry_policy: SessionRetryPolicy,
217 decoding_options: DecodingOptions,
218 config: &ClientConfig,
219 session_id: Option<NodeId>,
220 connector: T,
221 ) -> (Arc<Self>, SessionEventLoop<T>) {
222 let (publish_limits_watch_tx, publish_limits_watch_rx) =
223 tokio::sync::watch::channel(PublishLimits::new());
224 let (state_watch_tx, state_watch_rx) =
225 tokio::sync::watch::channel(SessionState::Disconnected);
226 let (trigger_publish_tx, trigger_publish_rx) = tokio::sync::watch::channel(Instant::now());
227 let (close_tx, close_rx) = tokio::sync::watch::channel(false);
228
229 let session = Arc::new(Session {
230 channel,
231 internal_session_id: AtomicU32::new(NEXT_SESSION_ID.fetch_add(1, Ordering::Relaxed)),
232 state_watch_rx,
233 state_watch_tx,
234 session_id: Arc::new(ArcSwap::new(Arc::new(session_id.unwrap_or_default()))),
235 session_name,
236 application_description,
237 request_timeout: config.request_timeout,
238 session_timeout: config.session_timeout as f64,
239 publish_timeout: config.publish_timeout,
240 recreate_monitored_items_chunk: config.performance.recreate_monitored_items_chunk,
241 recreate_subscriptions: config.recreate_subscriptions,
242 should_reconnect: AtomicBool::new(true),
243 subscription_state: Mutex::new(SubscriptionState::new(
244 config.min_publish_interval,
245 publish_limits_watch_tx.clone(),
246 )),
247 monitored_item_handle: AtomicHandle::new(1000),
248 publish_limits_watch_rx,
249 publish_limits_watch_tx,
250 trigger_publish_tx,
251 session_nonce_length: config.session_nonce_length,
252 decoding_options,
253 close_tx,
254 });
255
256 (
257 session.clone(),
258 SessionEventLoop::new(
259 session,
260 session_retry_policy,
261 trigger_publish_rx,
262 close_rx,
263 config.keep_alive_interval,
264 config.max_failed_keep_alive_count,
265 connector,
266 ),
267 )
268 }
269
270 pub(super) fn make_request_header(&self) -> RequestHeader {
272 self.channel.make_request_header(self.request_timeout)
273 }
274
275 pub(crate) fn reset(&self) {
278 self.session_id.store(Arc::new(NodeId::null()));
279 self.internal_session_id.store(
280 NEXT_SESSION_ID.fetch_add(1, Ordering::Relaxed),
281 Ordering::Relaxed,
282 );
283 }
284
285 async fn wait_for_state(&self, connected: bool) -> bool {
287 let mut rx = self.state_watch_rx.clone();
288
289 let res = rx
290 .wait_for(|s| {
291 connected && matches!(*s, SessionState::Connected)
292 || !connected && matches!(*s, SessionState::Disconnected)
293 })
294 .await
295 .is_ok();
296
297 res
299 }
300
301 pub fn session_id(&self) -> u32 {
303 self.internal_session_id.load(Ordering::Relaxed)
304 }
305
306 pub fn server_session_id(&self) -> NodeId {
309 (**(*self.session_id).load()).clone()
310 }
311
312 pub async fn wait_for_connection(&self) -> bool {
316 self.wait_for_state(true).await
317 }
318
319 pub fn close_on_drop(self: &Arc<Self>) -> SessionDropGuard {
322 SessionDropGuard {
323 session: self.clone(),
324 }
325 }
326
327 pub fn disable_reconnects(&self) {
331 self.should_reconnect.store(false, Ordering::Relaxed);
332 }
333
334 pub fn enable_reconnects(&self) {
337 self.should_reconnect.store(true, Ordering::Relaxed);
338 }
339
340 pub async fn disconnect_inner(
344 &self,
345 delete_subscriptions: bool,
346 disable_reconnect: bool,
347 ) -> Result<(), Error> {
348 if disable_reconnect {
349 self.should_reconnect.store(false, Ordering::Relaxed);
350 }
351 let mut res = Ok(());
352 if let Err(e) = self.close_session(delete_subscriptions).await {
353 session_warn!(
354 self,
355 "Failed to close session, channel will be closed anyway: {e}"
356 );
357 res = Err(e);
358 }
359 self.channel.close_channel().await;
360
361 self.wait_for_state(false).await;
362
363 res
364 }
365
366 pub async fn disconnect(&self) -> Result<(), Error> {
371 self.disconnect_inner(true, true).await
372 }
373
374 pub async fn disconnect_without_delete_subscriptions(&self) -> Result<(), Error> {
376 self.disconnect_inner(false, true).await
377 }
378
379 pub fn decoding_options(&self) -> &DecodingOptions {
381 &self.decoding_options
382 }
383
384 pub fn channel(&self) -> &AsyncSecureChannel {
386 &self.channel
387 }
388
389 pub fn request_handle(&self) -> IntegerId {
391 self.channel.request_handle()
392 }
393
394 pub fn encoding_context(&self) -> &RwLock<ContextOwned> {
396 self.channel.encoding_context()
397 }
398
399 pub fn endpoint_info(&self) -> &EndpointInfo {
401 self.channel.endpoint_info()
402 }
403
404 pub fn set_namespaces(&self, namespaces: NamespaceMap) {
408 *self.encoding_context().write().namespaces_mut() = namespaces;
409 }
410
411 pub fn add_type_loader(&self, type_loader: Arc<dyn TypeLoader>) {
416 self.encoding_context()
417 .write()
418 .loaders_mut()
419 .add(type_loader);
420 }
421
422 pub fn context(&self) -> Arc<RwLock<ContextOwned>> {
424 self.channel.secure_channel.read().context_arc()
425 }
426
427 pub fn browser(&self) -> Browser<'_, (), DefaultRetryPolicy<'_>> {
432 Browser::new(
433 self,
434 (),
435 DefaultRetryPolicy::new(ExponentialBackoff::new(
436 Duration::from_secs(30),
437 Some(5),
438 Duration::from_millis(500),
439 )),
440 )
441 }
442
443 pub async fn read_namespace_array(&self) -> Result<NamespaceMap, Error> {
445 let nodeid: NodeId = VariableId::Server_NamespaceArray.into();
446 let result = self
447 .read(
448 &[ReadValueId::from(nodeid)],
449 TimestampsToReturn::Neither,
450 0.0,
451 )
452 .await?;
453 if let Some(Variant::Array(array)) = &result[0].value {
454 let map = NamespaceMap::new_from_variant_array(&array.values)
455 .map_err(|e| Error::new(StatusCode::Bad, e))?;
456 let map_clone = map.clone();
457 self.set_namespaces(map);
458 Ok(map_clone)
459 } else {
460 Err(Error::new(
461 StatusCode::BadNoValue,
462 format!("Server namespace array is None. The server has an issue {result:?}"),
463 ))
464 }
465 }
466
467 pub fn get_namespace_index_from_cache(&self, url: &str) -> Option<u16> {
469 self.encoding_context().read().namespaces().get_index(url)
470 }
471
472 pub async fn get_namespace_index(&self, url: &str) -> Result<u16, Error> {
475 if let Some(idx) = self.get_namespace_index_from_cache(url) {
476 return Ok(idx);
477 };
478 let map = self.read_namespace_array().await?;
479 let idx = map.get_index(url).ok_or_else(|| {
480 Error::new(
481 StatusCode::BadNoMatch,
482 format!(
483 "Url {} not found in namespace array. Namspace array is {:?}",
484 url, map
485 ),
486 )
487 })?;
488 Ok(idx)
489 }
490}
491
492#[must_use = "SessionDropGuard disconnects on drop; assign it to a variable to control lifetime"]
497pub struct SessionDropGuard {
498 session: Arc<Session>,
499}
500
501impl SessionDropGuard {
502 pub fn arc(&self) -> Arc<Session> {
504 self.session.clone()
505 }
506}
507
508impl std::ops::Deref for SessionDropGuard {
509 type Target = Session;
510 fn deref(&self) -> &Session {
511 &self.session
512 }
513}
514
515impl Drop for SessionDropGuard {
516 fn drop(&mut self) {
517 let _ = self.session.close_tx.send(true);
518 }
519}