/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; namespace Opc.Ua.Client { /// /// Extensions to ISession that are not dependent on anything internal /// to the Session but layer over ISession /// public static class SessionExtensions { /// /// Establishes a session with the server. /// /// session to use /// The name to assign to the session. /// The user identity. /// The cancellation token. public static Task OpenAsync( this ISession session, string sessionName, IUserIdentity identity, CancellationToken ct = default) { return session.OpenAsync(sessionName, 0, identity, default, ct); } /// /// Establishes a session with the server. /// /// session to use /// The name to assign to the session. /// The session timeout. /// The user identity. /// The list of preferred locales. /// The cancellation token. public static Task OpenAsync( this ISession session, string sessionName, uint sessionTimeout, IUserIdentity identity, ArrayOf preferredLocales, CancellationToken ct = default) { return session.OpenAsync( sessionName, sessionTimeout, identity, preferredLocales, true, ct); } /// /// Establishes a session with the server. /// /// session to use /// The name to assign to the session. /// The session timeout. /// The user identity. /// The list of preferred locales. /// If set to true then the /// domain in the certificate must match the endpoint used. /// The cancellation token. public static Task OpenAsync( this ISession session, string sessionName, uint sessionTimeout, IUserIdentity identity, ArrayOf preferredLocales, bool checkDomain, CancellationToken ct = default) { return session.OpenAsync( sessionName, sessionTimeout, identity, preferredLocales, checkDomain, true, ct); } /// /// Reconnects to the server after a network failure. /// Uses the current channel if possible or creates /// a new one. /// public static Task ReconnectAsync( this ISession session, CancellationToken ct = default) { return session.ReconnectAsync(null, null, ct); } /// /// Reconnects to the server on a waiting connection. /// public static Task ReconnectAsync( this ISession session, ITransportWaitingConnection connection, CancellationToken ct = default) { return session.ReconnectAsync(connection, null, ct); } /// /// Reconnects to the server after a network failure /// using a new channel. /// public static Task ReconnectAsync( this ISession session, ITransportChannel channel, CancellationToken ct = default) { return session.ReconnectAsync(null, channel, ct); } /// /// Saves all the subscriptions of the session. /// /// session to use /// The file path. /// Known types public static void Save( this ISession session, string filePath, IEnumerable? knownTypes = null) { session.Save(filePath, session.Subscriptions, knownTypes); } /// /// Load the list of subscriptions saved in a file. /// /// session to use /// The file path. /// Load the subscriptions for transfer /// after load. /// Additional known types that may be needed to /// read the saved subscriptions. /// The list of loaded subscriptions public static IEnumerable Load( this ISession session, string filePath, bool transferSubscriptions = false, IEnumerable? knownTypes = null) { using FileStream stream = File.OpenRead(filePath); return session.Load(stream, transferSubscriptions, knownTypes); } /// /// Saves a set of subscriptions to a file. /// public static void Save( this ISession session, string filePath, IEnumerable subscriptions, IEnumerable? knownTypes = null) { using var stream = new FileStream(filePath, FileMode.Create); session.Save(stream, subscriptions, knownTypes); } /// /// Close the session with the server and optionally closes the channel. /// public static Task CloseAsync( this ISession session, bool closeChannel, CancellationToken ct = default) { return session.CloseAsync(session.KeepAliveInterval, closeChannel, ct); } /// /// Disconnects from the server and frees any network resources (closes /// the channel) with the specified timeout. /// public static Task CloseAsync( this ISession session, int timeout, CancellationToken ct = default) { return session.CloseAsync(timeout, true, ct); } /// /// Reads a byte string which is too large for the (server side) encoder to handle. /// /// session to use /// The node id of a byte string variable /// Cancellation token to cancel operation with /// public static async Task ReadByteStringInChunksAsync( this ISession session, NodeId nodeId, CancellationToken ct = default) { int maxByteStringLength = (int)session.ServerCapabilities.MaxByteStringLength; if (maxByteStringLength <= 1) { throw ServiceResultException.Create( StatusCodes.BadIndexRangeNoData, "The MaxByteStringLength is not known or too small for reading data in chunks."); } return await session.ReadBytesAsync( nodeId, maxByteStringLength, ct).ConfigureAwait(false); } /// /// Finds the NodeIds for the components for an instance. /// public static async Task<(ArrayOf, ArrayOf)> FindComponentIdsAsync( this ISession session, NodeId instanceId, IList componentPaths, CancellationToken ct = default) { var componentIds = new List(); var errors = new List(); // build list of paths to translate. var pathsToTranslateList = new List(); for (int ii = 0; ii < componentPaths.Count; ii++) { var pathToTranslate = new BrowsePath { StartingNode = instanceId, RelativePath = RelativePath.Parse(componentPaths[ii], session.TypeTree) }; pathsToTranslateList.Add(pathToTranslate); } var pathsToTranslate = pathsToTranslateList.ToArrayOf(); // translate the paths. TranslateBrowsePathsToNodeIdsResponse response = await session.TranslateBrowsePathsToNodeIdsAsync( null, pathsToTranslate, ct).ConfigureAwait(false); ArrayOf results = response.Results; ArrayOf diagnosticInfos = response.DiagnosticInfos; ResponseHeader responseHeader = response.ResponseHeader; // verify that the server returned the correct number of results. ClientBase.ValidateResponse(results, pathsToTranslate); ClientBase.ValidateDiagnosticInfos(diagnosticInfos, pathsToTranslate); for (int ii = 0; ii < componentPaths.Count; ii++) { componentIds.Add(NodeId.Null); errors.Add(ServiceResult.Good); // process any diagnostics associated with any error. if (StatusCode.IsBad(results[ii].StatusCode)) { errors[ii] = new ServiceResult( results[ii].StatusCode, ii, diagnosticInfos, responseHeader.StringTable); continue; } // Expecting exact one NodeId for a local node. // Report an error if the server returns anything other than that. if (results[ii].Targets.Count == 0) { errors[ii] = ServiceResult.Create( StatusCodes.BadTargetNodeIdInvalid, "Could not find target for path: {0}.", componentPaths[ii]); continue; } if (results[ii].Targets.Count != 1) { errors[ii] = ServiceResult.Create( StatusCodes.BadTooManyMatches, "Too many matches found for path: {0}.", componentPaths[ii]); continue; } if (results[ii].Targets[0].RemainingPathIndex != uint.MaxValue) { errors[ii] = ServiceResult.Create( StatusCodes.BadTargetNodeIdInvalid, "Cannot follow path to external server: {0}.", componentPaths[ii]); continue; } if (results[ii].Targets[0].TargetId.IsNull) { errors[ii] = ServiceResult.Create( StatusCodes.BadUnexpectedError, "Server returned a null NodeId for path: {0}.", componentPaths[ii]); continue; } if (results[ii].Targets[0].TargetId.IsAbsolute) { errors[ii] = ServiceResult.Create( StatusCodes.BadUnexpectedError, "Server returned a remote node for path: {0}.", componentPaths[ii]); continue; } // suitable target found. componentIds[ii] = ExpandedNodeId.ToNodeId( results[ii].Targets[0].TargetId, session.NamespaceUris); } return (componentIds, errors); } /// /// Returns the available encodings for a node /// /// The session to use /// The variable node. /// Cancellation token to use to cancel the operation /// public static async Task> ReadAvailableEncodingsAsync( this ISession session, NodeId variableId, CancellationToken ct = default) { if (await session.NodeCache.FindAsync(variableId, ct).ConfigureAwait(false) is not VariableNode variable) { throw ServiceResultException.Create( StatusCodes.BadNodeIdInvalid, "NodeId does not refer to a valid variable node."); } // no encodings available if there was a problem reading the // data type for the node. if (variable.DataType.IsNull) { return []; } // no encodings for non-structures. if (!await session.NodeCache.IsTypeOfAsync( variable.DataType, DataTypeIds.Structure, ct).ConfigureAwait(false)) { return []; } // look for cached values. ArrayOf encodings = await session.NodeCache.FindAsync( variableId, ReferenceTypeIds.HasEncoding, false, true, ct).ConfigureAwait(false); if (encodings.Count > 0) { var references = new List(); foreach (INode encoding in encodings) { var reference = new ReferenceDescription { ReferenceTypeId = ReferenceTypeIds.HasEncoding, IsForward = true, NodeId = encoding.NodeId, NodeClass = encoding.NodeClass, BrowseName = encoding.BrowseName, DisplayName = encoding.DisplayName, TypeDefinition = encoding.TypeDefinitionId }; references.Add(reference); } return references; } var browser = new Browser(session, new BrowserOptions { BrowseDirection = BrowseDirection.Forward, ReferenceTypeId = ReferenceTypeIds.HasEncoding, IncludeSubtypes = false, NodeClassMask = 0 }); return await browser.BrowseAsync(variable.DataType, ct).ConfigureAwait(false); } } }