Skip to main content

opcua_client/session/
mod.rs

1mod client;
2mod connect;
3mod connection;
4mod event_loop;
5mod request_builder;
6mod retry;
7mod services;
8
9/// Information about the server endpoint, security policy, security mode and user identity that the session will
10/// will use to establish a connection.
11#[derive(Debug, Clone)]
12pub struct EndpointInfo {
13    /// The endpoint
14    pub endpoint: EndpointDescription,
15    /// User identity token
16    pub user_identity_token: IdentityToken,
17    /// Preferred language locales
18    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
125/// Process the service result, i.e. where the request "succeeded" but the response
126/// contains a failure status code.
127pub(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
179/// An OPC-UA session. This session provides methods for all supported services that require an open session.
180///
181/// Note that not all servers may support all service requests and calling an unsupported API
182/// may cause the connection to be dropped. Your client is expected to know the capabilities of
183/// the server it is calling to avoid this.
184///
185pub 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    /// Reference to the subscription cache for the client.
200    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    /// Create a request header with the default timeout.
271    pub(super) fn make_request_header(&self) -> RequestHeader {
272        self.channel.make_request_header(self.request_timeout)
273    }
274
275    /// Reset the session after a hard disconnect, clearing the session ID and incrementing the internal
276    /// session counter.
277    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    /// Wait for the session to be in either a connected or disconnected state.
286    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        // Compiler limitation
298        res
299    }
300
301    /// The internal ID of the session, used to keep track of multiple sessions in the same program.
302    pub fn session_id(&self) -> u32 {
303        self.internal_session_id.load(Ordering::Relaxed)
304    }
305
306    /// Get the current session ID. This is different from `session_id`, which is the client-side ID
307    /// to keep track of multiple sessions. This is the session ID the server uses to identify this session.
308    pub fn server_session_id(&self) -> NodeId {
309        (**(*self.session_id).load()).clone()
310    }
311
312    /// Convenience method to wait for a connection to the server.
313    ///
314    /// You should also monitor the session event loop. If it ends, this method will never return.
315    pub async fn wait_for_connection(&self) -> bool {
316        self.wait_for_state(true).await
317    }
318
319    /// Returns a [`SessionDropGuard`] that will initiate a graceful disconnect
320    /// when dropped. Useful for RAII-style session lifetimes.
321    pub fn close_on_drop(self: &Arc<Self>) -> SessionDropGuard {
322        SessionDropGuard {
323            session: self.clone(),
324        }
325    }
326
327    /// Disable automatic reconnects.
328    /// This will make the event loop quit the next time
329    /// it disconnects for whatever reason.
330    pub fn disable_reconnects(&self) {
331        self.should_reconnect.store(false, Ordering::Relaxed);
332    }
333
334    /// Enable automatic reconnects.
335    /// Automatically reconnecting is enabled by default.
336    pub fn enable_reconnects(&self) {
337        self.should_reconnect.store(true, Ordering::Relaxed);
338    }
339
340    /// Inner method for disconnect. [`Session::disconnect`] and [`Session::disconnect_without_delete_subscriptions`]
341    /// are shortands for this with `delete_subscriptions` set to `false` and `true` respectively, and
342    /// `disable_reconnect` set to `true`.
343    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    /// Disconnect from the server and wait until disconnected.
367    /// This will set the `should_reconnect` flag to false on the session, indicating
368    /// that it should not attempt to reconnect to the server. You may clear this flag
369    /// yourself to
370    pub async fn disconnect(&self) -> Result<(), Error> {
371        self.disconnect_inner(true, true).await
372    }
373
374    /// Disconnect the server without deleting subscriptions, then wait until disconnected.
375    pub async fn disconnect_without_delete_subscriptions(&self) -> Result<(), Error> {
376        self.disconnect_inner(false, true).await
377    }
378
379    /// Get the decoding options used by the session.
380    pub fn decoding_options(&self) -> &DecodingOptions {
381        &self.decoding_options
382    }
383
384    /// Get a reference to the inner secure channel.
385    pub fn channel(&self) -> &AsyncSecureChannel {
386        &self.channel
387    }
388
389    /// Get the next request handle.
390    pub fn request_handle(&self) -> IntegerId {
391        self.channel.request_handle()
392    }
393
394    /// Get a reference to the global encoding context.
395    pub fn encoding_context(&self) -> &RwLock<ContextOwned> {
396        self.channel.encoding_context()
397    }
398
399    /// Get the target endpoint for the session.
400    pub fn endpoint_info(&self) -> &EndpointInfo {
401        self.channel.endpoint_info()
402    }
403
404    /// Set the namespace array on the session.
405    /// Make sure that this namespace array contains the base namespace,
406    /// or the session may behave unexpectedly.
407    pub fn set_namespaces(&self, namespaces: NamespaceMap) {
408        *self.encoding_context().write().namespaces_mut() = namespaces;
409    }
410
411    /// Add a type loader to the encoding context.
412    /// Note that there is no mechanism to ensure uniqueness,
413    /// you should avoid adding the same type loader more than once, it will
414    /// work, but there will be a small performance overhead.
415    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    /// Get a reference to the encoding
423    pub fn context(&self) -> Arc<RwLock<ContextOwned>> {
424        self.channel.secure_channel.read().context_arc()
425    }
426
427    /// Create a browser, used to recursively browse the node hierarchy.
428    ///
429    /// You must call `handler` on the returned browser and set a browse policy
430    /// before it can be used. You can, for example, use [BrowseFilter](crate::browser::BrowseFilter)
431    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    /// Return namespace array from server and store in namespace cache
444    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    /// Return index of supplied namespace url from cache
468    pub fn get_namespace_index_from_cache(&self, url: &str) -> Option<u16> {
469        self.encoding_context().read().namespaces().get_index(url)
470    }
471
472    /// Return index of supplied namespace url
473    /// by first looking at namespace cache and querying server if necessary
474    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/// RAII guard that initiates a graceful disconnect when dropped.
493///
494/// Obtained via [`Session::close_on_drop`]. Implements [`std::ops::Deref`] to `Session`
495/// so all session methods are accessible directly.
496#[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    /// Returns a clone of the underlying `Arc<Session>`.
503    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}