diff --git a/README.md b/README.md index a4ce51385a..fe68b801ad 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ -# UA-.NET-Windows-Universal-Platform -OPC Unified Architecture .NET for the Windows Universal Platform +# UA-Universal-Windows-Platform +OPC Unified Architecture for the Universal Windows Platform +Features included: +1. Fully ported core stack and SDK +2. Sample Client and Sample Server, including all required controls +3. X509 certificate support for client and server authentication +4. Anonymous user authentication +5. UA-TCP transport +6. Folder- and Windows-certificate-store support +7. Sessions (including UI support in the samples) +8. Subscriptions (including UI support in the samples) diff --git a/SampleApplications/SDK/Client/Browser.cs b/SampleApplications/SDK/Client/Browser.cs new file mode 100644 index 0000000000..8f5b3a9217 --- /dev/null +++ b/SampleApplications/SDK/Client/Browser.cs @@ -0,0 +1,457 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Text; +using System.ServiceModel; +using System.Runtime.Serialization; + +namespace Opc.Ua.Client +{ + /// + /// Stores the options to use for a browse operation. + /// + [DataContract(Namespace=Namespaces.OpcUaXsd)] + public class Browser + { + #region Constructors + /// + /// Creates an unattached instance of a browser. + /// + public Browser() + { + Initialize(); + } + + /// + /// Creates new instance of a browser and attaches it to a session. + /// + public Browser(Session session) + { + Initialize(); + m_session = session; + } + + /// + /// Creates a copy of a browser. + /// + public Browser(Browser template) + { + Initialize(); + + if (template != null) + { + m_session = template.m_session; + m_view = template.m_view; + m_maxReferencesReturned = template.m_maxReferencesReturned; + m_browseDirection = template.m_browseDirection; + m_referenceTypeId = template.m_referenceTypeId; + m_includeSubtypes = template.m_includeSubtypes; + m_nodeClassMask = template.m_nodeClassMask; + m_resultMask = template.m_resultMask; + m_continueUntilDone = template.m_continueUntilDone; + } + } + + /// + /// Sets all private fields to default values. + /// + private void Initialize() + { + m_session = null; + m_view = null; + m_maxReferencesReturned = 0; + m_browseDirection = Opc.Ua.BrowseDirection.Forward; + m_referenceTypeId = null; + m_includeSubtypes = true; + m_nodeClassMask = 0; + m_resultMask = (uint)BrowseResultMask.All; + m_continueUntilDone = false; + m_browseInProgress = false; + } + #endregion + + #region Public Properties + /// + /// The session that the browse is attached to. + /// + public Session Session + { + get { return m_session; } + + set + { + CheckBrowserState(); + m_session = value; + } + } + + /// + /// The view to use for the browse operation. + /// + [DataMember(Order = 1)] + public ViewDescription View + { + get { return m_view; } + + set + { + CheckBrowserState(); + m_view = value; + } + } + + /// + /// The maximum number of refrences to return in a single browse operation. + /// + [DataMember(Order = 2)] + public uint MaxReferencesReturned + { + get { return m_maxReferencesReturned; } + + set + { + CheckBrowserState(); + m_maxReferencesReturned = value; + } + } + + /// + /// The direction to browse. + /// + [DataMember(Order = 3)] + public BrowseDirection BrowseDirection + { + get { return m_browseDirection; } + + set + { + CheckBrowserState(); + m_browseDirection = value; + } + } + + /// + /// The reference type to follow. + /// + [DataMember(Order = 4)] + public NodeId ReferenceTypeId + { + get { return m_referenceTypeId; } + + set + { + CheckBrowserState(); + m_referenceTypeId = value; + } + } + + /// + /// Whether subtypes of the reference type should be included. + /// + [DataMember(Order = 5)] + public bool IncludeSubtypes + { + get { return m_includeSubtypes; } + + set + { + CheckBrowserState(); + m_includeSubtypes = value; + } + } + + /// + /// The classes of the target nodes. + /// + [DataMember(Order = 6)] + public int NodeClassMask + { + get { return Utils.ToInt32(m_nodeClassMask); } + + set + { + CheckBrowserState(); + m_nodeClassMask = Utils.ToUInt32(value); + } + } + + /// + /// The results to return. + /// + [DataMember(Order=6)] + public uint ResultMask + { + get { return m_resultMask; } + + set + { + CheckBrowserState(); + m_resultMask = value; + } + } + + /// + /// Raised when a browse operation halted because of a continuation point. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly")] + public event BrowserEventHandler MoreReferences + { + add { m_MoreReferences += value; } + remove { m_MoreReferences -= value; } + } + + /// + /// Whether subsequent continuation points should be processed automatically. + /// + public bool ContinueUntilDone + { + get { return m_continueUntilDone; } + + set + { + CheckBrowserState(); + m_continueUntilDone = value; + } + } + #endregion + + #region Public Methods + /// + /// Browses the specified node. + /// + public ReferenceDescriptionCollection Browse(NodeId nodeId) + { + if (m_session == null) + { + throw new ServiceResultException(StatusCodes.BadServerNotConnected, "Cannot browse if not connected to a server."); + } + + try + { + m_browseInProgress = true; + + // construct request. + BrowseDescription nodeToBrowse = new BrowseDescription(); + + nodeToBrowse.NodeId = nodeId; + nodeToBrowse.BrowseDirection = m_browseDirection; + nodeToBrowse.ReferenceTypeId = m_referenceTypeId; + nodeToBrowse.IncludeSubtypes = m_includeSubtypes; + nodeToBrowse.NodeClassMask = m_nodeClassMask; + nodeToBrowse.ResultMask = m_resultMask; + + BrowseDescriptionCollection nodesToBrowse = new BrowseDescriptionCollection(); + nodesToBrowse.Add(nodeToBrowse); + + // make the call to the server. + BrowseResultCollection results; + DiagnosticInfoCollection diagnosticInfos; + + ResponseHeader responseHeader = m_session.Browse( + null, + m_view, + m_maxReferencesReturned, + nodesToBrowse, + out results, + out diagnosticInfos); + + // ensure that the server returned valid results. + Session.ValidateResponse(results, nodesToBrowse); + Session.ValidateDiagnosticInfos(diagnosticInfos, nodesToBrowse); + + // check if valid. + if (StatusCode.IsBad(results[0].StatusCode)) + { + throw ServiceResultException.Create(results[0].StatusCode, 0, diagnosticInfos, responseHeader.StringTable); + } + + // fetch initial set of references. + byte[] continuationPoint = results[0].ContinuationPoint; + ReferenceDescriptionCollection references = results[0].References; + + // process any continuation point. + while (continuationPoint != null) + { + ReferenceDescriptionCollection additionalReferences; + + if (!m_continueUntilDone && m_MoreReferences != null) + { + BrowserEventArgs args = new BrowserEventArgs(references); + m_MoreReferences(this, args); + + // cancel browser and return the references fetched so far. + if (args.Cancel) + { + BrowseNext(ref continuationPoint, true); + return references; + } + + m_continueUntilDone = args.ContinueUntilDone; + } + + additionalReferences = BrowseNext(ref continuationPoint, false); + references.AddRange(additionalReferences); + } + + // return the results. + return references; + } + finally + { + m_browseInProgress = false; + } + } + #endregion + + #region Private Methods + /// + /// Checks the state of the browser. + /// + private void CheckBrowserState() + { + if (m_browseInProgress) + { + throw new ServiceResultException(StatusCodes.BadInvalidState, "Cannot change browse parameters while a browse operation is in progress."); + } + } + + /// + /// Fetches the next batch of references. + /// + /// The continuation point. + /// if set to true the browse operation is cancelled. + /// The next batch of references + private ReferenceDescriptionCollection BrowseNext(ref byte[] continuationPoint, bool cancel) + { + ByteStringCollection continuationPoints = new ByteStringCollection(); + continuationPoints.Add(continuationPoint); + + // make the call to the server. + BrowseResultCollection results; + DiagnosticInfoCollection diagnosticInfos; + + ResponseHeader responseHeader = m_session.BrowseNext( + null, + cancel, + continuationPoints, + out results, + out diagnosticInfos); + + // ensure that the server returned valid results. + Session.ValidateResponse(results, continuationPoints); + Session.ValidateDiagnosticInfos(diagnosticInfos, continuationPoints); + + // check if valid. + if (StatusCode.IsBad(results[0].StatusCode)) + { + throw ServiceResultException.Create(results[0].StatusCode, 0, diagnosticInfos, responseHeader.StringTable); + } + + // update continuation point. + continuationPoint = results[0].ContinuationPoint; + + // return references. + return results[0].References; + } + #endregion + + #region Private Fields + private Session m_session; + private ViewDescription m_view; + private uint m_maxReferencesReturned; + private BrowseDirection m_browseDirection; + private NodeId m_referenceTypeId; + private bool m_includeSubtypes; + private uint m_nodeClassMask; + private uint m_resultMask; + private event BrowserEventHandler m_MoreReferences; + private bool m_continueUntilDone; + private bool m_browseInProgress; + #endregion + } + + #region BrowserEventArgs Class + /// + /// The event arguments provided a browse operation returns a continuation point. + /// + public class BrowserEventArgs : EventArgs + { + #region Constructors + /// + /// Creates a new instance. + /// + internal BrowserEventArgs(ReferenceDescriptionCollection references) + { + m_references = references; + } + #endregion + + #region Public Properties + /// + /// Whether the browse operation should be cancelled. + /// + public bool Cancel + { + get { return m_cancel; } + set { m_cancel = value; } + } + + /// + /// Whether subsequent continuation points should be processed automatically. + /// + public bool ContinueUntilDone + { + get { return m_continueUntilDone; } + set { m_continueUntilDone = value; } + } + + /// + /// The references that have been fetched so far. + /// + public ReferenceDescriptionCollection References + { + get { return m_references; } + } + #endregion + + #region Private Fields + private bool m_cancel; + private bool m_continueUntilDone; + private ReferenceDescriptionCollection m_references; + #endregion + } + + /// + /// A delegate used to received browser events. + /// + public delegate void BrowserEventHandler(Browser sender, BrowserEventArgs e); + #endregion +} diff --git a/SampleApplications/SDK/Client/DataDictionary.cs b/SampleApplications/SDK/Client/DataDictionary.cs new file mode 100644 index 0000000000..d27b11f7d5 --- /dev/null +++ b/SampleApplications/SDK/Client/DataDictionary.cs @@ -0,0 +1,313 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Runtime.Serialization; +using System.IO; + +using Opc.Ua.Schema; +using System.Threading.Tasks; + +namespace Opc.Ua.Client +{ + /// + /// A class that holds the configuration for a UA service. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix"), DataContract(Namespace = Namespaces.OpcUaXsd)] + public class DataDictionary : ApplicationConfiguration + { + #region Constructors + /// + /// The default constructor. + /// + public DataDictionary(Session session) + { + Initialize(); + + m_session = session; + } + + /// + /// Sets private members to default values. + /// + private void Initialize() + { + m_session = null; + m_datatypes = new Dictionary(); + m_validator = null; + m_typeSystemId = null; + m_typeSystemName = null; + m_dictionaryId = null; + m_name = null; + } + #endregion + + #region Public Interface + /// + /// The node id for the dictionary. + /// + public NodeId DictionaryId + { + get + { + return m_dictionaryId; + } + } + + /// + /// The display name for the dictionary. + /// + public string Name + { + get + { + return m_name; + } + } + + /// + /// The node id for the type system. + /// + public NodeId TypeSystemId + { + get + { + return m_typeSystemId; + } + } + + /// + /// The display name for the type system. + /// + public string TypeSystemName + { + get + { + return m_typeSystemName; + } + } + + /// + /// Loads the dictionary idetified by the node id. + /// + public async Task Load(ReferenceDescription dictionary) + { + if (dictionary == null) throw new ArgumentNullException("dictionary"); + + NodeId dictionaryId = ExpandedNodeId.ToNodeId(dictionary.NodeId, m_session.NamespaceUris); + + GetTypeSystem(dictionaryId); + + byte[] schema = ReadDictionary(dictionaryId); + + if (schema == null || schema.Length == 0) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Cannot parse empty data dictionary."); + } + + await Validate(schema); + + ReadDataTypes(dictionaryId); + + m_dictionaryId = dictionaryId; + m_name = dictionary.ToString(); + } + + /// + /// Returns true if the dictionary contains the data type description; + /// + public bool Contains(NodeId descriptionId) + { + return m_datatypes.ContainsKey(descriptionId); + } + + /// + /// Returns the schema for the specified type (returns the entire dictionary if null). + /// + public string GetSchema(NodeId descriptionId) + { + ReferenceDescription description = null; + + if (descriptionId != null) + { + if (!m_datatypes.TryGetValue(descriptionId, out description)) + { + return null; + } + + return m_validator.GetSchema(description.BrowseName.Name); + } + + return m_validator.GetSchema(null); + } + #endregion + + #region Private Members + /// + /// Retrieves the type system for the dictionary. + /// + private void GetTypeSystem(NodeId dictionaryId) + { + Browser browser = new Browser(m_session); + + browser.BrowseDirection = BrowseDirection.Inverse; + browser.ReferenceTypeId = ReferenceTypeIds.HasComponent; + browser.IncludeSubtypes = false; + browser.NodeClassMask = 0; + + ReferenceDescriptionCollection references = browser.Browse(dictionaryId); + + if (references.Count > 0) + { + m_typeSystemId = ExpandedNodeId.ToNodeId(references[0].NodeId, m_session.NamespaceUris); + m_typeSystemName = references[0].ToString(); + } + } + + /// + /// Retrieves the data types in the dictionary. + /// + private void ReadDataTypes(NodeId dictionaryId) + { + Browser browser = new Browser(m_session); + + browser.BrowseDirection = BrowseDirection.Forward; + browser.ReferenceTypeId = ReferenceTypeIds.HasComponent; + browser.IncludeSubtypes = false; + browser.NodeClassMask = 0; + + ReferenceDescriptionCollection references = browser.Browse(dictionaryId); + + foreach (ReferenceDescription reference in references) + { + NodeId datatypeId = ExpandedNodeId.ToNodeId(reference.NodeId, m_session.NamespaceUris); + + if (datatypeId != null) + { + m_datatypes[datatypeId] = reference; + } + } + } + + /// + /// Reads the contents of a data dictionary. + /// + private byte[] ReadDictionary(NodeId dictionaryId) + { + // create item to read. + ReadValueId itemToRead = new ReadValueId(); + + itemToRead.NodeId = dictionaryId; + itemToRead.AttributeId = Attributes.Value; + itemToRead.IndexRange = null; + itemToRead.DataEncoding = null; + + ReadValueIdCollection itemsToRead = new ReadValueIdCollection(); + itemsToRead.Add(itemToRead); + + // read value. + DataValueCollection values; + DiagnosticInfoCollection diagnosticInfos; + + ResponseHeader responseHeader = m_session.Read( + null, + 0, + TimestampsToReturn.Neither, + itemsToRead, + out values, + out diagnosticInfos); + + ClientBase.ValidateResponse(values, itemsToRead); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToRead); + + // check for error. + if (StatusCode.IsBad(values[0].StatusCode)) + { + ServiceResult result = ClientBase.GetResult(values[0].StatusCode, 0, diagnosticInfos, responseHeader); + throw new ServiceResultException(result); + } + + // return as a byte array. + return values[0].Value as byte[]; + } + + /// + /// Validates the type dictionary. + /// + /// + private async Task Validate(byte[] dictionary) + { + MemoryStream istrm = new MemoryStream(dictionary); + + if (m_typeSystemId == Objects.XmlSchema_TypeSystem) + { + Schema.Xml.XmlSchemaValidator validator = new Schema.Xml.XmlSchemaValidator(); + + try + { + validator.Validate(istrm); + } + catch (Exception e) + { + Utils.Trace(e, "Could not validate schema."); + } + + m_validator = validator; + } + + if (m_typeSystemId == Objects.OPCBinarySchema_TypeSystem) + { + Schema.Binary.BinarySchemaValidator validator = new Schema.Binary.BinarySchemaValidator(); + + try + { + await validator.Validate(istrm); + } + catch (Exception e) + { + Utils.Trace(e, "Could not validate schema."); + } + + m_validator = validator; + } + } + #endregion + + #region Private Members + private Session m_session; + private NodeId m_dictionaryId; + private string m_name; + private NodeId m_typeSystemId; + private string m_typeSystemName; + private Dictionary m_datatypes; + private SchemaValidator m_validator; + #endregion + } +} diff --git a/SampleApplications/SDK/Client/Documentation/Opc.Ua.Client.cs b/SampleApplications/SDK/Client/Documentation/Opc.Ua.Client.cs new file mode 100644 index 0000000000..9742adc4d5 --- /dev/null +++ b/SampleApplications/SDK/Client/Documentation/Opc.Ua.Client.cs @@ -0,0 +1,43 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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/ + * ======================================================================*/ + +namespace Opc.Ua.Client +{ + /// + /// The Opc.Ua.Client namespace defines classes which can be used to implement a UA client. + /// These classes manage client side state information, provide higher level abstractions for UA + /// tasks such as managing sessions/subscriptions and saving/restoring connection information for + /// later use. + /// + /// + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class NamespaceDoc + { + } +} diff --git a/SampleApplications/SDK/Client/GlobalSuppressions.cs b/SampleApplications/SDK/Client/GlobalSuppressions.cs new file mode 100644 index 0000000000..55d1424751 --- /dev/null +++ b/SampleApplications/SDK/Client/GlobalSuppressions.cs @@ -0,0 +1,30 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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/ + * ======================================================================*/ + + diff --git a/SampleApplications/SDK/Client/MonitoredItem.cs b/SampleApplications/SDK/Client/MonitoredItem.cs new file mode 100644 index 0000000000..8394c1113a --- /dev/null +++ b/SampleApplications/SDK/Client/MonitoredItem.cs @@ -0,0 +1,1283 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Text; +using System.Xml; +using System.Runtime.Serialization; +using System.Reflection; + +namespace Opc.Ua.Client +{ + /// + /// A monitored item. + /// + [DataContract(Namespace = Namespaces.OpcUaXsd)] + [KnownType(typeof(DataChangeFilter))] + [KnownType(typeof(EventFilter))] + [KnownType(typeof(AggregateFilter))] + public class MonitoredItem + { + #region Constructors + /// + /// Initializes a new instance of the class. + /// + public MonitoredItem() + { + Initialize(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The client handle. The caller must ensure it uniquely identifies the monitored item. + public MonitoredItem(uint clientHandle) + { + Initialize(); + m_clientHandle = clientHandle; + } + + /// + /// Initializes a new instance of the class. + /// + /// The template used to specify the monitoring parameters. + public MonitoredItem(MonitoredItem template) : this(template, false) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The template used to specify the monitoring parameters. + /// if set to true the event handlers are copied. + public MonitoredItem(MonitoredItem template, bool copyEventHandlers) + { + Initialize(); + + if (template != null) + { + string displayName = template.DisplayName; + + if (displayName != null) + { + // remove any existing numeric suffix. + int index = displayName.LastIndexOf(' '); + + if (index != -1) + { + try + { + displayName = displayName.Substring(0, index); + } + catch + { + // not a numeric suffix. + } + } + } + + m_handle = template.m_handle; + m_displayName = Utils.Format("{0} {1}", displayName, m_clientHandle); + m_startNodeId = template.m_startNodeId; + m_relativePath = template.m_relativePath; + m_attributeId = template.m_attributeId; + m_indexRange = template.m_indexRange; + m_encoding = template.m_encoding; + m_monitoringMode = template.m_monitoringMode; + m_samplingInterval = template.m_samplingInterval; + m_filter = (MonitoringFilter)Utils.Clone(template.m_filter); + m_queueSize = template.m_queueSize; + m_discardOldest = template.m_discardOldest; + m_attributesModified = true; + + if (copyEventHandlers) + { + m_Notification = template.m_Notification; + } + + // this ensures the state is consistent with the node class. + NodeClass = template.m_nodeClass; + } + } + + /// + /// Called by the .NET framework during deserialization. + /// + [OnDeserializing] + private void Initialize(StreamingContext context) + { + // object initializers are not called during deserialization. + m_cache = new object(); + + Initialize(); + } + + /// + /// Sets the private members to default values. + /// + private void Initialize() + { + m_startNodeId = null; + m_relativePath = null; + m_clientHandle = 0; + m_attributeId = Attributes.Value; + m_indexRange = null; + m_encoding = null; + m_monitoringMode = MonitoringMode.Reporting; + m_samplingInterval = -1; + m_filter = null; + m_queueSize = 0; + m_discardOldest = true; + m_attributesModified = true; + m_status = new MonitoredItemStatus(); + + // this ensures the state is consistent with the node class. + NodeClass = NodeClass.Variable; + + // assign a unique handle. + m_clientHandle = Utils.IncrementIdentifier(ref s_GlobalClientHandle); + } + #endregion + + #region Persistent Properties + /// + /// A display name for the monitored item. + /// + [DataMember(Order = 1)] + public string DisplayName + { + get { return m_displayName; } + set { m_displayName = value; } + } + + + /// + /// The start node for the browse path that identifies the node to monitor. + /// + [DataMember(Order = 2)] + public NodeId StartNodeId + { + get { return m_startNodeId; } + set { m_startNodeId = value; } + } + + /// + /// The relative path from the browse path to the node to monitor. + /// + /// + /// A null or empty string specifies that the start node id should be monitored. + /// + [DataMember(Order = 3)] + public string RelativePath + { + get { return m_relativePath; } + + set + { + // clear resolved path if relative path has changed. + if (m_relativePath != value) + { + m_resolvedNodeId = null; + } + + m_relativePath = value; + } + } + + /// + /// The node class of the node being monitored (affects the type of filter available). + /// + [DataMember(Order = 4)] + public NodeClass NodeClass + { + get { return m_nodeClass; } + + set + { + if (m_nodeClass != value) + { + if ((value & (NodeClass.Object | NodeClass.View)) != 0) + { + // ensure a valid event filter. + if (!(m_filter is EventFilter)) + { + UseDefaultEventFilter(); + } + + // set the queue size to the default for events. + if (QueueSize <= 1) + { + QueueSize = Int32.MaxValue; + } + + m_eventCache = new MonitoredItemEventCache(100); + m_attributeId = Attributes.EventNotifier; + } + else + { + // clear the filter if it is only valid for events. + if (m_filter is EventFilter) + { + m_filter = null; + } + + // set the queue size to the default for data changes. + if (QueueSize == Int32.MaxValue) + { + QueueSize = 1; + } + + m_dataCache = new MonitoredItemDataCache(1); + } + } + + m_nodeClass = value; + } + } + + /// + /// The attribute to monitor. + /// + [DataMember(Order = 5)] + public uint AttributeId + { + get { return m_attributeId; } + set { m_attributeId = value; } + } + + /// + /// The range of array indexes to monitor. + /// + [DataMember(Order = 6)] + public string IndexRange + { + get { return m_indexRange; } + set { m_indexRange = value; } + } + + /// + /// The encoding to use when returning notifications. + /// + [DataMember(Order = 7)] + public QualifiedName Encoding + { + get { return m_encoding; } + set { m_encoding = value; } + } + + /// + /// The monitoring mode. + /// + [DataMember(Order = 8)] + public MonitoringMode MonitoringMode + { + get { return m_monitoringMode; } + set { m_monitoringMode = value; } + } + + /// + /// The sampling interval. + /// + [DataMember(Order = 9)] + public int SamplingInterval + { + get { return m_samplingInterval; } + + set + { + if (m_samplingInterval != value) + { + m_attributesModified = true; + } + + m_samplingInterval = value; + } + } + + /// + /// The filter to use to select values to return. + /// + [DataMember(Order = 10)] + public MonitoringFilter Filter + { + get { return m_filter; } + + set + { + // validate filter against node class. + ValidateFilter(m_nodeClass, value); + + m_attributesModified = true; + m_filter = value; + } + } + + /// + /// The length of the queue used to buffer values. + /// + [DataMember(Order = 11)] + public uint QueueSize + { + get { return m_queueSize; } + + set + { + if (m_queueSize != value) + { + m_attributesModified = true; + } + + m_queueSize = value; + } + } + + /// + /// Whether to discard the oldest entries in the queue when it is full. + /// + [DataMember(Order = 12)] + public bool DiscardOldest + { + get { return m_discardOldest; } + + set + { + if (m_discardOldest != value) + { + m_attributesModified = true; + } + + m_discardOldest = value; + } + } + #endregion + + #region Dynamic Properties + /// + /// The subscription that owns the monitored item. + /// + public Subscription Subscription + { + get { return m_subscription; } + internal set { m_subscription = value; } + } + + /// + /// A local handle assigned to the monitored item. + /// + public object Handle + { + get { return m_handle; } + set { m_handle = value; } + } + + /// + /// Whether the item has been created on the server. + /// + public bool Created + { + get { return m_status.Created; } + } + + /// + /// The identifier assigned by the client. + /// + public uint ClientHandle + { + get { return m_clientHandle; } + } + + /// + /// The node id to monitor after applying any relative path. + /// + public NodeId ResolvedNodeId + { + get + { + // just return the start id if relative path is empty. + if (String.IsNullOrEmpty(m_relativePath)) + { + return m_startNodeId; + } + + return m_resolvedNodeId; + } + + internal set { m_resolvedNodeId = value; } + } + + /// + /// Whether the monitoring attributes have been modified since the item was created. + /// + public bool AttributesModified + { + get { return m_attributesModified; } + } + + /// + /// The status associated with the monitored item. + /// + public MonitoredItemStatus Status + { + get { return m_status; } + } + #endregion + + #region Cache Related Functions + /// + /// Returns the queue size used by the cache. + /// + public int CacheQueueSize + { + get + { + lock (m_cache) + { + if (m_dataCache != null) + { + return m_dataCache.QueueSize; + } + + if (m_eventCache != null) + { + return m_eventCache.QueueSize; + } + + return 0; + } + } + + set + { + lock (m_cache) + { + if (m_dataCache != null) + { + m_dataCache.SetQueueSize(value); + } + + if (m_eventCache != null) + { + m_eventCache.SetQueueSize(value); + } + } + } + } + + /// + /// The last value or event recieved from the server. + /// + public IEncodeable LastValue + { + get + { + lock (m_cache) + { + return m_lastNotification; + } + } + } + + /// + /// Read all values in the cache queue. + /// + public IList DequeueValues() + { + lock (m_cache) + { + if (m_dataCache != null) + { + return m_dataCache.Publish(); + } + + return new List(); + } + } + + /// + /// Read all events in the cache queue. + /// + public IList DequeueEvents() + { + lock (m_cache) + { + if (m_eventCache != null) + { + return m_eventCache.Publish(); + } + + return new List(); + } + } + + /// + /// The last message containing a notification for the item. + /// + public NotificationMessage LastMessage + { + get + { + lock (m_cache) + { + if (m_dataCache != null) + { + return ((MonitoredItemNotification)m_lastNotification).Message; + } + + if (m_eventCache != null) + { + return ((EventFieldList)m_lastNotification).Message; + } + + return null; + } + } + } + + /// + /// Raised when a new notification arrives. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly")] + public event MonitoredItemNotificationEventHandler Notification + { + add + { + lock (m_cache) + { + m_Notification += value; + } + } + + remove + { + lock (m_cache) + { + m_Notification -= value; + } + } + } + + /// + /// Saves a data change or event in the cache. + /// + public void SaveValueInCache(IEncodeable newValue) + { + lock (m_cache) + { + m_lastNotification = newValue; + + if (m_dataCache != null) + { + MonitoredItemNotification datachange = newValue as MonitoredItemNotification; + + if (datachange != null) + { + Utils.Trace( + "SaveValueInCache: ServerHandle={0}, Value={1}, StatusCode={2}", + this.ClientHandle, + datachange.Value.WrappedValue, + datachange.Value.StatusCode); + + m_dataCache.OnNotification(datachange); + } + } + + if (m_eventCache != null) + { + EventFieldList eventchange = newValue as EventFieldList; + + if (m_eventCache != null) + { + m_eventCache.OnNotification(eventchange); + } + } + + if (m_Notification != null) + { + m_Notification(this, new MonitoredItemNotificationEventArgs(newValue)); + } + } + } + #endregion + + #region ICloneable Members + /// + /// Creates a deep copy of the object. + /// + public new object MemberwiseClone() + { + return new MonitoredItem(this); + } + #endregion + + #region Public Methods + /// + /// Sets the error status for the monitored item. + /// + public void SetError(ServiceResult error) + { + m_status.SetError(error); + } + + /// + /// Updates the object with the results of a translate browse path request. + /// + public void SetResolvePathResult( + BrowsePathResult result, + int index, + DiagnosticInfoCollection diagnosticInfos, + ResponseHeader responseHeader) + { + ServiceResult error = null; + + if (StatusCode.IsBad(result.StatusCode)) + { + error = ClientBase.GetResult(result.StatusCode, index, diagnosticInfos, responseHeader); + } + else + { + ResolvedNodeId = NodeId.Null; + + // update the node id. + if (result.Targets.Count > 0) + { + ResolvedNodeId = ExpandedNodeId.ToNodeId(result.Targets[0].TargetId, m_subscription.Session.NamespaceUris); + } + } + + m_status.SetResolvePathResult(result, error); + } + + /// + /// Updates the object with the results of a create monitored item request. + /// + public void SetCreateResult( + MonitoredItemCreateRequest request, + MonitoredItemCreateResult result, + int index, + DiagnosticInfoCollection diagnosticInfos, + ResponseHeader responseHeader) + { + ServiceResult error = null; + + if (StatusCode.IsBad(result.StatusCode)) + { + error = ClientBase.GetResult(result.StatusCode, index, diagnosticInfos, responseHeader); + } + + m_status.SetCreateResult(request, result, error); + m_attributesModified = false; + } + + /// + /// Updates the object with the results of a modify monitored item request. + /// + public void SetModifyResult( + MonitoredItemModifyRequest request, + MonitoredItemModifyResult result, + int index, + DiagnosticInfoCollection diagnosticInfos, + ResponseHeader responseHeader) + { + ServiceResult error = null; + + if (StatusCode.IsBad(result.StatusCode)) + { + error = ClientBase.GetResult(result.StatusCode, index, diagnosticInfos, responseHeader); + } + + m_status.SetModifyResult(request, result, error); + m_attributesModified = false; + } + + /// + /// Updates the object with the results of a modify monitored item request. + /// + public void SetDeleteResult( + StatusCode result, + int index, + DiagnosticInfoCollection diagnosticInfos, + ResponseHeader responseHeader) + { + ServiceResult error = null; + + if (StatusCode.IsBad(result)) + { + error = ClientBase.GetResult(result, index, diagnosticInfos, responseHeader); + } + + m_status.SetDeleteResult(error); + } + + /// + /// Returns the field name the specified SelectClause in the EventFilter. + /// + public string GetFieldName(int index) + { + EventFilter filter = m_filter as EventFilter; + + if (filter == null) + { + return null; + } + + if (index < 0 || index >= filter.SelectClauses.Count) + { + return null; + } + + return Utils.Format("{0}", SimpleAttributeOperand.Format(filter.SelectClauses[index].BrowsePath)); + } + + /// + /// Returns value of the field name containing the event type. + /// + public object GetFieldValue( + EventFieldList eventFields, + NodeId eventTypeId, + string browsePath, + uint attributeId) + { + QualifiedNameCollection browseNames = SimpleAttributeOperand.Parse(browsePath); + return GetFieldValue(eventFields, eventTypeId, browseNames, attributeId); + } + + /// + /// Returns value of the field name containing the event type. + /// + public object GetFieldValue( + EventFieldList eventFields, + NodeId eventTypeId, + QualifiedName browseName) + { + QualifiedNameCollection browsePath = new QualifiedNameCollection(); + browsePath.Add(browseName); + return GetFieldValue(eventFields, eventTypeId, browsePath, Attributes.Value); + } + + /// + /// Returns value of the field name containing the event type. + /// + public object GetFieldValue( + EventFieldList eventFields, + NodeId eventTypeId, + IList browsePath, + uint attributeId) + { + if (eventFields == null) + { + return null; + } + + EventFilter filter = m_filter as EventFilter; + + if (filter == null) + { + return null; + } + + for (int ii = 0; ii < filter.SelectClauses.Count; ii++) + { + if (ii >= eventFields.EventFields.Count) + { + return null; + } + + // check for match. + SimpleAttributeOperand clause = filter.SelectClauses[ii]; + + // attribute id + if (clause.AttributeId != attributeId) + { + continue; + } + + // match null browse path. + if (browsePath == null || browsePath.Count == 0) + { + if (clause.BrowsePath != null && clause.BrowsePath.Count > 0) + { + continue; + } + + // ignore event type id when matching null browse paths. + return eventFields.EventFields[ii].Value; + } + + // match browse path. + + // event type id. + if (clause.TypeDefinitionId != eventTypeId) + { + continue; + } + + // match element count. + if (clause.BrowsePath.Count != browsePath.Count) + { + continue; + } + + // check each element. + bool match = true; + + for (int jj = 0; jj < clause.BrowsePath.Count; jj++) + { + if (clause.BrowsePath[jj] != browsePath[jj]) + { + match = false; + break; + } + } + + // check of no match. + if (!match) + { + continue; + } + + // return value. + return eventFields.EventFields[ii].Value; + } + + // no event type in event field list. + return null; + } + + /// + /// Returns value of the field name containing the event type. + /// + public INode GetEventType(EventFieldList eventFields) + { + // get event type. + NodeId eventTypeId = GetFieldValue(eventFields, ObjectTypes.BaseEventType, Opc.Ua.BrowseNames.EventType) as NodeId; + + if (eventTypeId != null && m_subscription != null && m_subscription.Session != null) + { + return m_subscription.Session.NodeCache.Find(eventTypeId); + } + + // no event type in event field list. + return null; + } + + /// + /// Returns value of the field name containing the event type. + /// + public DateTime GetEventTime(EventFieldList eventFields) + { + // get event time. + DateTime? eventTime = GetFieldValue(eventFields, ObjectTypes.BaseEventType, Opc.Ua.BrowseNames.Time) as DateTime?; + + if (eventTime != null) + { + return eventTime.Value; + } + + // no event time in event field list. + return DateTime.MinValue; + } + + /// + /// The service result for a data change notification. + /// + public static ServiceResult GetServiceResult(IEncodeable notification) + { + MonitoredItemNotification datachange = notification as MonitoredItemNotification; + + if (datachange == null) + { + return null; + } + + NotificationMessage message = datachange.Message; + + if (message != null) + { + return null; + } + + return new ServiceResult(datachange.Value.StatusCode, datachange.DiagnosticInfo, message.StringTable); + } + + /// + /// The service result for a field in an notification (the field must contain a Status object). + /// + public static ServiceResult GetServiceResult(IEncodeable notification, int index) + { + EventFieldList eventFields = notification as EventFieldList; + + if (eventFields == null) + { + return null; + } + + NotificationMessage message = eventFields.Message; + + if (message != null) + { + return null; + } + + if (index < 0 || index >= eventFields.EventFields.Count) + { + return null; + } + + StatusResult status = ExtensionObject.ToEncodeable(eventFields.EventFields[index].Value as ExtensionObject) as StatusResult; + + if (status == null) + { + return null; + } + + return new ServiceResult(status.StatusCode, status.DiagnosticInfo, message.StringTable); + } + #endregion + + #region Private Methods + /// + /// Throws an exception if the flter cannot be used with the node class. + /// + private void ValidateFilter(NodeClass nodeClass, MonitoringFilter filter) + { + if (filter == null) + { + return; + } + + switch (nodeClass) + { + case NodeClass.Variable: + case NodeClass.VariableType: + { + if (!typeof(DataChangeFilter).IsInstanceOfType(filter)) + { + m_nodeClass = NodeClass.Variable; + } + + break; + } + + case NodeClass.Object: + case NodeClass.View: + { + if (!typeof(EventFilter).IsInstanceOfType(filter)) + { + m_nodeClass = NodeClass.Object; + } + + break; + } + + default: + { + throw ServiceResultException.Create(StatusCodes.BadFilterNotAllowed, "Filters may not be specified for nodes of class '{0}'.", nodeClass); + } + } + } + + /// + /// Sets the default event filter. + /// + private void UseDefaultEventFilter() + { + EventFilter filter = filter = new EventFilter(); + + filter.AddSelectClause(ObjectTypes.BaseEventType, Opc.Ua.BrowseNames.EventId); + filter.AddSelectClause(ObjectTypes.BaseEventType, Opc.Ua.BrowseNames.EventType); + filter.AddSelectClause(ObjectTypes.BaseEventType, Opc.Ua.BrowseNames.SourceNode); + filter.AddSelectClause(ObjectTypes.BaseEventType, Opc.Ua.BrowseNames.SourceName); + filter.AddSelectClause(ObjectTypes.BaseEventType, Opc.Ua.BrowseNames.Time); + filter.AddSelectClause(ObjectTypes.BaseEventType, Opc.Ua.BrowseNames.ReceiveTime); + filter.AddSelectClause(ObjectTypes.BaseEventType, Opc.Ua.BrowseNames.LocalTime); + filter.AddSelectClause(ObjectTypes.BaseEventType, Opc.Ua.BrowseNames.Message); + filter.AddSelectClause(ObjectTypes.BaseEventType, Opc.Ua.BrowseNames.Severity); + + m_filter = filter; + } + #endregion + + #region Private Fields + private Subscription m_subscription; + private object m_handle; + private string m_displayName; + private NodeId m_startNodeId; + private string m_relativePath; + private NodeId m_resolvedNodeId; + private NodeClass m_nodeClass; + private uint m_attributeId; + private string m_indexRange; + private QualifiedName m_encoding; + private MonitoringMode m_monitoringMode; + private int m_samplingInterval; + private MonitoringFilter m_filter; + private uint m_queueSize; + private bool m_discardOldest; + private uint m_clientHandle; + private MonitoredItemStatus m_status; + private bool m_attributesModified; + private static long s_GlobalClientHandle; + + private object m_cache = new object(); + private MonitoredItemDataCache m_dataCache; + private MonitoredItemEventCache m_eventCache; + private IEncodeable m_lastNotification; + private event MonitoredItemNotificationEventHandler m_Notification; + #endregion + } + + #region MonitoredItemEventArgs Class + /// + /// The event arguments provided when a new notification message arrives. + /// + public class MonitoredItemNotificationEventArgs : EventArgs + { + #region Constructors + /// + /// Creates a new instance. + /// + internal MonitoredItemNotificationEventArgs(IEncodeable notificationValue) + { + m_notificationValue = notificationValue; + } + #endregion + + #region Public Properties + /// + /// The new notification. + /// + public IEncodeable NotificationValue + { + get { return m_notificationValue; } + } + #endregion + + #region Private Fields + private IEncodeable m_notificationValue; + #endregion + } + + /// + /// The delegate used to receive monitored item value notifications. + /// + public delegate void MonitoredItemNotificationEventHandler(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e); + #endregion + + /// + /// An item in the cache + /// + public class MonitoredItemDataCache + { + #region Constructors + /// + /// Constructs a cache for a monitored item. + /// + public MonitoredItemDataCache(int queueSize) + { + m_queueSize = queueSize; + m_values = new Queue(); + } + #endregion + + #region Public Members + /// + /// The size of the queue to maintain. + /// + public int QueueSize + { + get { return m_queueSize; } + } + + /// + /// The last value received from the server. + /// + public DataValue LastValue + { + get { return m_lastValue; } + } + + /// + /// Returns all values in the queue. + /// + public IList Publish() + { + DataValue[] values = new DataValue[m_values.Count]; + + for (int ii = 0; ii < values.Length; ii++) + { + values[ii] = m_values.Dequeue(); + } + + return values; + } + + /// + /// Saves a notification in the cache. + /// + public void OnNotification(MonitoredItemNotification notification) + { + m_values.Enqueue(notification.Value); + m_lastValue = notification.Value; + + Utils.Trace( + "NotificationReceived: ClientHandle={0}, Value={1}", + notification.ClientHandle, + m_lastValue.Value); + + while (m_values.Count > m_queueSize) + { + m_values.Dequeue(); + } + } + + /// + /// Changes the queue size. + /// + public void SetQueueSize(int queueSize) + { + if (queueSize == m_queueSize) + { + return; + } + + if (queueSize < 1) + { + queueSize = 1; + } + + m_queueSize = queueSize; + + while (m_values.Count > m_queueSize) + { + m_values.Dequeue(); + } + } + #endregion + + #region Private Fields + private int m_queueSize; + private DataValue m_lastValue; + private Queue m_values; + #endregion + } + + /// + /// Saves the events received from the srever. + /// + public class MonitoredItemEventCache + { + #region Constructors + /// + /// Constructs a cache for a monitored item. + /// + public MonitoredItemEventCache(int queueSize) + { + m_queueSize = queueSize; + m_events = new Queue(); + } + #endregion + + #region Public Members + /// + /// The size of the queue to maintain. + /// + public int QueueSize + { + get { return m_queueSize; } + } + + /// + /// The last event received. + /// + public EventFieldList LastEvent + { + get { return m_lastEvent; } + } + + /// + /// Returns all events in the queue. + /// + public IList Publish() + { + EventFieldList[] events = new EventFieldList[m_events.Count]; + + for (int ii = 0; ii < events.Length; ii++) + { + events[ii] = m_events.Dequeue(); + } + + return events; + } + + /// + /// Saves a notification in the cache. + /// + public void OnNotification(EventFieldList notification) + { + m_events.Enqueue(notification); + m_lastEvent = notification; + + while (m_events.Count > m_queueSize) + { + m_events.Dequeue(); + } + } + + /// + /// Changes the queue size. + /// + public void SetQueueSize(int queueSize) + { + if (queueSize == m_queueSize) + { + return; + } + + if (queueSize < 1) + { + queueSize = 1; + } + + m_queueSize = queueSize; + + while (m_events.Count > m_queueSize) + { + m_events.Dequeue(); + } + } + #endregion + + #region Private Fields + private int m_queueSize; + private EventFieldList m_lastEvent; + private Queue m_events; + #endregion + } +} diff --git a/SampleApplications/SDK/Client/MonitoredItemStatus.cs b/SampleApplications/SDK/Client/MonitoredItemStatus.cs new file mode 100644 index 0000000000..607bb3dfa2 --- /dev/null +++ b/SampleApplications/SDK/Client/MonitoredItemStatus.cs @@ -0,0 +1,293 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Text; +using System.Xml; +using System.Runtime.Serialization; + +namespace Opc.Ua.Client +{ + /// + /// The current status of monitored item. + /// + public class MonitoredItemStatus + { + #region Constructors + /// + /// Creates a empty object. + /// + internal MonitoredItemStatus() + { + Initialize(); + } + + private void Initialize() + { + m_id = 0; + m_nodeId = null; + m_attributeId = Attributes.Value; + m_indexRange = null; + m_encoding = null; + m_monitoringMode = MonitoringMode.Disabled; + m_clientHandle = 0; + m_samplingInterval = 0; + m_filter = null; + m_queueSize = 0; + m_discardOldest = true; + } + #endregion + + #region Public Properties + /// + /// The identifier assigned by the server. + /// + public uint Id + { + get { return m_id; } + } + + /// + /// Whether the item has been created on the server. + /// + public bool Created + { + get { return m_id != 0; } + } + + /// + /// Any error condition associated with the monitored item. + /// + public ServiceResult Error + { + get { return m_error; } + } + + /// + /// The node id being monitored. + /// + public NodeId NodeId + { + get { return m_nodeId; } + } + + /// + /// The attribute being monitored. + /// + public uint AttributeId + { + get { return m_attributeId; } + } + + /// + /// The range of array indexes to being monitored. + /// + public string IndexRange + { + get { return m_indexRange; } + } + + /// + /// The encoding to use when returning notifications. + /// + public QualifiedName DataEncoding + { + get { return m_encoding; } + } + + /// + /// The monitoring mode. + /// + public MonitoringMode MonitoringMode + { + get { return m_monitoringMode; } + } + + /// + /// The identifier assigned by the client. + /// + public uint ClientHandle + { + get { return m_clientHandle; } + } + + /// + /// The sampling interval. + /// + public double SamplingInterval + { + get { return m_samplingInterval; } + } + + /// + /// The filter to use to select values to return. + /// + public MonitoringFilter Filter + { + get { return m_filter; } + } + + /// + /// The length of the queue used to buffer values. + /// + public uint QueueSize + { + get { return m_queueSize; } + } + + /// + /// Whether to discard the oldest entries in the queue when it is full. + /// + public bool DiscardOldest + { + get { return m_discardOldest; } + } + #endregion + + #region Public Methods + /// + /// Updates the monitoring mode. + /// + public void SetMonitoringMode(MonitoringMode monitoringMode) + { + m_monitoringMode = monitoringMode; + } + + /// + /// Updates the object with the results of a translate browse paths request. + /// + internal void SetResolvePathResult( + BrowsePathResult result, + ServiceResult error) + { + m_error = error; + } + + /// + /// Updates the object with the results of a create monitored item request. + /// + internal void SetCreateResult( + MonitoredItemCreateRequest request, + MonitoredItemCreateResult result, + ServiceResult error) + { + if (request == null) throw new ArgumentNullException("request"); + if (result == null) throw new ArgumentNullException("result"); + + m_nodeId = request.ItemToMonitor.NodeId; + m_attributeId = request.ItemToMonitor.AttributeId; + m_indexRange = request.ItemToMonitor.IndexRange; + m_encoding = request.ItemToMonitor.DataEncoding; + m_monitoringMode = request.MonitoringMode; + m_clientHandle = request.RequestedParameters.ClientHandle; + m_samplingInterval = request.RequestedParameters.SamplingInterval; + m_queueSize = request.RequestedParameters.QueueSize; + m_discardOldest = request.RequestedParameters.DiscardOldest; + m_filter = null; + m_error = error; + + if (request.RequestedParameters.Filter != null) + { + m_filter = Utils.Clone(request.RequestedParameters.Filter.Body) as MonitoringFilter; + } + + if (ServiceResult.IsGood(error)) + { + m_id = result.MonitoredItemId; + m_samplingInterval = result.RevisedSamplingInterval; + m_queueSize = result.RevisedQueueSize; + } + } + + /// + /// Updates the object with the results of a modify monitored item request. + /// + internal void SetModifyResult( + MonitoredItemModifyRequest request, + MonitoredItemModifyResult result, + ServiceResult error) + { + if (request == null) throw new ArgumentNullException("request"); + if (result == null) throw new ArgumentNullException("result"); + + m_error = error; + + if (ServiceResult.IsGood(error)) + { + m_clientHandle = request.RequestedParameters.ClientHandle; + m_samplingInterval = request.RequestedParameters.SamplingInterval; + m_queueSize = request.RequestedParameters.QueueSize; + m_discardOldest = request.RequestedParameters.DiscardOldest; + m_filter = null; + + if (request.RequestedParameters.Filter != null) + { + m_filter = Utils.Clone(request.RequestedParameters.Filter.Body) as MonitoringFilter; + } + + m_samplingInterval = result.RevisedSamplingInterval; + m_queueSize = result.RevisedQueueSize; + } + } + + /// + /// Updates the object with the results of a delete item request. + /// + internal void SetDeleteResult(ServiceResult error) + { + m_id = 0; + m_error = error; + } + + /// + /// Sets the error state for the monitored item status. + /// + internal void SetError(ServiceResult error) + { + m_error = error; + } + #endregion + + #region Private Fields + private uint m_id; + private ServiceResult m_error; + private NodeId m_nodeId; + private uint m_attributeId; + private string m_indexRange; + private QualifiedName m_encoding; + private MonitoringMode m_monitoringMode; + private uint m_clientHandle; + private double m_samplingInterval; + private MonitoringFilter m_filter; + private uint m_queueSize; + private bool m_discardOldest; + #endregion + } +} diff --git a/SampleApplications/SDK/Client/NodeCache.cs b/SampleApplications/SDK/Client/NodeCache.cs new file mode 100644 index 0000000000..fecbdad312 --- /dev/null +++ b/SampleApplications/SDK/Client/NodeCache.cs @@ -0,0 +1,844 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Text; +using System.Reflection; + +namespace Opc.Ua.Client +{ + /// + /// A client side cache of the server's type model. + /// + public class NodeCache : INodeTable, ITypeTable + { + #region Constructors + /// + /// Initializes the object with default values. + /// + public NodeCache(Session session) + { + if (session == null) throw new ArgumentNullException("session"); + + m_session = session; + m_typeTree = new TypeTable(m_session.NamespaceUris); + m_nodes = new NodeTable(m_session.NamespaceUris, m_session.ServerUris, m_typeTree); + } + #endregion + + #region INodeTable Members + /// + public NamespaceTable NamespaceUris + { + get { return m_session.NamespaceUris; } + } + + /// + public StringTable ServerUris + { + get { return m_session.ServerUris; } + } + + /// + public ITypeTable TypeTree + { + get { return this; } + } + + /// + public bool Exists(ExpandedNodeId nodeId) + { + return Find(nodeId) != null; + } + + /// + public INode Find(ExpandedNodeId nodeId) + { + // check for null. + if (NodeId.IsNull(nodeId)) + { + return null; + } + + // check if node alredy exists. + INode node = m_nodes.Find(nodeId); + + if (node != null) + { + // do not return temporary nodes created after a Browse(). + if (node.GetType() != typeof(Node)) + { + return node; + } + } + + // fetch node from server. + try + { + return FetchNode(nodeId); + } + catch (Exception e) + { + Utils.Trace("Could not fetch node from server: NodeId={0}, Reason='{1}'.", nodeId, e.Message); + // m_nodes[nodeId] = null; + return null; + } + } + + /// + public INode Find( + ExpandedNodeId sourceId, + NodeId referenceTypeId, + bool isInverse, + bool includeSubtypes, + QualifiedName browseName) + { + // find the source. + Node source = Find(sourceId) as Node; + + if (source == null) + { + return null; + } + + // find all references. + IList references = source.ReferenceTable.Find(referenceTypeId, isInverse, includeSubtypes, m_typeTree); + + foreach (IReference reference in references) + { + INode target = Find(reference.TargetId); + + if (target == null) + { + continue; + } + + if (target.BrowseName == browseName) + { + return target; + } + } + + // target not found. + return null; + } + + /// + public IList Find( + ExpandedNodeId sourceId, + NodeId referenceTypeId, + bool isInverse, + bool includeSubtypes) + { + List hits = new List(); + + // find the source. + Node source = Find(sourceId) as Node; + + if (source == null) + { + return hits; + } + + // find all references. + IList references = source.ReferenceTable.Find(referenceTypeId, isInverse, includeSubtypes, m_typeTree); + + foreach (IReference reference in references) + { + INode target = Find(reference.TargetId); + + if (target == null) + { + continue; + } + + hits.Add(target); + } + + return hits; + } + #endregion + + #region ITypeTable Methods + /// + /// Determines whether a node id is a known type id. + /// + /// The type extended identifier. + /// + /// true if the specified type id is known; otherwise, false. + /// + public bool IsKnown(ExpandedNodeId typeId) + { + INode type = Find(typeId); + + if (type == null) + { + return false; + } + + return m_typeTree.IsKnown(typeId); + } + + /// + /// Determines whether a node id is a known type id. + /// + /// The type identifier. + /// + /// true if the specified type id is known; otherwise, false. + /// + public bool IsKnown(NodeId typeId) + { + INode type = Find(typeId); + + if (type == null) + { + return false; + } + + return m_typeTree.IsKnown(typeId); + } + + /// + /// Returns the immediate supertype for the type. + /// + /// The extended type identifier. + /// + /// A type identifier of the + /// + public NodeId FindSuperType(ExpandedNodeId typeId) + { + INode type = Find(typeId); + + if (type == null) + { + return null; + } + + return m_typeTree.FindSuperType(typeId); + } + + /// + /// Returns the immediate supertype for the type. + /// + /// The type identifier. + /// + /// The immediate supertype idnetyfier for + /// + public NodeId FindSuperType(NodeId typeId) + { + INode type = Find(typeId); + + if (type == null) + { + return null; + } + + return m_typeTree.FindSuperType(typeId); + } + + /// + /// Returns the immediate subtypes for the type. + /// + /// The extended type identifier. + /// + /// List of type identifiers for + /// + public IList FindSubTypes(ExpandedNodeId typeId) + { + ILocalNode type = Find(typeId) as ILocalNode; + + if (type == null) + { + return new List(); + } + + List subtypes = new List(); + + foreach (IReference reference in type.References.Find(ReferenceTypeIds.HasSubtype, false, true, m_typeTree)) + { + if (!reference.TargetId.IsAbsolute) + { + subtypes.Add((NodeId)reference.TargetId); + } + } + + return subtypes; + } + + /// + /// Determines whether a type is a subtype of another type. + /// + /// The subtype identifier. + /// The supertype identifier. + /// + /// true if is supertype of ; otherwise, false. + /// + public bool IsTypeOf(ExpandedNodeId subTypeId, ExpandedNodeId superTypeId) + { + if (subTypeId == superTypeId) + { + return true; + } + + ILocalNode subtype = Find(subTypeId) as ILocalNode; + + if (subtype == null) + { + return false; + } + + ILocalNode supertype = subtype; + + while (supertype != null) + { + ExpandedNodeId currentId = supertype.References.FindTarget(ReferenceTypeIds.HasSubtype, true, true, m_typeTree, 0); + + if (currentId == superTypeId) + { + return true; + } + + supertype = Find(currentId) as ILocalNode; + } + + return false; + } + + /// + /// Determines whether a type is a subtype of another type. + /// + /// The subtype identifier. + /// The supertype identyfier. + /// + /// true if is supertype of ; otherwise, false. + /// + public bool IsTypeOf(NodeId subTypeId, NodeId superTypeId) + { + if (subTypeId == superTypeId) + { + return true; + } + + ILocalNode subtype = Find(subTypeId) as ILocalNode; + + if (subtype == null) + { + return false; + } + + ILocalNode supertype = subtype; + + while (supertype != null) + { + ExpandedNodeId currentId = supertype.References.FindTarget(ReferenceTypeIds.HasSubtype, true, true, m_typeTree, 0); + + if (currentId == superTypeId) + { + return true; + } + + supertype = Find(currentId) as ILocalNode; + } + + return false; + } + + /// + /// Returns the qualified name for the reference type id. + /// + /// The reference type + /// + /// A name qualified with a namespace for the reference . + /// + public QualifiedName FindReferenceTypeName(NodeId referenceTypeId) + { + return m_typeTree.FindReferenceTypeName(referenceTypeId); + } + + /// + /// Returns the node identifier for the reference type with the specified browse name. + /// + /// Browse name of the reference. + /// + /// The identifier for the + /// + public NodeId FindReferenceType(QualifiedName browseName) + { + return m_typeTree.FindReferenceType(browseName); + } + + /// + /// Checks if the identifier represents a that provides encodings + /// for the . + /// + /// The id the encoding node . + /// The id of the DataType node. + /// + /// true if is encoding of the ; otherwise, false. + /// + public bool IsEncodingOf(ExpandedNodeId encodingId, ExpandedNodeId datatypeId) + { + ILocalNode encoding = Find(encodingId) as ILocalNode; + + if (encoding == null) + { + return false; + } + + foreach (IReference reference in encoding.References.Find(ReferenceTypeIds.HasEncoding, true, true, m_typeTree)) + { + if (reference.TargetId == datatypeId) + { + return true; + } + } + + // no match. + return false; + } + + /// + /// Determines if the value contained in an extension object matches the expected data type. + /// + /// The identifier of the expected type . + /// The value. + /// + /// true if the value contained in an extension object matches the + /// expected data type; otherwise, false. + /// + public bool IsEncodingFor(NodeId expectedTypeId, ExtensionObject value) + { + // no match on null values. + if (value == null) + { + return false; + } + + // check for exact match. + if (expectedTypeId == value.TypeId) + { + return true; + } + + // find the encoding. + ILocalNode encoding = Find(value.TypeId) as ILocalNode; + + if (encoding == null) + { + return false; + } + + // find data type. + foreach (IReference reference in encoding.References.Find(ReferenceTypeIds.HasEncoding, true, true, m_typeTree)) + { + if (reference.TargetId == expectedTypeId) + { + return true; + } + } + + // no match. + return false; + } + + /// + /// Determines if the value is an encoding of the + /// + /// The expected type id. + /// The value. + /// + /// true the value is an encoding of the ; otherwise, false. + /// + public bool IsEncodingFor(NodeId expectedTypeId, object value) + { + // null actual datatype matches nothing. + if (value == null) + { + return false; + } + + // null expected datatype matches everything. + if (NodeId.IsNull(expectedTypeId)) + { + return true; + } + + // get the actual datatype. + NodeId actualTypeId = TypeInfo.GetDataTypeId(value); + + // value is valid if the expected datatype is same as or a supertype of the actual datatype + // for example: expected datatype of 'Integer' matches an actual datatype of 'UInt32'. + if (IsTypeOf(actualTypeId, expectedTypeId)) + { + return true; + } + + // allow matches non-structure values where the actual datatype is a supertype of the expected datatype. + // for example: expected datatype of 'UtcTime' matches an actual datatype of 'DateTime'. + if (actualTypeId != DataTypes.Structure) + { + return IsTypeOf(expectedTypeId, actualTypeId); + } + + // for structure types must try to determine the subtype. + ExtensionObject extension = value as ExtensionObject; + + if (extension != null) + { + return IsEncodingFor(expectedTypeId, extension); + } + + // every element in an array must match. + ExtensionObject[] extensions = value as ExtensionObject[]; + + if (extensions != null) + { + for (int ii = 0; ii < extensions.Length; ii++) + { + if (!IsEncodingFor(expectedTypeId, extensions[ii])) + { + return false; + } + } + + return true; + } + + // can only get here if the value is an unrecognized data type. + return false; + } + + /// + /// Returns the data type for the specified encoding. + /// + /// The encoding id. + /// + public NodeId FindDataTypeId(ExpandedNodeId encodingId) + { + ILocalNode encoding = Find(encodingId) as ILocalNode; + + if (encoding == null) + { + return NodeId.Null; + } + + IList references = encoding.References.Find(ReferenceTypeIds.HasEncoding, true, true, m_typeTree); + + if (references.Count > 0) + { + return ExpandedNodeId.ToNodeId(references[0].TargetId, m_session.NamespaceUris); + } + + return NodeId.Null; + } + + /// + /// Returns the data type for the specified encoding. + /// + /// The encoding id. + /// + /// The data type for the + /// + public NodeId FindDataTypeId(NodeId encodingId) + { + ILocalNode encoding = Find(encodingId) as ILocalNode; + + if (encoding == null) + { + return NodeId.Null; + } + + IList references = encoding.References.Find(ReferenceTypeIds.HasEncoding, true, true, m_typeTree); + + if (references.Count > 0) + { + return ExpandedNodeId.ToNodeId(references[0].TargetId, m_session.NamespaceUris); + } + + return NodeId.Null; + } + #endregion + + #region Public Methods + /// + /// Loads the UA defined types into the cache. + /// + /// The context. + public void LoadUaDefinedTypes(ISystemContext context) + { + NodeStateCollection predefinedNodes = new NodeStateCollection(); + predefinedNodes.LoadFromBinaryResource(context, "Opc.Ua.Core.Stack.Generated.Opc.Ua.PredefinedNodes.uanodes", typeof(ArgumentCollection).GetTypeInfo().Assembly, true); + + for (int ii = 0; ii < predefinedNodes.Count; ii++) + { + BaseTypeState type = predefinedNodes[ii] as BaseTypeState; + + if (type == null) + { + continue; + } + + type.Export(context, m_nodes); + } + } + + /// + /// Removes all nodes from the cache. + /// + public void Clear() + { + m_nodes.Clear(); + } + + /// + /// Fetches a node from the server and updates the cache. + /// + public Node FetchNode(ExpandedNodeId nodeId) + { + NodeId localId = ExpandedNodeId.ToNodeId(nodeId, m_session.NamespaceUris); + + if (localId == null) + { + return null; + } + + // fetch node from server. + Node source = m_session.ReadNode(localId); + + try + { + // fetch references from server. + ReferenceDescriptionCollection references = m_session.FetchReferences(localId); + + foreach (ReferenceDescription reference in references) + { + // create a placeholder for the node if it does not already exist. + if (!m_nodes.Exists(reference.NodeId)) + { + Node target = new Node(reference); + m_nodes.Attach(target); + } + + // add the reference. + source.ReferenceTable.Add(reference.ReferenceTypeId, !reference.IsForward, reference.NodeId); + } + } + catch (Exception e) + { + Utils.Trace("Could not fetch references for valid node with NodeId = {0}. Error = {1}", nodeId, e.Message); + } + + // add to cache. + m_nodes.Attach(source); + + return source; + } + + /// + /// Adds the supertypes of the node to the cache. + /// + public void FetchSuperTypes(ExpandedNodeId nodeId) + { + // find the target node, + ILocalNode source = Find(nodeId) as ILocalNode; + + if (source == null) + { + return; + } + + // follow the tree. + ILocalNode subType = source; + + while (subType != null) + { + ILocalNode superType = null; + + IList references = subType.References.Find(ReferenceTypeIds.HasSubtype, true, true, this); + + if (references != null && references.Count > 0) + { + superType = Find(references[0].TargetId) as ILocalNode; + } + + subType = superType; + } + } + + /// + /// Returns the references of the specified node that meet the criteria specified. + /// + public IList FindReferences( + ExpandedNodeId nodeId, + NodeId referenceTypeId, + bool isInverse, + bool includeSubtypes) + { + IList targets = new List(); + + Node source = Find(nodeId) as Node; + + if (source == null) + { + return targets; + } + + IList references = source.ReferenceTable.Find( + referenceTypeId, + isInverse, + includeSubtypes, + m_typeTree); + + foreach (IReference reference in references) + { + INode target = Find(reference.TargetId); + + if (target != null) + { + targets.Add(target); + } + } + + return targets; + } + + /// + /// Returns a display name for a node. + /// + public string GetDisplayText(INode node) + { + // check for null. + if (node == null) + { + return String.Empty; + } + + // check for remote node. + Node target = node as Node; + + if (target == null) + { + return node.ToString(); + } + + string displayText = null; + + // use the modelling rule to determine which parent to follow. + NodeId modellingRule = target.ModellingRule; + + foreach (IReference reference in target.ReferenceTable.Find(ReferenceTypeIds.Aggregates, true, true, m_typeTree)) + { + Node parent = Find(reference.TargetId) as Node; + + // use the first parent if modelling rule is new. + if (modellingRule == Objects.ModellingRule_Mandatory) + { + displayText = GetDisplayText(parent); + break; + } + + // use the type node as the parent for other modelling rules. + if (parent is VariableTypeNode || parent is ObjectTypeNode) + { + displayText = GetDisplayText(parent); + break; + } + } + + // prepend the parent display name. + if (displayText != null) + { + return Utils.Format("{0}.{1}", displayText, node); + } + + // simply use the node name. + return node.ToString(); + } + + /// + /// Returns a display name for a node. + /// + public string GetDisplayText(ExpandedNodeId nodeId) + { + if (NodeId.IsNull(nodeId)) + { + return String.Empty; + } + + INode node = Find(nodeId); + + if (node != null) + { + return GetDisplayText(node); + } + + return Utils.Format("{0}", nodeId); + } + + /// + /// Returns a display name for the target of a reference. + /// + public string GetDisplayText(ReferenceDescription reference) + { + if (reference == null || NodeId.IsNull(reference.NodeId)) + { + return String.Empty; + } + + INode node = Find(reference.NodeId); + + if (node != null) + { + return GetDisplayText(node); + } + + return reference.ToString(); + } + + /// + /// Builds the relative path from a type to a node. + /// + public NodeId BuildBrowsePath(ILocalNode node, IList browsePath) + { + NodeId typeId = null; + + browsePath.Add(node.BrowseName); + + return typeId; + } + #endregion + + #region Private Fields + private Session m_session; + private TypeTable m_typeTree; + private NodeTable m_nodes; + #endregion + } +} diff --git a/SampleApplications/SDK/Client/Opc.Ua.Client.csproj b/SampleApplications/SDK/Client/Opc.Ua.Client.csproj new file mode 100644 index 0000000000..2bbe8ffb97 --- /dev/null +++ b/SampleApplications/SDK/Client/Opc.Ua.Client.csproj @@ -0,0 +1,149 @@ + + + + + Debug + AnyCPU + {B89BFA9D-419F-49B4-8210-286E7167E646} + Library + Properties + Opc.Ua.Client + Opc.Ua.Client + en-US + UAP + 10.0.10240.0 + 10.0.10240.0 + 14 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + ARM + true + bin\ARM\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + ARM + false + prompt + true + + + ARM + bin\ARM\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + ARM + false + prompt + true + + + x64 + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x64 + false + prompt + true + + + x64 + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x64 + false + prompt + true + + + x86 + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x86 + false + prompt + true + + + x86 + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x86 + false + prompt + true + + + + + + + + + + + + + + + + + + + + + + + + + {aa9d8d17-5dbd-4e77-8496-e32177573bf3} + Opc.Ua.Core + + + + 14.0 + + + + \ No newline at end of file diff --git a/SampleApplications/SDK/Client/Properties/AssemblyInfo.cs b/SampleApplications/SDK/Client/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..01dc073bee --- /dev/null +++ b/SampleApplications/SDK/Client/Properties/AssemblyInfo.cs @@ -0,0 +1,63 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Opc.Ua.Client")] +[assembly: AssemblyDescription("UA Client Library")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("OPC Foundation")] +[assembly: AssemblyProduct("OPC UA SDK")] +[assembly: AssemblyCopyright(AssemblyVersionInfo.Copyright)] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("4f44cd9c-d8cf-4e52-9c21-d0330fe0912c")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion(AssemblyVersionInfo.CurrentVersion)] +[assembly: AssemblyFileVersion(AssemblyVersionInfo.CurrentFileVersion)] diff --git a/SampleApplications/SDK/Client/Properties/AssemblyVersionInfo.cs b/SampleApplications/SDK/Client/Properties/AssemblyVersionInfo.cs new file mode 100644 index 0000000000..0b0b5ea13e --- /dev/null +++ b/SampleApplications/SDK/Client/Properties/AssemblyVersionInfo.cs @@ -0,0 +1,47 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 The OPC Foundation, Inc. All rights reserved. + * + * OPC Reciprocal Community License ("RCL") Version 1.00 + * + * Unless explicitly acquired and licensed from Licensor under another + * license, the contents of this file are subject to the Reciprocal + * Community License ("RCL") Version 1.00, or subsequent versions + * as allowed by the RCL, and You may not copy or use this file in either + * source code or executable form, except in compliance with the terms and + * conditions of the RCL. + * + * All software distributed under the RCL is provided strictly on an + * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, + * AND LICENSOR HEREBY DISCLAIMS ALL SUCH WARRANTIES, INCLUDING WITHOUT + * LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE, QUIET ENJOYMENT, OR NON-INFRINGEMENT. See the RCL for specific + * language governing rights and limitations under the RCL. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/RCL/1.00/ + * ======================================================================*/ + +using System; +using System.Collections.Generic; +using System.Text; + +/// +/// Defines string constants for SDK version information. +/// +internal static class AssemblyVersionInfo +{ + /// + /// The current copy right notice. + /// + public const string Copyright = "Copyright © 2004-2013 OPC Foundation, Inc"; + + /// + /// The current build version. + /// + public const string CurrentVersion = "1.02.334.6"; + + /// + /// The current build file version. + /// + public const string CurrentFileVersion = "1.02.334.6"; +} diff --git a/SampleApplications/SDK/Client/Properties/Opc.Ua.Client.rd.xml b/SampleApplications/SDK/Client/Properties/Opc.Ua.Client.rd.xml new file mode 100644 index 0000000000..f193fdc1c3 --- /dev/null +++ b/SampleApplications/SDK/Client/Properties/Opc.Ua.Client.rd.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/SampleApplications/SDK/Client/Session.cs b/SampleApplications/SDK/Client/Session.cs new file mode 100644 index 0000000000..90cd5986f8 --- /dev/null +++ b/SampleApplications/SDK/Client/Session.cs @@ -0,0 +1,4076 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Text; +using System.Threading; +using System.ServiceModel; +using System.Runtime.Serialization; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Globalization; +using System.IO; +using System.Xml; +using System.Threading.Tasks; +using Windows.Security.Cryptography; +using Windows.Storage.Streams; +using System.Reflection; + +namespace Opc.Ua.Client +{ + /// + /// Manages a session with a server. + /// + public class Session : SessionClient, IDisposable + { + #region Constructors + /// + /// Constructs a new instance of the session. + /// + /// The channel used to communicate with the server. + /// The configuration for the client application. + /// The endpoint use to initialize the channel. + public Session( + ISessionChannel channel, + ApplicationConfiguration configuration, + ConfiguredEndpoint endpoint) + : + this(channel as ITransportChannel, configuration, endpoint, null) + { + } + + /// + /// Constructs a new instance of the session. + /// + /// The channel used to communicate with the server. + /// The configuration for the client application. + /// The endpoint use to initialize the channel. + /// The certificate to use for the client. + /// + /// The application configuration is used to look up the certificate if none is provided. + /// The clientCertificate must have the private key. This will require that the certificate + /// be loaded from a certicate store. Converting a DER encoded blob to a X509Certificate2 + /// will not include a private key. + /// + public Session( + ITransportChannel channel, + ApplicationConfiguration configuration, + ConfiguredEndpoint endpoint, + X509Certificate2 clientCertificate) + : + base(channel) + { + Initialize(channel, configuration, endpoint, clientCertificate); + } + + /// + /// Initializes a new instance of the class. + /// + /// The channel. + /// The template session. + /// if set to true the event handlers are copied. + public Session(ITransportChannel channel, Session template, bool copyEventHandlers) + : + base(channel) + { + Initialize(channel, template.m_configuration, template.m_endpoint, template.m_instanceCertificate); + + m_defaultSubscription = template.m_defaultSubscription; + m_sessionTimeout = template.m_sessionTimeout; + m_maxRequestMessageSize = template.m_maxRequestMessageSize; + m_preferredLocales = template.m_preferredLocales; + m_sessionName = template.m_sessionName; + m_handle = template.m_handle; + m_identity = template.m_identity; + m_keepAliveInterval = template.m_keepAliveInterval; + + if (copyEventHandlers) + { + m_KeepAlive = template.m_KeepAlive; + m_Publish = template.m_Publish; + m_PublishError = template.m_PublishError; + m_SubscriptionsChanged = template.m_SubscriptionsChanged; + m_SessionClosing = template.m_SessionClosing; + } + + foreach (Subscription subscription in template.Subscriptions) + { + this.AddSubscription(new Subscription(subscription, copyEventHandlers)); + } + } + + /// + /// Initializes the channel. + /// + private void Initialize( + ITransportChannel channel, + ApplicationConfiguration configuration, + ConfiguredEndpoint endpoint, + X509Certificate2 clientCertificate) + { + Initialize(); + + // save configuration information. + m_configuration = configuration; + m_endpoint = endpoint; + + // update the default subscription. + m_defaultSubscription.MinLifetimeInterval = (uint)configuration.ClientConfiguration.MinSubscriptionLifetime; + + if (m_endpoint.Description.SecurityPolicyUri != SecurityPolicies.None) + { + // update client certificate. + m_instanceCertificate = clientCertificate; + + if (clientCertificate == null) + { + // load the application instance certificate. + if (m_configuration.SecurityConfiguration.ApplicationCertificate == null) + { + throw new ServiceResultException( + StatusCodes.BadConfigurationError, + "The client configuration does not specify an application instance certificate."); + } + + Task t = Task.Run( async () => + { + m_instanceCertificate = await m_configuration.SecurityConfiguration.ApplicationCertificate.Find(true); + }); + t.Wait(); + } + + // check for valid certificate. + if (m_instanceCertificate == null) + { + throw ServiceResultException.Create( + StatusCodes.BadConfigurationError, + "Cannot find the application instance certificate. Store={0}, SubjectName={1}, Thumbprint={2}.", + m_configuration.SecurityConfiguration.ApplicationCertificate.StorePath, + m_configuration.SecurityConfiguration.ApplicationCertificate.SubjectName, + m_configuration.SecurityConfiguration.ApplicationCertificate.Thumbprint); + } + + // check for private key. + if (!m_instanceCertificate.HasPrivateKey) + { + throw ServiceResultException.Create( + StatusCodes.BadConfigurationError, + "Do not have a privat key for the application instance certificate. Subject={0}, Thumbprint={1}.", + m_instanceCertificate.Subject, + m_instanceCertificate.Thumbprint); + } + + //load certificate chain + /*m_instanceCertificateChain = new X509Certificate2Collection(m_instanceCertificate); + List issuers = new List(); + configuration.CertificateValidator.GetIssuers(m_instanceCertificate, issuers); + for (int i = 0; i < issuers.Count; i++) + { + m_instanceCertificateChain.Add(issuers[i].Certificate); + }*/ + } + + // initialize the message context. + ServiceMessageContext messageContext = channel.MessageContext; + + if (messageContext != null) + { + m_namespaceUris = messageContext.NamespaceUris; + m_serverUris = messageContext.ServerUris; + m_factory = messageContext.Factory; + } + else + { + m_namespaceUris = new NamespaceTable(); + m_serverUris = new StringTable(); + m_factory = ServiceMessageContext.GlobalContext.Factory; + } + + // set the default preferred locales. + m_preferredLocales = new string[] { CultureInfo.CurrentCulture.Name }; + + // create a context to use. + m_systemContext = new SystemContext(); + + m_systemContext.SystemHandle = this; + m_systemContext.EncodeableFactory = m_factory; + m_systemContext.NamespaceUris = m_namespaceUris; + m_systemContext.ServerUris = m_serverUris; + m_systemContext.TypeTable = this.TypeTree; + m_systemContext.PreferredLocales = null; + m_systemContext.SessionId = null; + m_systemContext.UserIdentity = null; + } + + /// + /// Sets the object members to default values. + /// + private void Initialize() + { + m_sessionTimeout = 0; + m_namespaceUris = new NamespaceTable(); + m_serverUris = new StringTable(); + m_factory = EncodeableFactory.GlobalFactory; + m_nodeCache = new NodeCache(this); + m_configuration = null; + m_instanceCertificate = null; + m_endpoint = null; + m_subscriptions = new List(); + m_dictionaries = new Dictionary(); + m_acknowledgementsToSend = new SubscriptionAcknowledgementCollection(); + m_identityHistory = new List(); + m_outstandingRequests = new LinkedList(); + m_keepAliveInterval = 5000; + m_sessionName = ""; + + m_defaultSubscription = new Subscription(); + + m_defaultSubscription.DisplayName = "Subscription"; + m_defaultSubscription.PublishingInterval = 1000; + m_defaultSubscription.KeepAliveCount = 10; + m_defaultSubscription.LifetimeCount = 1000; + m_defaultSubscription.Priority = 255; + m_defaultSubscription.PublishingEnabled = true; + } + #endregion + + #region IDisposable Members + /// + /// Closes the session and the underlying channel. + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + Utils.SilentDispose(m_keepAliveTimer); + m_keepAliveTimer = null; + + Utils.SilentDispose(m_defaultSubscription); + m_defaultSubscription = null; + + foreach (Subscription subscription in m_subscriptions) + { + Utils.SilentDispose(subscription); + } + + m_subscriptions.Clear(); + } + + base.Dispose(disposing); + } + #endregion + + #region Events + /// + /// Raised when a keep alive arrives from the server or an error is detected. + /// + /// + /// Once a session is created a timer will periodically read the server state and current time. + /// If this read operation succeeds this event will be raised each time the keep alive period elapses. + /// If an error is detected (KeepAliveStopped == true) then this event will be raised as well. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly")] + public event KeepAliveEventHandler KeepAlive + { + add + { + lock (m_eventLock) + { + m_KeepAlive += value; + } + } + + remove + { + lock (m_eventLock) + { + m_KeepAlive -= value; + } + } + } + + /// + /// Raised when a notification message arrives in a publish response. + /// + /// + /// All publish requests are managed by the Session object. When a response arrives it is + /// validated and passed to the appropriate Subscription object and this event is raised. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly")] + public event NotificationEventHandler Notification + { + add + { + lock (m_eventLock) + { + m_Publish += value; + } + } + + remove + { + lock (m_eventLock) + { + m_Publish -= value; + } + } + } + + /// + /// Raised when an exception occurs while processing a publish response. + /// + /// + /// Exceptions in a publish response are not necessarily fatal and the Session will + /// attempt to recover by issuing Republish requests if missing messages are detected. + /// That said, timeout errors may be a symptom of a OperationTimeout that is too short + /// when compared to the shortest PublishingInterval/KeepAliveCount amount the current + /// Subscriptions. The OperationTimeout should be twice the minimum value for + /// PublishingInterval*KeepAliveCount. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly")] + public event PublishErrorEventHandler PublishError + { + add + { + lock (m_eventLock) + { + m_PublishError += value; + } + } + + remove + { + lock (m_eventLock) + { + m_PublishError -= value; + } + } + } + + /// + /// Raised when a subscription is added or removed + /// + public event EventHandler SubscriptionsChanged + { + add + { + m_SubscriptionsChanged += value; + } + + remove + { + m_SubscriptionsChanged -= value; + } + } + + /// + /// Raised to indicate the session is closing. + /// + public event EventHandler SessionClosing + { + add + { + m_SessionClosing += value; + } + + remove + { + m_SessionClosing -= value; + } + } + #endregion + + #region Public Properties + /// + /// Gets the endpoint used to connect to the server. + /// + public ConfiguredEndpoint ConfiguredEndpoint + { + get + { + return m_endpoint; + } + } + + /// + /// Gets the name assigned to the session. + /// + public string SessionName + { + get + { + return m_sessionName; + } + } + + /// + /// Gets the period for wich the server will maintain the session if there is no communication from the client. + /// + public double SessionTimeout + { + get + { + return m_sessionTimeout; + } + } + + /// + /// Gets the local handle assigned to the session + /// + public object Handle + { + get { return m_handle; } + set { m_handle = value; } + } + + /// + /// Gets the user identity currently used for the session. + /// + public IUserIdentity Identity + { + get + { + return m_identity; + } + } + + /// + /// Gets a list of user identities that can be used to connect to the server. + /// + public IEnumerable IdentityHistory + { + get { return m_identityHistory; } + } + + /// + /// Gets the table of namespace uris known to the server. + /// + public NamespaceTable NamespaceUris + { + get { return m_namespaceUris; } + } + + /// + /// Gest the table of remote server uris known to the server. + /// + public StringTable ServerUris + { + get { return m_serverUris; } + } + + /// + /// Gets the system context for use with the session. + /// + public ISystemContext SystemContext + { + get { return m_systemContext; } + } + + /// + /// Gets the factory used to create encodeable objects that the server understands. + /// + public EncodeableFactory Factory + { + get { return m_factory; } + } + + /// + /// Gets the cache of the server's type tree. + /// + public ITypeTable TypeTree + { + get { return m_nodeCache.TypeTree; } + } + + /// + /// Gets the cache of nodes fetched from the server. + /// + public NodeCache NodeCache + { + get { return m_nodeCache; } + } + + /// + /// Gets the context to use for filter operations. + /// + public FilterContext FilterContext + { + get { return new FilterContext(m_namespaceUris, m_nodeCache.TypeTree, m_preferredLocales); } + } + + /// + /// Gets the locales that the server should use when returning localized text. + /// + public StringCollection PreferredLocales + { + get { return m_preferredLocales; } + } + + /// + /// Gets the subscriptions owned by the session. + /// + public IEnumerable Subscriptions + { + get + { + lock (SyncRoot) + { + return new ReadOnlyList(m_subscriptions); + } + } + } + + /// + /// Gets the number of subscriptions owned by the session. + /// + public int SubscriptionCount + { + get + { + lock (SyncRoot) + { + return m_subscriptions.Count; + } + } + } + + /// + /// Gets or Sets the default subscription for the session. + /// + public Subscription DefaultSubscription + { + get { return m_defaultSubscription; } + set { m_defaultSubscription = value; } + } + + /// + /// Gets or Sets how frequently the server is pinged to see if communication is still working. + /// + /// + /// This interval controls how much time elaspes before a communication error is detected. + /// If everything is ok the KeepAlive event will be raised each time this period elapses. + /// + public int KeepAliveInterval + { + get + { + return m_keepAliveInterval; + } + + set + { + m_keepAliveInterval = value; + StartKeepAliveTimer(); + } + } + + /// + /// Returns true if the session is not receiving keep alives. + /// + /// + /// Set to true if the server does not respond for 2 times the KeepAliveInterval. + /// Set to false is communication recovers. + /// + public bool KeepAliveStopped + { + get + { + lock (m_eventLock) + { + long delta = DateTime.UtcNow.Ticks - m_lastKeepAliveTime.Ticks; + + // add a 1000ms guard band to allow for network lag. + return (m_keepAliveInterval * 2) * TimeSpan.TicksPerMillisecond <= delta; + } + } + } + + /// + /// Gets the time of the last keep alive. + /// + public DateTime LastKeepAliveTime + { + get { return m_lastKeepAliveTime; } + } + + /// + /// Gets the number of outstanding publish or keep alive requests. + /// + public int OutstandingRequestCount + { + get + { + lock (m_outstandingRequests) + { + return m_outstandingRequests.Count; + } + } + } + + /// + /// Gets the number of outstanding publish or keep alive requests which appear to hung. + /// + public int DefunctRequestCount + { + get + { + lock (m_outstandingRequests) + { + int count = 0; + + for (LinkedListNode ii = m_outstandingRequests.First; ii != null; ii = ii.Next) + { + if (ii.Value.Defunct) + { + count++; + } + } + + return count; + } + } + } + + /// + /// Gets the number of good outstanding publish requests. + /// + public int GoodPublishRequestCount + { + get + { + lock (m_outstandingRequests) + { + int count = 0; + + for (LinkedListNode ii = m_outstandingRequests.First; ii != null; ii = ii.Next) + { + if (!ii.Value.Defunct && ii.Value.RequestTypeId == DataTypes.PublishRequest) + { + count++; + } + } + + return count; + } + } + } + #endregion + + #region Public Methods + + /// + /// Creates a new communication session with a server by invoking the CreateSession service + /// + /// The configuration for the client application. + /// The endpoint for the server. + /// If set to true the discovery endpoint is used to update the endpoint description before connecting. + /// The name to assign to the session. + /// The timeout period for the session. + /// The identity. + /// The user identity to associate with the session. + /// The new session object + public static async Task Create( + ApplicationConfiguration configuration, + ConfiguredEndpoint endpoint, + bool updateBeforeConnect, + string sessionName, + uint sessionTimeout, + IUserIdentity identity, + IList preferredLocales) + { + return await Create(configuration, endpoint, updateBeforeConnect, false, sessionName, sessionTimeout, identity, preferredLocales); + } + + /// + /// Creates a new communication session with a server by invoking the CreateSession service + /// + /// The configuration for the client application. + /// The endpoint for the server. + /// If set to true the discovery endpoint is used to update the endpoint description before connecting. + /// If set to true then the domain in the certificate must match the endpoint used. + /// The name to assign to the session. + /// The timeout period for the session. + /// The user identity to associate with the session. + /// The preferred locales. + /// The new session object. + public static async Task Create( + ApplicationConfiguration configuration, + ConfiguredEndpoint endpoint, + bool updateBeforeConnect, + bool checkDomain, + string sessionName, + uint sessionTimeout, + IUserIdentity identity, + IList preferredLocales) + { + endpoint.UpdateBeforeConnect = updateBeforeConnect; + + EndpointDescription endpointDescription = endpoint.Description; + + // create the endpoint configuration (use the application configuration to provide default values). + EndpointConfiguration endpointConfiguration = endpoint.Configuration; + + if (endpointConfiguration == null) + { + endpoint.Configuration = endpointConfiguration = EndpointConfiguration.Create(configuration); + } + + // create message context. + ServiceMessageContext messageContext = configuration.CreateMessageContext(); + + // update endpoint description using the discovery endpoint. + if (endpoint.UpdateBeforeConnect) + { + BindingFactory bindingFactory = BindingFactory.Create(configuration, messageContext); + endpoint.UpdateFromServer(bindingFactory); + + endpointDescription = endpoint.Description; + endpointConfiguration = endpoint.Configuration; + } + + // checks the domains in the certificate. + if (checkDomain && endpoint.Description.ServerCertificate != null && endpoint.Description.ServerCertificate.Length > 0) + { + bool domainFound = false; + + X509Certificate2 serverCertificate = new X509Certificate2(endpoint.Description.ServerCertificate); + + // check the certificate domains. + IList domains = Utils.GetDomainsFromCertficate(serverCertificate); + + if (domains != null) + { + string hostname = endpoint.EndpointUrl.DnsSafeHost; + + if (hostname == "localhost" || hostname == "127.0.0.1") + { + hostname = Utils.GetHostName(); + } + + for (int ii = 0; ii < domains.Count; ii++) + { + if (String.Compare(hostname, domains[ii], StringComparison.CurrentCultureIgnoreCase) == 0) + { + domainFound = true; + break; + } + } + } + + if (!domainFound) + { + throw new ServiceResultException(StatusCodes.BadCertificateHostNameInvalid); + } + } + + X509Certificate2 clientCertificate = null; + + if (endpointDescription.SecurityPolicyUri != SecurityPolicies.None) + { + if (configuration.SecurityConfiguration.ApplicationCertificate == null) + { + throw ServiceResultException.Create(StatusCodes.BadConfigurationError, "ApplicationCertificate must be specified."); + } + + clientCertificate = await configuration.SecurityConfiguration.ApplicationCertificate.Find(true); + + if (clientCertificate == null) + { + throw ServiceResultException.Create(StatusCodes.BadConfigurationError, "ApplicationCertificate cannot be found."); + } + } + + // initialize the channel which will be created with the server. + ITransportChannel channel = SessionChannel.Create( + configuration, + endpointDescription, + endpointConfiguration, + //clientCertificateChain, + clientCertificate, + messageContext); + + // create the session object. + Session session = new Session(channel, configuration, endpoint, null); + + // create the session. + try + { + session.Open(sessionName, sessionTimeout, identity, preferredLocales); + } + catch + { + session.Dispose(); + throw; + } + + return session; + } + + + /// + /// Recreates a session based on a specified template. + /// + /// The Session object to use as template + /// The new session object. + public static Session Recreate(Session template) + { + // create the channel object used to connect to the server. + ITransportChannel channel = SessionChannel.Create( + template.m_configuration, + template.m_endpoint.Description, + template.m_endpoint.Configuration, + template.m_instanceCertificate, + template.m_configuration.CreateMessageContext()); + + // create the session object. + Session session = new Session(channel, template, true); + + try + { + // open the session. + session.Open( + template.m_sessionName, + (uint)template.m_sessionTimeout, + template.m_identity, + template.m_preferredLocales); + + // create the subscriptions. + foreach (Subscription subscription in session.Subscriptions) + { + subscription.Create(); + } + } + catch (Exception e) + { + session.Dispose(); + throw ServiceResultException.Create(StatusCodes.BadCommunicationError, e, "Could not recreate session. {0}", template.m_sessionName); + } + + return session; + } + + /// + /// Used to handle renews of user identity tokens before reconnect. + /// + public delegate IUserIdentity RenewUserIdentityEventHandler(Session session, IUserIdentity identity); + + /// + /// Raised before a reconnect operation completes. + /// + public event RenewUserIdentityEventHandler RenewUserIdentity + { + add { m_RenewUserIdentity += value; } + remove { m_RenewUserIdentity -= value; } + } + + private event RenewUserIdentityEventHandler m_RenewUserIdentity; + + /// + /// Reconnects to the server after a network failure. + /// + public void Reconnect() + { + try + { + lock (SyncRoot) + { + // check if already connecting. + if (m_reconnecting) + { + throw ServiceResultException.Create( + StatusCodes.BadInvalidState, + "Session is already attempting to reconnect."); + } + + Utils.Trace("Session RECONNECT starting."); + m_reconnecting = true; + + // stop keep alives. + if (m_keepAliveTimer != null) + { + m_keepAliveTimer.Dispose(); + m_keepAliveTimer = null; + } + } + + EndpointDescription endpoint = m_endpoint.Description; + + // create the client signature. + byte[] dataToSign = Utils.Append(endpoint.ServerCertificate, m_serverNonce); + SignatureData clientSignature = SecurityPolicies.Sign(m_instanceCertificate, endpoint.SecurityPolicyUri, dataToSign); + + // check that the user identity is supported by the endpoint. + UserTokenPolicy identityPolicy = endpoint.FindUserTokenPolicy(m_identity.TokenType, m_identity.IssuedTokenType); + + if (identityPolicy == null) + { + throw ServiceResultException.Create( + StatusCodes.BadUserAccessDenied, + "Endpoint does not supported the user identity type provided."); + } + + // select the security policy for the user token. + string securityPolicyUri = identityPolicy.SecurityPolicyUri; + + if (String.IsNullOrEmpty(securityPolicyUri)) + { + securityPolicyUri = endpoint.SecurityPolicyUri; + } + + // need to refresh the identity (reprompt for password, refresh token). + if (m_RenewUserIdentity != null) + { + m_identity = m_RenewUserIdentity(this, m_identity); + } + + // sign data with user token. + UserIdentityToken identityToken = m_identity.GetIdentityToken(); + identityToken.PolicyId = identityPolicy.PolicyId; + SignatureData userTokenSignature = identityToken.Sign(dataToSign, securityPolicyUri); + + // encrypt token. + identityToken.Encrypt(m_serverCertificate, m_serverNonce, securityPolicyUri); + + // send the software certificates assigned to the client. + SignedSoftwareCertificateCollection clientSoftwareCertificates = GetSoftwareCertificates(); + + Utils.Trace("Session REPLACING channel."); + + // check if the channel supports reconnect. + if ((TransportChannel.SupportedFeatures & TransportChannelFeatures.Reconnect) != 0) + { + TransportChannel.Reconnect(); + } + else + { + // initialize the channel which will be created with the server. + ITransportChannel channel = SessionChannel.Create( + m_configuration, + m_endpoint.Description, + m_endpoint.Configuration, + m_instanceCertificate, + MessageContext); + + // disposes the existing channel. + TransportChannel = channel; + } + + // reactivate session. + byte[] serverNonce = null; + StatusCodeCollection certificateResults = null; + DiagnosticInfoCollection certificateDiagnosticInfos = null; + + Utils.Trace("Session RE-ACTIVATING session."); + + IAsyncResult result = BeginActivateSession( + null, + clientSignature, + null, + m_preferredLocales, + new ExtensionObject(identityToken), + userTokenSignature, + null, + null); + + if (!result.AsyncWaitHandle.WaitOne(5000)) + { + Utils.Trace("WARNING: ACTIVATE SESSION timed out. {1}/{0}", OutstandingRequestCount, GoodPublishRequestCount); + } + + EndActivateSession( + result, + out serverNonce, + out certificateResults, + out certificateDiagnosticInfos); + + int publishCount = 0; + + lock (SyncRoot) + { + Utils.Trace("Session RECONNECT completed successfully."); + m_serverNonce = serverNonce; + m_reconnecting = false; + publishCount = m_subscriptions.Count; + } + + // refill pipeline. + for (int ii = 0; ii < publishCount; ii++) + { + BeginPublish(OperationTimeout); + } + + StartKeepAliveTimer(); + } + finally + { + m_reconnecting = false; + } + } + + + /// + /// Saves all the subscriptions of the session. + /// + /// The file path. + public void Save(string filePath) + { + Save(filePath, Subscriptions); + } + + /// + /// Saves a set of subscriptions. + /// + public void Save(string filePath, IEnumerable subscriptions) + { + XmlWriterSettings settings = new XmlWriterSettings(); + + settings.Indent = true; + settings.OmitXmlDeclaration = false; + settings.Encoding = Encoding.UTF8; + + XmlWriter writer = XmlWriter.Create(new StringBuilder(filePath), settings); + + SubscriptionCollection subscriptionList = new SubscriptionCollection(subscriptions); + + try + { + DataContractSerializer serializer = new DataContractSerializer(typeof(SubscriptionCollection)); + serializer.WriteObject(writer, subscriptionList); + } + finally + { + writer.Flush(); + writer.Dispose(); + } + } + + + /// + /// Load the list of subscriptions saved in a file. + /// + /// The file path. + /// The list of loaded subscritons + public IEnumerable Load(string filePath) + { + XmlReaderSettings settings = new XmlReaderSettings(); + + settings.ConformanceLevel = ConformanceLevel.Document; + settings.CloseInput = true; + + XmlReader reader = XmlReader.Create(filePath, settings); + + try + { + DataContractSerializer serializer = new DataContractSerializer(typeof(SubscriptionCollection)); + + SubscriptionCollection subscriptions = (SubscriptionCollection)serializer.ReadObject(reader); + + foreach (Subscription subscription in subscriptions) + { + AddSubscription(subscription); + } + + return subscriptions; + } + finally + { + reader.Dispose(); + } + } + + /// + /// Updates the local copy of the server's namespace uri and server uri tables. + /// + public void FetchNamespaceTables() + { + ReadValueIdCollection nodesToRead = new ReadValueIdCollection(); + + // request namespace array. + ReadValueId valueId = new ReadValueId(); + + valueId.NodeId = Variables.Server_NamespaceArray; + valueId.AttributeId = Attributes.Value; + + nodesToRead.Add(valueId); + + // request server array. + valueId = new ReadValueId(); + + valueId.NodeId = Variables.Server_ServerArray; + valueId.AttributeId = Attributes.Value; + + nodesToRead.Add(valueId); + + // read from server. + DataValueCollection values = null; + DiagnosticInfoCollection diagnosticInfos = null; + + ResponseHeader responseHeader = this.Read( + null, + 0, + TimestampsToReturn.Both, + nodesToRead, + out values, + out diagnosticInfos); + + ValidateResponse(values, nodesToRead); + ValidateDiagnosticInfos(diagnosticInfos, nodesToRead); + + // validate namespace array. + ServiceResult result = ValidateDataValue(values[0], typeof(string[]), 0, diagnosticInfos, responseHeader); + + if (ServiceResult.IsBad(result)) + { + throw new ServiceResultException(result); + } + + m_namespaceUris.Update((string[])values[0].Value); + + // validate server array. + result = ValidateDataValue(values[1], typeof(string[]), 1, diagnosticInfos, responseHeader); + + if (ServiceResult.IsBad(result)) + { + throw new ServiceResultException(result); + } + + m_serverUris.Update((string[])values[1].Value); + } + + /// + /// Updates the cache with the type and its subtypes. + /// + /// + /// This method can be used to ensure the TypeTree is populated. + /// + public void FetchTypeTree(ExpandedNodeId typeId) + { + Node node = NodeCache.Find(typeId) as Node; + + if (node != null) + { + foreach (IReference reference in node.Find(ReferenceTypeIds.HasSubtype, false)) + { + FetchTypeTree(reference.TargetId); + } + } + } + + /// + /// Returns the available encodings for a node + /// + /// The variable node. + /// + public ReferenceDescriptionCollection ReadAvailableEncodings(NodeId variableId) + { + VariableNode variable = NodeCache.Find(variableId) as VariableNode; + + if (variable == null) + { + 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 (NodeId.IsNull(variable.DataType)) + { + return new ReferenceDescriptionCollection(); + } + + // no encodings for non-structures. + if (!TypeTree.IsTypeOf(variable.DataType, DataTypes.Structure)) + { + return new ReferenceDescriptionCollection(); + } + + // look for cached values. + IList encodings = NodeCache.Find(variableId, ReferenceTypeIds.HasEncoding, false, true); + + if (encodings.Count > 0) + { + ReferenceDescriptionCollection references = new ReferenceDescriptionCollection(); + + foreach (INode encoding in encodings) + { + ReferenceDescription reference = new ReferenceDescription(); + + reference.ReferenceTypeId = ReferenceTypeIds.HasEncoding; + reference.IsForward = true; + reference.NodeId = encoding.NodeId; + reference.NodeClass = encoding.NodeClass; + reference.BrowseName = encoding.BrowseName; + reference.DisplayName = encoding.DisplayName; + reference.TypeDefinition = encoding.TypeDefinitionId; + + references.Add(reference); + } + + return references; + } + + Browser browser = new Browser(this); + + browser.BrowseDirection = BrowseDirection.Forward; + browser.ReferenceTypeId = ReferenceTypeIds.HasEncoding; + browser.IncludeSubtypes = false; + browser.NodeClassMask = 0; + + return browser.Browse(variable.DataType); + } + + + /// + /// Returns the data description for the encoding. + /// + /// The encoding Id. + /// + public ReferenceDescription FindDataDescription(NodeId encodingId) + { + Browser browser = new Browser(this); + + browser.BrowseDirection = BrowseDirection.Forward; + browser.ReferenceTypeId = ReferenceTypeIds.HasDescription; + browser.IncludeSubtypes = false; + browser.NodeClassMask = 0; + + ReferenceDescriptionCollection references = browser.Browse(encodingId); + + if (references.Count == 0) + { + throw ServiceResultException.Create(StatusCodes.BadNodeIdInvalid, "Encoding does not refer to a valid data description."); + } + + return references[0]; + } + + + /// + /// Returns the data dictionary that constains the description. + /// + /// The description id. + /// + public async Task FindDataDictionary(NodeId descriptionId) + { + // check if the dictionary has already been loaded. + foreach (DataDictionary dictionary in m_dictionaries.Values) + { + if (dictionary.Contains(descriptionId)) + { + return dictionary; + } + } + + // find the dictionary for the description. + Browser browser = new Browser(this); + + browser.BrowseDirection = BrowseDirection.Inverse; + browser.ReferenceTypeId = ReferenceTypeIds.HasComponent; + browser.IncludeSubtypes = false; + browser.NodeClassMask = 0; + + ReferenceDescriptionCollection references = browser.Browse(descriptionId); + + if (references.Count == 0) + { + throw ServiceResultException.Create(StatusCodes.BadNodeIdInvalid, "Description does not refer to a valid data dictionary."); + } + + // load the dictionary. + NodeId dictionaryId = ExpandedNodeId.ToNodeId(references[0].NodeId, m_namespaceUris); + + DataDictionary dictionaryToLoad = new DataDictionary(this); + + await dictionaryToLoad.Load(references[0]); + + m_dictionaries[dictionaryId] = dictionaryToLoad; + + return dictionaryToLoad; + } + + /// + /// Reads the values for the node attributes and returns a node object. + /// + /// The nodeId. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1505:AvoidUnmaintainableCode"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")] + public Node ReadNode(NodeId nodeId) + { + // build list of attributes. + SortedDictionary attributes = new SortedDictionary(); + + attributes.Add(Attributes.NodeId, null); + attributes.Add(Attributes.NodeClass, null); + attributes.Add(Attributes.BrowseName, null); + attributes.Add(Attributes.DisplayName, null); + attributes.Add(Attributes.Description, null); + attributes.Add(Attributes.WriteMask, null); + attributes.Add(Attributes.UserWriteMask, null); + attributes.Add(Attributes.DataType, null); + attributes.Add(Attributes.ValueRank, null); + attributes.Add(Attributes.ArrayDimensions, null); + attributes.Add(Attributes.AccessLevel, null); + attributes.Add(Attributes.UserAccessLevel, null); + attributes.Add(Attributes.Historizing, null); + attributes.Add(Attributes.MinimumSamplingInterval, null); + attributes.Add(Attributes.EventNotifier, null); + attributes.Add(Attributes.Executable, null); + attributes.Add(Attributes.UserExecutable, null); + attributes.Add(Attributes.IsAbstract, null); + attributes.Add(Attributes.InverseName, null); + attributes.Add(Attributes.Symmetric, null); + attributes.Add(Attributes.ContainsNoLoops, null); + + // build list of values to read. + ReadValueIdCollection itemsToRead = new ReadValueIdCollection(); + + foreach (uint attributeId in attributes.Keys) + { + ReadValueId itemToRead = new ReadValueId(); + + itemToRead.NodeId = nodeId; + itemToRead.AttributeId = attributeId; + + itemsToRead.Add(itemToRead); + } + + // read from server. + DataValueCollection values = null; + DiagnosticInfoCollection diagnosticInfos = null; + + ResponseHeader responseHeader = Read( + null, + 0, + TimestampsToReturn.Neither, + itemsToRead, + out values, + out diagnosticInfos); + + ClientBase.ValidateResponse(values, itemsToRead); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToRead); + + // process results. + int? nodeClass = null; + + for (int ii = 0; ii < itemsToRead.Count; ii++) + { + uint attributeId = itemsToRead[ii].AttributeId; + + // the node probably does not exist if the node class is not found. + if (attributeId == Attributes.NodeClass) + { + if (!DataValue.IsGood(values[ii])) + { + throw ServiceResultException.Create(values[ii].StatusCode, ii, diagnosticInfos, responseHeader.StringTable); + } + + // check for valid node class. + nodeClass = values[ii].Value as int?; + + if (nodeClass == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Node does not have a valid value for NodeClass: {0}.", values[ii].Value); + } + } + else + { + if (!DataValue.IsGood(values[ii])) + { + // check for unsupported attributes. + if (values[ii].StatusCode == StatusCodes.BadAttributeIdInvalid) + { + continue; + } + + // all supported attributes must be readable. + if (attributeId != Attributes.Value) + { + throw ServiceResultException.Create(values[ii].StatusCode, ii, diagnosticInfos, responseHeader.StringTable); + } + } + } + + attributes[attributeId] = values[ii]; + } + + Node node = null; + DataValue value = null; + + switch ((NodeClass)nodeClass.Value) + { + default: + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Node does not have a valid value for NodeClass: {0}.", nodeClass.Value); + } + + case NodeClass.Object: + { + ObjectNode objectNode = new ObjectNode(); + + value = attributes[Attributes.EventNotifier]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Object does not support the EventNotifier attribute."); + } + + objectNode.EventNotifier = (byte)attributes[Attributes.EventNotifier].GetValue(typeof(byte)); + node = objectNode; + break; + } + + case NodeClass.ObjectType: + { + ObjectTypeNode objectTypeNode = new ObjectTypeNode(); + + value = attributes[Attributes.IsAbstract]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "ObjectType does not support the IsAbstract attribute."); + } + + objectTypeNode.IsAbstract = (bool)attributes[Attributes.IsAbstract].GetValue(typeof(bool)); + node = objectTypeNode; + break; + } + + case NodeClass.Variable: + { + VariableNode variableNode = new VariableNode(); + + // DataType Attribute + value = attributes[Attributes.DataType]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Variable does not support the DataType attribute."); + } + + variableNode.DataType = (NodeId)attributes[Attributes.DataType].GetValue(typeof(NodeId)); + + // ValueRank Attribute + value = attributes[Attributes.ValueRank]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Variable does not support the ValueRank attribute."); + } + + variableNode.ValueRank = (int)attributes[Attributes.ValueRank].GetValue(typeof(int)); + + // ArrayDimensions Attribute + value = attributes[Attributes.ArrayDimensions]; + + if (value != null) + { + if (value.Value == null) + { + variableNode.ArrayDimensions = new uint[0]; + } + else + { + variableNode.ArrayDimensions = (uint[])value.GetValue(typeof(uint[])); + } + } + + // AccessLevel Attribute + value = attributes[Attributes.AccessLevel]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Variable does not support the AccessLevel attribute."); + } + + variableNode.AccessLevel = (byte)attributes[Attributes.AccessLevel].GetValue(typeof(byte)); + + // UserAccessLevel Attribute + value = attributes[Attributes.UserAccessLevel]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Variable does not support the UserAccessLevel attribute."); + } + + variableNode.UserAccessLevel = (byte)attributes[Attributes.UserAccessLevel].GetValue(typeof(byte)); + + // Historizing Attribute + value = attributes[Attributes.Historizing]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Variable does not support the Historizing attribute."); + } + + variableNode.Historizing = (bool)attributes[Attributes.Historizing].GetValue(typeof(bool)); + + // MinimumSamplingInterval Attribute + value = attributes[Attributes.MinimumSamplingInterval]; + + if (value != null) + { + variableNode.MinimumSamplingInterval = Convert.ToDouble(attributes[Attributes.MinimumSamplingInterval].Value); + } + + node = variableNode; + break; + } + + case NodeClass.VariableType: + { + VariableTypeNode variableTypeNode = new VariableTypeNode(); + + // IsAbstract Attribute + value = attributes[Attributes.IsAbstract]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "VariableType does not support the IsAbstract attribute."); + } + + variableTypeNode.IsAbstract = (bool)attributes[Attributes.IsAbstract].GetValue(typeof(bool)); + + // DataType Attribute + value = attributes[Attributes.DataType]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "VariableType does not support the DataType attribute."); + } + + variableTypeNode.DataType = (NodeId)attributes[Attributes.DataType].GetValue(typeof(NodeId)); + + // ValueRank Attribute + value = attributes[Attributes.ValueRank]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "VariableType does not support the ValueRank attribute."); + } + + variableTypeNode.ValueRank = (int)attributes[Attributes.ValueRank].GetValue(typeof(int)); + + // ArrayDimensions Attribute + value = attributes[Attributes.ArrayDimensions]; + + if (value != null && value.Value != null) + { + variableTypeNode.ArrayDimensions = (uint[])attributes[Attributes.ArrayDimensions].GetValue(typeof(uint[])); + } + + node = variableTypeNode; + break; + } + + case NodeClass.Method: + { + MethodNode methodNode = new MethodNode(); + + // Executable Attribute + value = attributes[Attributes.Executable]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Method does not support the Executable attribute."); + } + + methodNode.Executable = (bool)attributes[Attributes.Executable].GetValue(typeof(bool)); + + // UserExecutable Attribute + value = attributes[Attributes.UserExecutable]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Method does not support the UserExecutable attribute."); + } + + methodNode.UserExecutable = (bool)attributes[Attributes.UserExecutable].GetValue(typeof(bool)); + + node = methodNode; + break; + } + + case NodeClass.DataType: + { + DataTypeNode dataTypeNode = new DataTypeNode(); + + // IsAbstract Attribute + value = attributes[Attributes.IsAbstract]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "DataType does not support the IsAbstract attribute."); + } + + dataTypeNode.IsAbstract = (bool)attributes[Attributes.IsAbstract].GetValue(typeof(bool)); + + node = dataTypeNode; + break; + } + + case NodeClass.ReferenceType: + { + ReferenceTypeNode referenceTypeNode = new ReferenceTypeNode(); + + // IsAbstract Attribute + value = attributes[Attributes.IsAbstract]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "ReferenceType does not support the IsAbstract attribute."); + } + + referenceTypeNode.IsAbstract = (bool)attributes[Attributes.IsAbstract].GetValue(typeof(bool)); + + // Symmetric Attribute + value = attributes[Attributes.Symmetric]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "ReferenceType does not support the Symmetric attribute."); + } + + referenceTypeNode.Symmetric = (bool)attributes[Attributes.IsAbstract].GetValue(typeof(bool)); + + // InverseName Attribute + value = attributes[Attributes.InverseName]; + + if (value != null && value.Value != null) + { + referenceTypeNode.InverseName = (LocalizedText)attributes[Attributes.InverseName].GetValue(typeof(LocalizedText)); + } + + node = referenceTypeNode; + break; + } + + case NodeClass.View: + { + ViewNode viewNode = new ViewNode(); + + // EventNotifier Attribute + value = attributes[Attributes.EventNotifier]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "View does not support the EventNotifier attribute."); + } + + viewNode.EventNotifier = (byte)attributes[Attributes.EventNotifier].GetValue(typeof(byte)); + + // ContainsNoLoops Attribute + value = attributes[Attributes.ContainsNoLoops]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "View does not support the ContainsNoLoops attribute."); + } + + viewNode.ContainsNoLoops = (bool)attributes[Attributes.ContainsNoLoops].GetValue(typeof(bool)); + + node = viewNode; + break; + } + } + + // NodeId Attribute + value = attributes[Attributes.NodeId]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Node does not support the NodeId attribute."); + } + + node.NodeId = (NodeId)attributes[Attributes.NodeId].GetValue(typeof(NodeId)); + node.NodeClass = (NodeClass)nodeClass.Value; + + // BrowseName Attribute + value = attributes[Attributes.BrowseName]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Node does not support the BrowseName attribute."); + } + + node.BrowseName = (QualifiedName)attributes[Attributes.BrowseName].GetValue(typeof(QualifiedName)); + + // DisplayName Attribute + value = attributes[Attributes.DisplayName]; + + if (value == null) + { + throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Node does not support the DisplayName attribute."); + } + + node.DisplayName = (LocalizedText)attributes[Attributes.DisplayName].GetValue(typeof(LocalizedText)); + + // Description Attribute + value = attributes[Attributes.Description]; + + if (value != null && value.Value != null) + { + node.Description = (LocalizedText)attributes[Attributes.Description].GetValue(typeof(LocalizedText)); + } + + // WriteMask Attribute + value = attributes[Attributes.WriteMask]; + + if (value != null) + { + node.WriteMask = (uint)attributes[Attributes.WriteMask].GetValue(typeof(uint)); + } + + // UserWriteMask Attribute + value = attributes[Attributes.UserWriteMask]; + + if (value != null) + { + node.WriteMask = (uint)attributes[Attributes.UserWriteMask].GetValue(typeof(uint)); + } + + return node; + } + + /// + /// Reads the value for a node. + /// + /// The node Id. + /// + public DataValue ReadValue(NodeId nodeId) + { + ReadValueId itemToRead = new ReadValueId(); + + itemToRead.NodeId = nodeId; + itemToRead.AttributeId = Attributes.Value; + + ReadValueIdCollection itemsToRead = new ReadValueIdCollection(); + itemsToRead.Add(itemToRead); + + // read from server. + DataValueCollection values = null; + DiagnosticInfoCollection diagnosticInfos = null; + + ResponseHeader responseHeader = Read( + null, + 0, + TimestampsToReturn.Both, + itemsToRead, + out values, + out diagnosticInfos); + + ClientBase.ValidateResponse(values, itemsToRead); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToRead); + + if (StatusCode.IsBad(values[0].StatusCode)) + { + ServiceResult result = ClientBase.GetResult(values[0].StatusCode, 0, diagnosticInfos, responseHeader); + throw new ServiceResultException(result); + } + + return values[0]; + } + + /// + /// Reads the value for a node an checks that it is the specified type. + /// + /// The node id. + /// The expected type. + /// + public object ReadValue(NodeId nodeId, Type expectedType) + { + DataValue dataValue = ReadValue(nodeId); + + object value = dataValue.Value; + + if (expectedType != null) + { + ExtensionObject extension = value as ExtensionObject; + + if (extension != null) + { + value = extension.Body; + } + + if (!expectedType.IsInstanceOfType(value)) + { + throw ServiceResultException.Create( + StatusCodes.BadTypeMismatch, + "Server returned value unexpected type: {0}", + (value != null) ? value.GetType().Name : "(null)"); + } + } + + return value; + } + + + /// + /// Fetches all references for the specified node. + /// + /// The node id. + /// + public ReferenceDescriptionCollection FetchReferences(NodeId nodeId) + { + // browse for all references. + byte[] continuationPoint; + ReferenceDescriptionCollection descriptions; + + Browse( + null, + null, + nodeId, + 0, + BrowseDirection.Both, + null, + true, + 0, + out continuationPoint, + out descriptions); + + // process any continuation point. + while (continuationPoint != null) + { + byte[] revisedContinuationPoint; + ReferenceDescriptionCollection additionalDescriptions; + + BrowseNext( + null, + false, + continuationPoint, + out revisedContinuationPoint, + out additionalDescriptions); + + continuationPoint = revisedContinuationPoint; + + descriptions.AddRange(additionalDescriptions); + } + + return descriptions; + } + + /// + /// Establishes a session with the server. + /// + /// The name to assign to the session. + /// The user identity. + public void Open( + string sessionName, + IUserIdentity identity) + { + Open(sessionName, 0, identity, null); + } + + /// + /// Establishes a session with the server. + /// + /// The name to assign to the session. + /// The session timeout. + /// The user identity. + /// The list of preferred locales. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")] + public void Open( + string sessionName, + uint sessionTimeout, + IUserIdentity identity, + IList preferredLocales) + { + // check connection state. + lock (SyncRoot) + { + if (Connected) + { + throw new ServiceResultException(StatusCodes.BadInvalidState, "Already connected to server."); + } + } + + string securityPolicyUri = m_endpoint.Description.SecurityPolicyUri; + + // get the identity token. + if (identity == null) + { + identity = new UserIdentity(); + } + + // get identity token. + UserIdentityToken identityToken = identity.GetIdentityToken(); + + // check that the user identity is supported by the endpoint. + UserTokenPolicy identityPolicy = m_endpoint.Description.FindUserTokenPolicy(identityToken.PolicyId); + + if (identityPolicy == null) + { + // try looking up by TokenType if the policy id was not found. + identityPolicy = m_endpoint.Description.FindUserTokenPolicy(identity.TokenType, identity.IssuedTokenType); + + if (identityPolicy == null) + { + throw ServiceResultException.Create( + StatusCodes.BadUserAccessDenied, + "Endpoint does not supported the user identity type provided."); + } + + identityToken.PolicyId = identityPolicy.PolicyId; + } + + bool requireEncryption = securityPolicyUri != SecurityPolicies.None; + if (!requireEncryption) + { + requireEncryption = identityPolicy.SecurityPolicyUri != SecurityPolicies.None; + } + + // validate the server certificate. + X509Certificate2 serverCertificate = null; + byte[] certificateData = m_endpoint.Description.ServerCertificate; + + if (certificateData != null && certificateData.Length > 0 && requireEncryption) + { + serverCertificate = Utils.ParseCertificateBlob(certificateData); + m_configuration.CertificateValidator.Validate(serverCertificate); + } + + // create a nonce. + uint length = (uint)m_configuration.SecurityConfiguration.NonceLength; + byte[] clientNonce = new byte[length]; + IBuffer buffer = CryptographicBuffer.GenerateRandom(length); + CryptographicBuffer.CopyToByteArray(buffer, out clientNonce); + + NodeId sessionId = null; + NodeId sessionCookie = null; + byte[] serverNonce = new byte[0]; + byte[] serverCertificateData = new byte[0]; + SignatureData serverSignature = null; + EndpointDescriptionCollection serverEndpoints = null; + SignedSoftwareCertificateCollection serverSoftwareCertificates = null; + + // send the application instance certificate for the client. + byte[] clientCertificateData = m_instanceCertificate != null ? m_instanceCertificate.RawData : null; + + ApplicationDescription clientDescription = new ApplicationDescription(); + + clientDescription.ApplicationUri = m_configuration.ApplicationUri; + clientDescription.ApplicationName = m_configuration.ApplicationName; + clientDescription.ApplicationType = ApplicationType.Client; + clientDescription.ProductUri = m_configuration.ProductUri; + + if (sessionTimeout == 0) + { + sessionTimeout = (uint)m_configuration.ClientConfiguration.DefaultSessionTimeout; + } + + bool successCreateSession = false; + //if security none, first try to connect without certificate + if (m_endpoint.Description.SecurityPolicyUri == SecurityPolicies.None) + { + //first try to connect with client certificate NULL + try + { + CreateSession( + null, + clientDescription, + m_endpoint.Description.Server.ApplicationUri, + m_endpoint.EndpointUrl.ToString(), + sessionName, + clientNonce, + null, + sessionTimeout, + (uint)MessageContext.MaxMessageSize, + out sessionId, + out sessionCookie, + out m_sessionTimeout, + out serverNonce, + out serverCertificateData, + out serverEndpoints, + out serverSoftwareCertificates, + out serverSignature, + out m_maxRequestMessageSize); + + successCreateSession = true; + } + catch (Exception ex) + { + Utils.Trace("Create session failed with client certificate NULL. " + ex.Message); + successCreateSession = false; + } + } + + if (!successCreateSession) + { + CreateSession( + null, + clientDescription, + m_endpoint.Description.Server.ApplicationUri, + m_endpoint.EndpointUrl.ToString(), + sessionName, + clientNonce, + clientCertificateData, + sessionTimeout, + (uint)MessageContext.MaxMessageSize, + out sessionId, + out sessionCookie, + out m_sessionTimeout, + out serverNonce, + out serverCertificateData, + out serverEndpoints, + out serverSoftwareCertificates, + out serverSignature, + out m_maxRequestMessageSize); + + } + // save session id. + lock (SyncRoot) + { + base.SessionCreated(sessionId, sessionCookie); + } + + //we need to call CloseSession if CreateSession was successful but some other exception is thrown + try + { + + // verify that the server returned the same instance certificate. + if (serverCertificateData != null && !Utils.IsEqual(serverCertificateData, m_endpoint.Description.ServerCertificate)) + { + throw ServiceResultException.Create( + StatusCodes.BadCertificateInvalid, + "Server did not return the certificate used to create the secure channel."); + } + + // find the matching description (TBD - check domains against certificate). + bool found = false; + Uri expectedUrl = Utils.ParseUri(m_endpoint.Description.EndpointUrl); + + if (expectedUrl != null) + { + for (int ii = 0; ii < serverEndpoints.Count; ii++) + { + EndpointDescription serverEndpoint = serverEndpoints[ii]; + Uri actualUrl = Utils.ParseUri(serverEndpoint.EndpointUrl); + + if (actualUrl != null && actualUrl.Scheme == expectedUrl.Scheme) + { + if (serverEndpoint.SecurityPolicyUri == m_endpoint.Description.SecurityPolicyUri) + { + if (serverEndpoint.SecurityMode == m_endpoint.Description.SecurityMode) + { + // ensure endpoint has up to date information. + m_endpoint.Description.Server.ApplicationName = serverEndpoint.Server.ApplicationName; + m_endpoint.Description.Server.ApplicationUri = serverEndpoint.Server.ApplicationUri; + m_endpoint.Description.Server.ApplicationType = serverEndpoint.Server.ApplicationType; + m_endpoint.Description.Server.ProductUri = serverEndpoint.Server.ProductUri; + m_endpoint.Description.TransportProfileUri = serverEndpoint.TransportProfileUri; + m_endpoint.Description.UserIdentityTokens = serverEndpoint.UserIdentityTokens; + + found = true; + break; + } + } + } + } + } + + // could be a security risk. + if (!found) + { + throw ServiceResultException.Create( + StatusCodes.BadSecurityChecksFailed, + "Server did not return an EndpointDescription that matched the one used to create the secure channel."); + } + + // validate the server's signature. + byte[] dataToSign = Utils.Append(clientCertificateData, clientNonce); + + if (!SecurityPolicies.Verify(serverCertificate, m_endpoint.Description.SecurityPolicyUri, dataToSign, serverSignature)) + { + throw ServiceResultException.Create( + StatusCodes.BadApplicationSignatureInvalid, + "Server did not provide a correct signature for the nonce data provided by the client."); + } + + // get a validator to check certificates provided by server. + CertificateValidator validator = m_configuration.CertificateValidator; + + // validate software certificates. + List softwareCertificates = new List(); + + foreach (SignedSoftwareCertificate signedCertificate in serverSoftwareCertificates) + { + SoftwareCertificate softwareCertificate = null; + + ServiceResult result = SoftwareCertificate.Validate( + validator, + signedCertificate.CertificateData, + out softwareCertificate); + + if (ServiceResult.IsBad(result)) + { + OnSoftwareCertificateError(signedCertificate, result); + } + + softwareCertificates.Add(softwareCertificate); + } + + // check if software certificates meet application requirements. + ValidateSoftwareCertificates(softwareCertificates); + + // create the client signature. + dataToSign = Utils.Append(serverCertificateData, serverNonce); + SignatureData clientSignature = SecurityPolicies.Sign(m_instanceCertificate, securityPolicyUri, dataToSign); + + // select the security policy for the user token. + securityPolicyUri = identityPolicy.SecurityPolicyUri; + + if (String.IsNullOrEmpty(securityPolicyUri)) + { + securityPolicyUri = m_endpoint.Description.SecurityPolicyUri; + } + + // sign data with user token. + SignatureData userTokenSignature = identityToken.Sign(dataToSign, securityPolicyUri); + + // encrypt token. + identityToken.Encrypt(serverCertificate, serverNonce, securityPolicyUri); + + // send the software certificates assigned to the client. + SignedSoftwareCertificateCollection clientSoftwareCertificates = GetSoftwareCertificates(); + + // copy the preferred locales if provided. + if (preferredLocales != null && preferredLocales.Count > 0) + { + m_preferredLocales = new StringCollection(preferredLocales); + } + + StatusCodeCollection certificateResults = null; + DiagnosticInfoCollection certificateDiagnosticInfos = null; + + // activate session. + ActivateSession( + null, + clientSignature, + clientSoftwareCertificates, + m_preferredLocales, + new ExtensionObject(identityToken), + userTokenSignature, + out serverNonce, + out certificateResults, + out certificateDiagnosticInfos); + + // fetch namespaces. + FetchNamespaceTables(); + + lock (SyncRoot) + { + // save nonces. + m_sessionName = sessionName; + m_identity = identity; + m_serverNonce = serverNonce; + m_serverCertificate = serverCertificate; + + // update system context. + m_systemContext.PreferredLocales = m_preferredLocales; + m_systemContext.SessionId = this.SessionId; + m_systemContext.UserIdentity = identity; + } + + // start keep alive thread. + StartKeepAliveTimer(); + } + catch + { + try + { + CloseSession(null, false); + CloseChannel(); + } + catch (Exception e) + { + Utils.Trace("Cleanup: CloseSession() or CloseChannel() raised exception. " + e.Message); + } + finally + { + SessionCreated(null, null); + } + + throw; + } + } + + /// + /// Updates the preferred locales used for the session. + /// + /// The preferred locales. + public void ChangePreferredLocales(StringCollection preferredLocales) + { + UpdateSession(Identity, preferredLocales); + } + + /// + /// Updates the user identity and/or locales used for the session. + /// + /// The user identity. + /// The preferred locales. + public void UpdateSession(IUserIdentity identity, StringCollection preferredLocales) + { + byte[] serverNonce = null; + + lock (SyncRoot) + { + // check connection state. + if (!Connected) + { + throw new ServiceResultException(StatusCodes.BadInvalidState, "Not connected to server."); + } + + // get current nonce. + serverNonce = m_serverNonce; + + if (preferredLocales == null) + { + preferredLocales = m_preferredLocales; + } + } + + // get the identity token. + UserIdentityToken identityToken = null; + SignatureData userTokenSignature = null; + + string securityPolicyUri = m_endpoint.Description.SecurityPolicyUri; + + // create the client signature. + byte[] serverCertificateData = null; + if (m_serverCertificate != null) + { + serverCertificateData = m_serverCertificate.RawData; + } + // create the client signature. + byte[] dataToSign = Utils.Append(serverCertificateData, serverNonce); + SignatureData clientSignature = SecurityPolicies.Sign(m_instanceCertificate, securityPolicyUri, dataToSign); + + // choose a default token. + if (identity == null) + { + identity = new UserIdentity(); + } + + // check that the user identity is supported by the endpoint. + UserTokenPolicy identityPolicy = m_endpoint.Description.FindUserTokenPolicy(identity.TokenType, identity.IssuedTokenType); + + if (identityPolicy == null) + { + throw ServiceResultException.Create( + StatusCodes.BadUserAccessDenied, + "Endpoint does not supported the user identity type provided."); + } + + // select the security policy for the user token. + securityPolicyUri = identityPolicy.SecurityPolicyUri; + + if (String.IsNullOrEmpty(securityPolicyUri)) + { + securityPolicyUri = m_endpoint.Description.SecurityPolicyUri; + } + + // sign data with user token. + identityToken = identity.GetIdentityToken(); + identityToken.PolicyId = identityPolicy.PolicyId; + userTokenSignature = identityToken.Sign(dataToSign, securityPolicyUri); + + // encrypt token. + identityToken.Encrypt(m_serverCertificate, serverNonce, securityPolicyUri); + + // send the software certificates assigned to the client. + SignedSoftwareCertificateCollection clientSoftwareCertificates = GetSoftwareCertificates(); + + StatusCodeCollection certificateResults = null; + DiagnosticInfoCollection certificateDiagnosticInfos = null; + + // activate session. + ActivateSession( + null, + clientSignature, + clientSoftwareCertificates, + preferredLocales, + new ExtensionObject(identityToken), + userTokenSignature, + out serverNonce, + out certificateResults, + out certificateDiagnosticInfos); + + // save nonce and new values. + lock (SyncRoot) + { + if (identity != null) + { + m_identity = identity; + } + + m_serverNonce = serverNonce; + m_preferredLocales = preferredLocales; + + // update system context. + m_systemContext.PreferredLocales = m_preferredLocales; + m_systemContext.SessionId = this.SessionId; + m_systemContext.UserIdentity = identity; + } + } + + /// + /// Finds the NodeIds for the components for an instance. + /// + public void FindComponentIds( + NodeId instanceId, + IList componentPaths, + out NodeIdCollection componentIds, + out List errors) + { + componentIds = new NodeIdCollection(); + errors = new List(); + + // build list of paths to translate. + BrowsePathCollection pathsToTranslate = new BrowsePathCollection(); + + for (int ii = 0; ii < componentPaths.Count; ii++) + { + BrowsePath pathToTranslate = new BrowsePath(); + + pathToTranslate.StartingNode = instanceId; + pathToTranslate.RelativePath = RelativePath.Parse(componentPaths[ii], TypeTree); + + pathsToTranslate.Add(pathToTranslate); + } + + // translate the paths. + BrowsePathResultCollection results = null; + DiagnosticInfoCollection diagnosticInfos = null; + + ResponseHeader responseHeader = TranslateBrowsePathsToNodeIds( + null, + pathsToTranslate, + out results, + out diagnosticInfos); + + // 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 != UInt32.MaxValue) + { + errors[ii] = ServiceResult.Create( + StatusCodes.BadTargetNodeIdInvalid, + "Cannot follow path to external server: {0}.", + componentPaths[ii]); + + continue; + } + + if (NodeId.IsNull(results[ii].Targets[0].TargetId)) + { + 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, m_namespaceUris); + } + } + + + /// + /// Reads the values for a set of variables. + /// + /// The variable ids. + /// The expected types. + /// The list of returned values. + /// The list of returned errors. + public void ReadValues( + IList variableIds, + IList expectedTypes, + out List values, + out List errors) + { + values = new List(); + errors = new List(); + + // build list of values to read. + ReadValueIdCollection valuesToRead = new ReadValueIdCollection(); + + for (int ii = 0; ii < variableIds.Count; ii++) + { + ReadValueId valueToRead = new ReadValueId(); + + valueToRead.NodeId = variableIds[ii]; + valueToRead.AttributeId = Attributes.Value; + valueToRead.IndexRange = null; + valueToRead.DataEncoding = null; + + valuesToRead.Add(valueToRead); + } + + // read the values. + DataValueCollection results = null; + DiagnosticInfoCollection diagnosticInfos = null; + + ResponseHeader responseHeader = Read( + null, + Int32.MaxValue, + TimestampsToReturn.Both, + valuesToRead, + out results, + out diagnosticInfos); + + // verify that the server returned the correct number of results. + ClientBase.ValidateResponse(results, valuesToRead); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, valuesToRead); + + for (int ii = 0; ii < variableIds.Count; ii++) + { + values.Add(null); + errors.Add(ServiceResult.Good); + + // process any diagnostics associated with bad or uncertain data. + if (StatusCode.IsNotGood(results[ii].StatusCode)) + { + errors[ii] = new ServiceResult(results[ii].StatusCode, ii, diagnosticInfos, responseHeader.StringTable); + continue; + } + + object value = results[ii].Value; + + // extract the body from extension objects. + ExtensionObject extension = value as ExtensionObject; + + if (extension != null && extension.Body is IEncodeable) + { + value = extension.Body; + } + + // check expected type. + if (expectedTypes[ii] != null && !expectedTypes[ii].IsInstanceOfType(value)) + { + errors[ii] = ServiceResult.Create( + StatusCodes.BadTypeMismatch, + "Value {0} does not have expected type: {1}.", + value, + expectedTypes[ii].Name); + + continue; + } + + // suitable value found. + values[ii] = value; + } + } + + + /// + /// Reads the display name for a set of Nodes. + /// + public void ReadDisplayName( + IList nodeIds, + out List displayNames, + out List errors) + { + displayNames = new List(); + errors = new List(); + + // build list of values to read. + ReadValueIdCollection valuesToRead = new ReadValueIdCollection(); + + for (int ii = 0; ii < nodeIds.Count; ii++) + { + ReadValueId valueToRead = new ReadValueId(); + + valueToRead.NodeId = nodeIds[ii]; + valueToRead.AttributeId = Attributes.DisplayName; + valueToRead.IndexRange = null; + valueToRead.DataEncoding = null; + + valuesToRead.Add(valueToRead); + } + + // read the values. + DataValueCollection results = null; + DiagnosticInfoCollection diagnosticInfos = null; + + ResponseHeader responseHeader = Read( + null, + Int32.MaxValue, + TimestampsToReturn.Both, + valuesToRead, + out results, + out diagnosticInfos); + + // verify that the server returned the correct number of results. + ClientBase.ValidateResponse(results, valuesToRead); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, valuesToRead); + + for (int ii = 0; ii < nodeIds.Count; ii++) + { + displayNames.Add(String.Empty); + errors.Add(ServiceResult.Good); + + // process any diagnostics associated with bad or uncertain data. + if (StatusCode.IsNotGood(results[ii].StatusCode)) + { + errors[ii] = new ServiceResult(results[ii].StatusCode, ii, diagnosticInfos, responseHeader.StringTable); + continue; + } + + // extract the name. + LocalizedText displayName = results[ii].GetValue(null); + + if (!LocalizedText.IsNullOrEmpty(displayName)) + { + displayNames[ii] = displayName.Text; + } + } + } + + /// + /// Disconnects from the server and frees any network resources. + /// + public override StatusCode Close() + { + return Close(m_keepAliveInterval); + } + + /// + /// Disconnects from the server and frees any network resources with the specified timeout. + /// + public virtual StatusCode Close(int timeout) + { + // check if already called. + if (Disposed) + { + return StatusCodes.Good; + } + + StatusCode result = StatusCodes.Good; + + // stop the keep alive timer. + if (m_keepAliveTimer != null) + { + m_keepAliveTimer.Dispose(); + m_keepAliveTimer = null; + } + + // check if currectly connected. + bool connected = Connected; + + // halt all background threads. + if (connected) + { + if (m_SessionClosing != null) + { + try + { + m_SessionClosing(this, null); + } + catch (Exception e) + { + Utils.Trace(e, "Session: Unexpected eror raising SessionClosing event."); + } + } + } + + // close the session with the server. + if (connected && !KeepAliveStopped) + { + int existingTimeout = this.OperationTimeout; + + try + { + // close the session and delete all subscriptions. + this.OperationTimeout = timeout; + CloseSession(null, true); + this.OperationTimeout = existingTimeout; + + CloseChannel(); + + // raised notification indicating the session is closed. + SessionCreated(null, null); + } + catch (Exception e) + { + // dont throw errors on disconnect, but return them + // so the caller can log the error. + if (e is ServiceResultException) + { + result = ((ServiceResultException)e).StatusCode; + } + else + { + result = StatusCodes.Bad; + } + } + } + + // clean up. + Dispose(); + return result; + } + + /// + /// Adds a subscription to the session. + /// + /// The subscription to add. + /// + public bool AddSubscription(Subscription subscription) + { + if (subscription == null) throw new ArgumentNullException("subscription"); + + lock (SyncRoot) + { + if (m_subscriptions.Contains(subscription)) + { + return false; + } + + subscription.Session = this; + m_subscriptions.Add(subscription); + } + + if (m_SubscriptionsChanged != null) + { + m_SubscriptionsChanged(this, null); + } + + return true; + } + + /// + /// Removes a subscription from the session. + /// + /// The subscription to remove. + /// + public bool RemoveSubscription(Subscription subscription) + { + if (subscription == null) throw new ArgumentNullException("subscription"); + + if (subscription.Created) + { + subscription.Delete(true); + } + + lock (SyncRoot) + { + if (!m_subscriptions.Remove(subscription)) + { + return false; + } + + subscription.Session = null; + } + + if (m_SubscriptionsChanged != null) + { + m_SubscriptionsChanged(this, null); + } + + return true; + } + + /// + /// Removes a list of subscriptions from the sessiont. + /// + /// The list of subscriptions to remove. + /// + public bool RemoveSubscriptions(IEnumerable subscriptions) + { + if (subscriptions == null) throw new ArgumentNullException("subscriptions"); + + bool removed = false; + List subscriptionsToDelete = new List(); + + lock (SyncRoot) + { + foreach (Subscription subscription in subscriptions) + { + if (m_subscriptions.Remove(subscription)) + { + if (subscription.Created) + { + subscriptionsToDelete.Add(subscription); + } + + removed = true; + } + } + } + + foreach (Subscription subscription in subscriptionsToDelete) + { + subscription.Delete(true); + } + + if (removed) + { + if (m_SubscriptionsChanged != null) + { + m_SubscriptionsChanged(this, null); + } + } + + return true; + } + + #region Browse Methods + /// + /// Invokes the Browse service. + /// + /// The request header. + /// The view to browse. + /// The node to browse. + /// The maximum number of returned values. + /// The browse direction. + /// The reference type id. + /// If set to true the subtypes of the ReferenceType will be included in the browse. + /// The node class mask. + /// The continuation point. + /// The list of node references. + /// + public virtual ResponseHeader Browse( + RequestHeader requestHeader, + ViewDescription view, + NodeId nodeToBrowse, + uint maxResultsToReturn, + BrowseDirection browseDirection, + NodeId referenceTypeId, + bool includeSubtypes, + uint nodeClassMask, + out byte[] continuationPoint, + out ReferenceDescriptionCollection references) + { + BrowseDescription description = new BrowseDescription(); + + description.NodeId = nodeToBrowse; + description.BrowseDirection = browseDirection; + description.ReferenceTypeId = referenceTypeId; + description.IncludeSubtypes = includeSubtypes; + description.NodeClassMask = nodeClassMask; + description.ResultMask = (uint)BrowseResultMask.All; + + BrowseDescriptionCollection nodesToBrowse = new BrowseDescriptionCollection(); + nodesToBrowse.Add(description); + + BrowseResultCollection results; + DiagnosticInfoCollection diagnosticInfos; + + ResponseHeader responseHeader = Browse( + requestHeader, + view, + maxResultsToReturn, + nodesToBrowse, + out results, + out diagnosticInfos); + + ClientBase.ValidateResponse(results, nodesToBrowse); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToBrowse); + + if (StatusCode.IsBad(results[0].StatusCode)) + { + throw new ServiceResultException(new ServiceResult(results[0].StatusCode, 0, diagnosticInfos, responseHeader.StringTable)); + } + + continuationPoint = results[0].ContinuationPoint; + references = results[0].References; + + return responseHeader; + } + + /// + /// Begins an asynchronous invocation of the Browse service. + /// + /// The request header. + /// The view to browse. + /// The node to browse. + /// The maximum number of returned values.. + /// The browse direction. + /// The reference type id. + /// If set to true the subtypes of the ReferenceType will be included in the browse. + /// The node class mask. + /// The callback. + /// + /// + public IAsyncResult BeginBrowse( + RequestHeader requestHeader, + ViewDescription view, + NodeId nodeToBrowse, + uint maxResultsToReturn, + BrowseDirection browseDirection, + NodeId referenceTypeId, + bool includeSubtypes, + uint nodeClassMask, + AsyncCallback callback, + object asyncState) + { + BrowseDescription description = new BrowseDescription(); + + description.NodeId = nodeToBrowse; + description.BrowseDirection = browseDirection; + description.ReferenceTypeId = referenceTypeId; + description.IncludeSubtypes = includeSubtypes; + description.NodeClassMask = nodeClassMask; + description.ResultMask = (uint)BrowseResultMask.All; + + BrowseDescriptionCollection nodesToBrowse = new BrowseDescriptionCollection(); + nodesToBrowse.Add(description); + + return BeginBrowse( + requestHeader, + view, + maxResultsToReturn, + nodesToBrowse, + callback, + asyncState); + } + + /// + /// Finishes an asynchronous invocation of the Browse service. + /// + /// The result. + /// The continuation point. + /// The list of node references. + /// + public ResponseHeader EndBrowse( + IAsyncResult result, + out byte[] continuationPoint, + out ReferenceDescriptionCollection references) + { + BrowseResultCollection results; + DiagnosticInfoCollection diagnosticInfos; + + ResponseHeader responseHeader = EndBrowse( + result, + out results, + out diagnosticInfos); + + if (results == null || results.Count != 1) + { + throw new ServiceResultException(StatusCodes.BadUnknownResponse); + } + + if (StatusCode.IsBad(results[0].StatusCode)) + { + throw new ServiceResultException(new ServiceResult(results[0].StatusCode, 0, diagnosticInfos, responseHeader.StringTable)); + } + + continuationPoint = results[0].ContinuationPoint; + references = results[0].References; + + return responseHeader; + } + #endregion + + #region BrowseNext Methods + /// + /// Invokes the BrowseNext service. + /// + public virtual ResponseHeader BrowseNext( + RequestHeader requestHeader, + bool releaseContinuationPoint, + byte[] continuationPoint, + out byte[] revisedContinuationPoint, + out ReferenceDescriptionCollection references) + { + ByteStringCollection continuationPoints = new ByteStringCollection(); + continuationPoints.Add(continuationPoint); + + BrowseResultCollection results; + DiagnosticInfoCollection diagnosticInfos; + + ResponseHeader responseHeader = BrowseNext( + requestHeader, + releaseContinuationPoint, + continuationPoints, + out results, + out diagnosticInfos); + + ClientBase.ValidateResponse(results, continuationPoints); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, continuationPoints); + + if (StatusCode.IsBad(results[0].StatusCode)) + { + throw new ServiceResultException(new ServiceResult(results[0].StatusCode, 0, diagnosticInfos, responseHeader.StringTable)); + } + + revisedContinuationPoint = results[0].ContinuationPoint; + references = results[0].References; + + return responseHeader; + } + + /// + /// Begins an asynchronous invocation of the BrowseNext service. + /// + public IAsyncResult BeginBrowseNext( + RequestHeader requestHeader, + bool releaseContinuationPoint, + byte[] continuationPoint, + AsyncCallback callback, + object asyncState) + { + ByteStringCollection continuationPoints = new ByteStringCollection(); + continuationPoints.Add(continuationPoint); + + return BeginBrowseNext( + requestHeader, + releaseContinuationPoint, + continuationPoints, + callback, + asyncState); + } + + /// + /// Finishes an asynchronous invocation of the BrowseNext service. + /// + public ResponseHeader EndBrowseNext( + IAsyncResult result, + out byte[] revisedContinuationPoint, + out ReferenceDescriptionCollection references) + { + BrowseResultCollection results; + DiagnosticInfoCollection diagnosticInfos; + + ResponseHeader responseHeader = EndBrowseNext( + result, + out results, + out diagnosticInfos); + + if (results == null || results.Count != 1) + { + throw new ServiceResultException(StatusCodes.BadUnknownResponse); + } + + if (StatusCode.IsBad(results[0].StatusCode)) + { + throw new ServiceResultException(new ServiceResult(results[0].StatusCode, 0, diagnosticInfos, responseHeader.StringTable)); + } + + revisedContinuationPoint = results[0].ContinuationPoint; + references = results[0].References; + + return responseHeader; + } + #endregion + + /// + /// Calls the specified method and returns the output arguments. + /// + /// The NodeId of the object that provides the method. + /// The NodeId of the method to call. + /// The input arguments. + /// The list of output argument values. + public IList Call(NodeId objectId, NodeId methodId, params object[] args) + { + VariantCollection inputArguments = new VariantCollection(); + + if (args != null) + { + for (int ii = 0; ii < args.Length; ii++) + { + inputArguments.Add(new Variant(args[ii])); + } + } + + CallMethodRequest request = new CallMethodRequest(); + + request.ObjectId = objectId; + request.MethodId = methodId; + request.InputArguments = inputArguments; + + CallMethodRequestCollection requests = new CallMethodRequestCollection(); + requests.Add(request); + + CallMethodResultCollection results; + DiagnosticInfoCollection diagnosticInfos; + + ResponseHeader responseHeader = Call( + null, + requests, + out results, + out diagnosticInfos); + + ClientBase.ValidateResponse(results, requests); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, requests); + + if (StatusCode.IsBad(results[0].StatusCode)) + { + throw ServiceResultException.Create(results[0].StatusCode, 0, diagnosticInfos, responseHeader.StringTable); + } + + List outputArguments = new List(); + + foreach (Variant arg in results[0].OutputArguments) + { + outputArguments.Add(arg.Value); + } + + return outputArguments; + } + #endregion + + #region Protected Methods + /// + /// Returns the software certificates assigned to the application. + /// + protected virtual SignedSoftwareCertificateCollection GetSoftwareCertificates() + { + return new SignedSoftwareCertificateCollection(); + } + + /// + /// Handles an error when validating the application instance certificate provided by the server. + /// + protected virtual void OnApplicationCertificateError(byte[] serverCertificate, ServiceResult result) + { + throw new ServiceResultException(result); + } + + /// + /// Handles an error when validating software certificates provided by the server. + /// + protected virtual void OnSoftwareCertificateError(SignedSoftwareCertificate signedCertificate, ServiceResult result) + { + throw new ServiceResultException(result); + } + + /// + /// Inspects the software certificates provided by the server. + /// + protected virtual void ValidateSoftwareCertificates(List softwareCertificates) + { + // always accept valid certificates. + } + + /// + /// Starts a timer to check that the connection to the server is still available. + /// + private void StartKeepAliveTimer() + { + int keepAliveInterval = m_keepAliveInterval; + + lock (m_eventLock) + { + m_serverState = ServerState.Unknown; + m_lastKeepAliveTime = DateTime.UtcNow; + } + + ReadValueIdCollection nodesToRead = new ReadValueIdCollection(); + + // read the server state. + ReadValueId serverState = new ReadValueId(); + + serverState.NodeId = Variables.Server_ServerStatus_State; + serverState.AttributeId = Attributes.Value; + serverState.DataEncoding = null; + serverState.IndexRange = null; + + nodesToRead.Add(serverState); + + // restart the publish timer. + lock (SyncRoot) + { + if (m_keepAliveTimer != null) + { + m_keepAliveTimer.Dispose(); + m_keepAliveTimer = null; + } + + // start timer. + m_keepAliveTimer = new Timer(OnKeepAlive, nodesToRead, keepAliveInterval, keepAliveInterval); + } + + // send initial keep alive. + OnKeepAlive(nodesToRead); + } + + /// + /// Removes a completed async request. + /// + private AsyncRequestState RemoveRequest(IAsyncResult result, uint requestId, uint typeId) + { + lock (m_outstandingRequests) + { + for (LinkedListNode ii = m_outstandingRequests.First; ii != null; ii = ii.Next) + { + if (Object.ReferenceEquals(result, ii.Value.Result) || (requestId == ii.Value.RequestId && typeId == ii.Value.RequestTypeId)) + { + AsyncRequestState state = ii.Value; + m_outstandingRequests.Remove(ii); + return state; + } + } + + return null; + } + } + + /// + /// Adds a new async request. + /// + private void AsyncRequestStarted(IAsyncResult result, uint requestId, uint typeId) + { + lock (m_outstandingRequests) + { + // check if the request completed asynchronously. + AsyncRequestState state = RemoveRequest(result, requestId, typeId); + + // add a new request. + if (state == null) + { + state = new AsyncRequestState(); + + state.Defunct = false; + state.RequestId = requestId; + state.RequestTypeId = typeId; + state.Result = result; + state.Timestamp = DateTime.UtcNow; + + m_outstandingRequests.AddLast(state); + } + } + } + + /// + /// Removes a completed async request. + /// + private void AsyncRequestCompleted(IAsyncResult result, uint requestId, uint typeId) + { + lock (m_outstandingRequests) + { + // remove the request. + AsyncRequestState state = RemoveRequest(result, requestId, typeId); + + if (state != null) + { + // mark any old requests as default (i.e. the should have returned before this request). + DateTime maxAge = state.Timestamp.AddSeconds(-1); + + for (LinkedListNode ii = m_outstandingRequests.First; ii != null; ii = ii.Next) + { + if (ii.Value.RequestTypeId == typeId && ii.Value.Timestamp < maxAge) + { + ii.Value.Defunct = true; + } + } + } + + // add a dummy placeholder since the begin request has not completed yet. + if (state == null) + { + state = new AsyncRequestState(); + + state.Defunct = true; + state.RequestId = requestId; + state.RequestTypeId = typeId; + state.Result = result; + state.Timestamp = DateTime.UtcNow; + + m_outstandingRequests.AddLast(state); + } + } + } + + /// + /// Sends a keep alive by reading from the server. + /// + private void OnKeepAlive(object state) + { + ReadValueIdCollection nodesToRead = (ReadValueIdCollection)state; + + try + { + // check if session has been closed. + if (!Connected || m_keepAliveTimer == null) + { + return; + } + + // raise error if keep alives are not coming back. + if (KeepAliveStopped) + { + if (!OnKeepAliveError(ServiceResult.Create(StatusCodes.BadNoCommunication, "Server not responding to keep alive requests."))) + { + return; + } + } + + // limit the number of keep alives sent. + if (OutstandingRequestCount > SubscriptionCount + 10) + { + return; + } + + RequestHeader requestHeader = new RequestHeader(); + + requestHeader.RequestHandle = Utils.IncrementIdentifier(ref m_keepAliveCounter); + requestHeader.TimeoutHint = (uint)(KeepAliveInterval * 2); + requestHeader.ReturnDiagnostics = 0; + + IAsyncResult result = BeginRead( + requestHeader, + 0, + TimestampsToReturn.Neither, + nodesToRead, + OnKeepAliveComplete, + nodesToRead); + + AsyncRequestStarted(result, requestHeader.RequestHandle, DataTypes.ReadRequest); + } + catch (Exception e) + { + Utils.Trace("Could not send keep alive request: {1} {0}", e.Message, e.GetType().FullName); + } + } + + /// + /// Checks if a notification has arrived. Sends a publish if it has not. + /// + private void OnKeepAliveComplete(IAsyncResult result) + { + ReadValueIdCollection nodesToRead = (ReadValueIdCollection)result.AsyncState; + + AsyncRequestCompleted(result, 0, DataTypes.ReadRequest); + + try + { + // read the server status. + DataValueCollection values = new DataValueCollection(); + DiagnosticInfoCollection diagnosticInfos = new DiagnosticInfoCollection(); + + ResponseHeader responseHeader = EndRead( + result, + out values, + out diagnosticInfos); + + ValidateResponse(values, nodesToRead); + ValidateDiagnosticInfos(diagnosticInfos, nodesToRead); + + // validate value returned. + ServiceResult error = ValidateDataValue(values[0], typeof(int), 0, diagnosticInfos, responseHeader); + + if (ServiceResult.IsBad(error)) + { + throw new ServiceResultException(error); + } + + // send notification that keep alive completed. + OnKeepAlive((ServerState)(int)values[0].Value, responseHeader.Timestamp); + } + catch (Exception e) + { + Utils.Trace("Unexpected keep alive error occurred: {0}", e.Message); + } + } + + /// + /// Called when the server returns a keep alive response. + /// + protected virtual void OnKeepAlive(ServerState currentState, DateTime currentTime) + { + // restart publishing if keep alives recovered. + if (KeepAliveStopped) + { + // ignore if already reconnecting. + if (m_reconnecting) + { + return; + } + + int count = 0; + + lock (m_outstandingRequests) + { + for (LinkedListNode ii = m_outstandingRequests.First; ii != null; ii = ii.Next) + { + if (ii.Value.RequestTypeId == DataTypes.PublishRequest) + { + ii.Value.Defunct = true; + } + } + } + + lock (SyncRoot) + { + count = m_subscriptions.Count; + } + + while (count-- > 0) + { + BeginPublish(OperationTimeout); + } + } + + KeepAliveEventHandler callback = null; + + lock (m_eventLock) + { + callback = m_KeepAlive; + + // save server state. + m_serverState = currentState; + m_lastKeepAliveTime = DateTime.UtcNow; + } + + if (callback != null) + { + try + { + callback(this, new KeepAliveEventArgs(null, currentState, currentTime)); + } + catch (Exception e) + { + Utils.Trace(e, "Session: Unexpected error invoking KeepAliveCallback."); + } + } + } + + /// + /// Called when a error occurs during a keep alive. + /// + protected virtual bool OnKeepAliveError(ServiceResult result) + { + long delta = 0; + + lock (m_eventLock) + { + delta = DateTime.UtcNow.Ticks - m_lastKeepAliveTime.Ticks; + } + + Utils.Trace( + "KEEP ALIVE LATE: {0}s, EndpointUrl={1}, RequestCount={3}/{2}", + ((double)delta) / TimeSpan.TicksPerSecond, + this.Endpoint.EndpointUrl, + this.OutstandingRequestCount, + this.GoodPublishRequestCount); + + KeepAliveEventHandler callback = null; + + lock (m_eventLock) + { + callback = m_KeepAlive; + } + + if (callback != null) + { + try + { + KeepAliveEventArgs args = new KeepAliveEventArgs(result, ServerState.Unknown, DateTime.UtcNow); + callback(this, args); + return !args.CancelKeepAlive; + } + catch (Exception e) + { + Utils.Trace(e, "Session: Unexpected error invoking KeepAliveCallback."); + } + } + + return true; + } + #endregion + + #region Publish Methods + /// + /// Sends an additional publish request. + /// + public IAsyncResult BeginPublish(int timeout) + { + // do not publish if reconnecting. + if (m_reconnecting) + { + Utils.Trace("Published skipped due to reconnect"); + return null; + } + + SubscriptionAcknowledgementCollection acknowledgementsToSend = null; + + // collect the current set if acknowledgements. + lock (SyncRoot) + { + acknowledgementsToSend = m_acknowledgementsToSend; + m_acknowledgementsToSend = new SubscriptionAcknowledgementCollection(); + } + + // send publish request. + RequestHeader requestHeader = new RequestHeader(); + + // ensure the publish request is discarded before the timeout occurs to ensure the channel is dropped. + requestHeader.TimeoutHint = (uint)OperationTimeout / 2; + requestHeader.ReturnDiagnostics = (uint)(int)ReturnDiagnostics; + requestHeader.RequestHandle = Utils.IncrementIdentifier(ref m_publishCounter); + + AsyncRequestState state = new AsyncRequestState(); + + state.RequestTypeId = DataTypes.PublishRequest; + state.RequestId = requestHeader.RequestHandle; + state.Timestamp = DateTime.UtcNow; + + try + { + IAsyncResult result = BeginPublish( + requestHeader, + acknowledgementsToSend, + OnPublishComplete, + new object[] { SessionId, acknowledgementsToSend, requestHeader }); + + AsyncRequestStarted(result, requestHeader.RequestHandle, DataTypes.PublishRequest); + + Utils.Trace("PUBLISH #{0} SENT", requestHeader.RequestHandle); + + return result; + } + catch (Exception e) + { + Utils.Trace(e, "Unexpected error sending publish request."); + return null; + } + } + + /// + /// Completes an asynchronous publish operation. + /// + private void OnPublishComplete(IAsyncResult result) + { + // extract state information. + object[] state = (object[])result.AsyncState; + NodeId sessionId = (NodeId)state[0]; + SubscriptionAcknowledgementCollection acknowledgementsToSend = (SubscriptionAcknowledgementCollection)state[1]; + RequestHeader requestHeader = (RequestHeader)state[2]; + bool moreNotifications; + + AsyncRequestCompleted(result, requestHeader.RequestHandle, DataTypes.PublishRequest); + + try + { + Utils.Trace("PUBLISH #{0} RECEIVED", requestHeader.RequestHandle); + + // complete publish. + uint subscriptionId; + UInt32Collection availableSequenceNumbers; + NotificationMessage notificationMessage; + StatusCodeCollection acknowledgeResults; + DiagnosticInfoCollection acknowledgeDiagnosticInfos; + + ResponseHeader responseHeader = EndPublish( + result, + out subscriptionId, + out availableSequenceNumbers, + out moreNotifications, + out notificationMessage, + out acknowledgeResults, + out acknowledgeDiagnosticInfos); + + // nothing more to do if session changed. + if (sessionId != SessionId) + { + Utils.Trace("Publish response discarded because session id changed: Old {0} != New {1}", sessionId, SessionId); + return; + } + + Utils.Trace("NOTIFICATION RECEIVED: SubId={0}, SeqNo={1}", subscriptionId, notificationMessage.SequenceNumber); + + // process response. + ProcessPublishResponse( + responseHeader, + subscriptionId, + availableSequenceNumbers, + moreNotifications, + notificationMessage); + + // nothing more to do if reconnecting. + if (m_reconnecting) + { + Utils.Trace("No new publish sent because of reconnect in progress."); + return; + } + } + catch (Exception e) + { + Utils.Trace("Publish #{0}, Reconnecting={2}, Error: {1}", requestHeader.RequestHandle, e.Message, m_reconnecting); + + moreNotifications = false; + + // ignore errors if reconnecting. + if (m_reconnecting) + { + Utils.Trace("Publish abandoned after error due to reconnect: {0}", e.Message); + return; + } + + // nothing more to do if session changed. + if (sessionId != SessionId) + { + Utils.Trace("Publish abandoned after error because session id changed: Old {0} != New {1}", sessionId, SessionId); + return; + } + + // try to acknowlege the notifications again in the next publish. + if (acknowledgementsToSend != null) + { + lock (SyncRoot) + { + m_acknowledgementsToSend.AddRange(acknowledgementsToSend); + } + } + + // raise an error event. + ServiceResult error = new ServiceResult(e); + + if (error.Code != StatusCodes.BadNoSubscription) + { + PublishErrorEventHandler callback = null; + + lock (m_eventLock) + { + callback = m_PublishError; + } + + if (callback != null) + { + try + { + callback(this, new PublishErrorEventArgs(error)); + } + catch (Exception e2) + { + Utils.Trace(e2, "Session: Unexpected error invoking PublishErrorCallback."); + } + } + } + + // don't send another publish for these errors. + switch (error.Code) + { + case StatusCodes.BadNoSubscription: + case StatusCodes.BadSessionClosed: + case StatusCodes.BadTooManyPublishRequests: + case StatusCodes.BadServerHalted: + { + return; + } + } + + Utils.Trace(e, "PUBLISH #{0} - Unhandled error during Publish.", requestHeader.RequestHandle); + } + + int requestCount = GoodPublishRequestCount; + + if (requestCount < m_subscriptions.Count) + { + BeginPublish(OperationTimeout); + } + else + { + Utils.Trace("PUBLISH - Did not send another publish request. GoodPublishRequestCount={0}, Subscriptions={1}", requestCount, m_subscriptions.Count); + } + } + + /// + /// Sends a republish request. + /// + public bool Republish(uint subscriptionId, uint sequenceNumber) + { + // send publish request. + RequestHeader requestHeader = new RequestHeader(); + + requestHeader.TimeoutHint = (uint)OperationTimeout; + requestHeader.ReturnDiagnostics = (uint)(int)ReturnDiagnostics; + requestHeader.RequestHandle = Utils.IncrementIdentifier(ref m_publishCounter); + + try + { + Utils.Trace("Requesting Republish for {0}-{1}", subscriptionId, sequenceNumber); + + // request republish. + NotificationMessage notificationMessage = null; + + ResponseHeader responseHeader = Republish( + requestHeader, + subscriptionId, + sequenceNumber, + out notificationMessage); + + Utils.Trace("Received Republish for {0}-{1}", subscriptionId, sequenceNumber); + + // process response. + ProcessPublishResponse( + responseHeader, + subscriptionId, + null, + false, + notificationMessage); + + return true; + } + catch (Exception e) + { + ServiceResult error = new ServiceResult(e); + + bool result = (error.StatusCode == StatusCodes.BadMessageNotAvailable); + + if (result) + { + Utils.Trace("Message {0}-{1} no longer available.", subscriptionId, sequenceNumber); + } + else + { + Utils.Trace(e, "Unexpected error sending republish request."); + } + + PublishErrorEventHandler callback = null; + + lock (m_eventLock) + { + callback = m_PublishError; + } + + // raise an error event. + if (callback != null) + { + try + { + PublishErrorEventArgs args = new PublishErrorEventArgs( + error, + subscriptionId, + sequenceNumber); + + callback(this, args); + } + catch (Exception e2) + { + Utils.Trace(e2, "Session: Unexpected error invoking PublishErrorCallback."); + } + } + + return result; + } + } + + /// + /// Processes the response from a publish request. + /// + private void ProcessPublishResponse( + ResponseHeader responseHeader, + uint subscriptionId, + UInt32Collection availableSequenceNumbers, + bool moreNotifications, + NotificationMessage notificationMessage) + { + Subscription subscription = null; + + // send notification that the server is alive. + OnKeepAlive(m_serverState, responseHeader.Timestamp); + + // collect the current set if acknowledgements. + lock (SyncRoot) + { + // clear out acknowledgements for messages that the server does not have any more. + SubscriptionAcknowledgementCollection acknowledgementsToSend = new SubscriptionAcknowledgementCollection(); + + for (int ii = 0; ii < m_acknowledgementsToSend.Count; ii++) + { + SubscriptionAcknowledgement acknowledgement = m_acknowledgementsToSend[ii]; + + if (acknowledgement.SubscriptionId != subscriptionId) + { + acknowledgementsToSend.Add(acknowledgement); + } + else + { + if (availableSequenceNumbers == null || availableSequenceNumbers.Contains(acknowledgement.SequenceNumber)) + { + acknowledgementsToSend.Add(acknowledgement); + } + } + } + + // create an acknowledgement to be sent back to the server. + if (notificationMessage.NotificationData.Count > 0) + { + SubscriptionAcknowledgement acknowledgement = new SubscriptionAcknowledgement(); + + acknowledgement.SubscriptionId = subscriptionId; + acknowledgement.SequenceNumber = notificationMessage.SequenceNumber; + + acknowledgementsToSend.Add(acknowledgement); + } + + m_acknowledgementsToSend = acknowledgementsToSend; + + // find the subscription. + foreach (Subscription current in m_subscriptions) + { + if (current.Id == subscriptionId) + { + subscription = current; + break; + } + } + } + + // ignore messages with a subscription that has been deleted. + if (subscription != null) + { + // update subscription cache. + subscription.SaveMessageInCache( + availableSequenceNumbers, + notificationMessage, + responseHeader.StringTable); + + // raise the notification. + lock (m_eventLock) + { + NotificationEventArgs args = new NotificationEventArgs(subscription, notificationMessage, responseHeader.StringTable); + + if (m_Publish != null) + { + Task.Run(() => + { + OnRaisePublishNotification(args); + }); + } + } + } + } + + /// + /// Raises an event indicating that publish has returned a notification. + /// + private void OnRaisePublishNotification(object state) + { + try + { + NotificationEventArgs args = (NotificationEventArgs)state; + NotificationEventHandler callback = m_Publish; + + if (callback != null && args.Subscription.Id != 0) + { + callback(this, args); + } + } + catch (Exception e) + { + Utils.Trace(e, "Session: Unexpected rrror while raising Notification event."); + } + } + #endregion + + #region Private Fields + private SubscriptionAcknowledgementCollection m_acknowledgementsToSend; + private List m_subscriptions; + private Dictionary m_dictionaries; + private Subscription m_defaultSubscription; + private double m_sessionTimeout; + private uint m_maxRequestMessageSize; + private StringCollection m_preferredLocales; + private NamespaceTable m_namespaceUris; + private StringTable m_serverUris; + private EncodeableFactory m_factory; + private SystemContext m_systemContext; + private NodeCache m_nodeCache; + private ApplicationConfiguration m_configuration; + private ConfiguredEndpoint m_endpoint; + private X509Certificate2 m_instanceCertificate; + //private X509Certificate2Collection m_instanceCertificateChain; + private List m_identityHistory; + + private string m_sessionName; + private object m_handle; + private IUserIdentity m_identity; + private byte[] m_serverNonce; + private X509Certificate2 m_serverCertificate; + private long m_publishCounter; + private DateTime m_lastKeepAliveTime; + private ServerState m_serverState; + private int m_keepAliveInterval; + private Timer m_keepAliveTimer; + private long m_keepAliveCounter; + private bool m_reconnecting; + private LinkedList m_outstandingRequests; + + private class AsyncRequestState + { + public uint RequestTypeId; + public uint RequestId; + public DateTime Timestamp; + public IAsyncResult Result; + public bool Defunct; + } + + private object m_eventLock = new object(); + private event KeepAliveEventHandler m_KeepAlive; + private event NotificationEventHandler m_Publish; + private event PublishErrorEventHandler m_PublishError; + private event EventHandler m_SubscriptionsChanged; + private event EventHandler m_SessionClosing; + #endregion + } + + #region KeepAliveEventArgs Class + /// + /// The event arguments provided when a keep alive response arrives. + /// + public class KeepAliveEventArgs : EventArgs + { + #region Constructors + /// + /// Creates a new instance. + /// + internal KeepAliveEventArgs( + ServiceResult status, + ServerState currentState, + DateTime currentTime) + { + m_status = status; + m_currentState = currentState; + m_currentTime = currentTime; + } + #endregion + + #region Public Properties + /// + /// Gets the status associated with the keep alive operation. + /// + public ServiceResult Status + { + get { return m_status; } + } + + /// + /// Gets the current server state. + /// + public ServerState CurrentState + { + get { return m_currentState; } + } + + /// + /// Gets the current server time. + /// + public DateTime CurrentTime + { + get { return m_currentTime; } + } + + /// + /// Gets or sets a flag indicating whether the session should send another keep alive. + /// + public bool CancelKeepAlive + { + get { return m_cancelKeepAlive; } + set { m_cancelKeepAlive = value; } + } + #endregion + + #region Private Fields + private ServiceResult m_status; + private ServerState m_currentState; + private DateTime m_currentTime; + private bool m_cancelKeepAlive; + #endregion + } + + /// + /// The delegate used to receive keep alive notifications. + /// + public delegate void KeepAliveEventHandler(Session session, KeepAliveEventArgs e); + #endregion + + #region NotificationEventArgs Class + /// + /// Represents the event arguments provided when a new notification message arrives. + /// + public class NotificationEventArgs : EventArgs + { + #region Constructors + /// + /// Creates a new instance. + /// + internal NotificationEventArgs( + Subscription subscription, + NotificationMessage notificationMessage, + IList stringTable) + { + m_subscription = subscription; + m_notificationMessage = notificationMessage; + m_stringTable = stringTable; + } + #endregion + + #region Public Properties + /// + /// Gets the subscription that the notification applies to. + /// + public Subscription Subscription + { + get { return m_subscription; } + } + + /// + /// Gets the notification message. + /// + public NotificationMessage NotificationMessage + { + get { return m_notificationMessage; } + } + + /// + /// Gets the string table returned with the notification message. + /// + public IList StringTable + { + get { return m_stringTable; } + } + #endregion + + #region Private Fields + private Subscription m_subscription; + private NotificationMessage m_notificationMessage; + private IList m_stringTable; + #endregion + } + + /// + /// The delegate used to receive publish notifications. + /// + public delegate void NotificationEventHandler(Session session, NotificationEventArgs e); + #endregion + + #region PublishErrorEventArgs Class + /// + /// Represents the event arguments provided when a publish error occurs. + /// + public class PublishErrorEventArgs : EventArgs + { + #region Constructors + /// + /// Creates a new instance. + /// + internal PublishErrorEventArgs(ServiceResult status) + { + m_status = status; + } + + /// + /// Creates a new instance. + /// + internal PublishErrorEventArgs(ServiceResult status, uint subscriptionId, uint sequenceNumber) + { + m_status = status; + m_subscriptionId = subscriptionId; + m_sequenceNumber = sequenceNumber; + } + #endregion + + #region Public Properties + /// + /// Gets the status associated with the keep alive operation. + /// + public ServiceResult Status + { + get { return m_status; } + } + + /// + /// Gets the subscription with the message that could not be republished. + /// + public uint SubscriptionId + { + get { return m_subscriptionId; } + } + + /// + /// Gets the sequence number for the message that could not be republished. + /// + public uint SequenceNumber + { + get { return m_sequenceNumber; } + } + #endregion + + #region Private Fields + private uint m_subscriptionId; + private uint m_sequenceNumber; + private ServiceResult m_status; + #endregion + } + + /// + /// The delegate used to receive pubish error notifications. + /// + public delegate void PublishErrorEventHandler(Session session, PublishErrorEventArgs e); + #endregion +} diff --git a/SampleApplications/SDK/Client/SessionReconnectHandler.cs b/SampleApplications/SDK/Client/SessionReconnectHandler.cs new file mode 100644 index 0000000000..722a5b8433 --- /dev/null +++ b/SampleApplications/SDK/Client/SessionReconnectHandler.cs @@ -0,0 +1,223 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Text; +using System.Threading; +using Opc.Ua; + +namespace Opc.Ua.Client +{ + /// + /// Attempts to reconnect to the server. + /// + public class SessionReconnectHandler : IDisposable + { + #region IDisposable Members + /// + /// Frees any unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + } + + /// + /// An overrideable version of the Dispose. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + lock (m_lock) + { + if (m_reconnectTimer != null) + { + m_reconnectTimer.Dispose(); + m_reconnectTimer = null; + } + } + } + } + #endregion + + #region Public Methods + /// + /// Gets the session managed by the handler. + /// + /// The session. + public Session Session + { + get { return m_session; } + } + + /// + /// Begins the reconnect process. + /// + public void BeginReconnect(Session session, int reconnectPeriod, EventHandler callback) + { + lock (m_lock) + { + if (m_reconnectTimer != null) + { + throw new ServiceResultException(StatusCodes.BadInvalidState); + } + + m_session = session; + m_reconnectFailed = false; + m_reconnectPeriod = reconnectPeriod; + m_callback = callback; + m_reconnectTimer = new System.Threading.Timer(OnReconnect, null, reconnectPeriod, Timeout.Infinite); + } + } + #endregion + + #region Private Methods + /// + /// Called when the reconnect timer expires. + /// + private void OnReconnect(object state) + { + try + { + // check for exit. + if (m_reconnectTimer == null) + { + return; + } + + // dispose of the timer. + lock (m_lock) + { + if (m_reconnectTimer != null) + { + m_reconnectTimer.Dispose(); + m_reconnectTimer = null; + } + } + + // do the reconnect. + if (DoReconnect()) + { + // notify the caller. + m_callback(this, null); + return; + } + } + catch (Exception exception) + { + Utils.Trace(exception, "Unexpected error during reconnect."); + } + + // schedule the next reconnect. + lock (m_lock) + { + m_reconnectTimer = new System.Threading.Timer(OnReconnect, null, m_reconnectPeriod, Timeout.Infinite); + } + } + + /// + /// Reconnects to the server. + /// + private bool DoReconnect() + { + // try a reconnect. + if (!m_reconnectFailed) + { + try + { + m_session.Reconnect(); + + // monitored items should start updating on their own. + return true; + } + catch (Exception exception) + { + bool recreateNow = false; + + // recreate the session if it has been closed or the nonce is wrong. + ServiceResultException sre = exception as ServiceResultException; + + if (sre != null) + { + switch (sre.StatusCode) + { + case StatusCodes.BadSessionClosed: + case StatusCodes.BadApplicationSignatureInvalid: + { + recreateNow = true; + break; + } + + default: + { + Utils.Trace((int)Utils.TraceMasks.Error, "Unexpected RECONNECT error code. {0}", sre.StatusCode); + break; + } + } + } + + m_reconnectFailed = true; + + // try a reconnect again after a delay. + if (!recreateNow) + { + Utils.Trace("Reconnect failed. {0}", exception.Message); + return false; + } + } + } + + // re-create the session. + try + { + Session session = Session.Recreate(m_session); + m_session.Close(); + m_session = session; + return true; + } + catch (Exception exception) + { + Utils.Trace("Unexpected re-creating a Session with the UA Server. {0}", exception.Message); + return false; + } + } + #endregion + + #region Private Fields + private object m_lock = new object(); + private Session m_session; + private bool m_reconnectFailed; + private int m_reconnectPeriod; + private Timer m_reconnectTimer; + private EventHandler m_callback; + #endregion + } +} diff --git a/SampleApplications/SDK/Client/Subscription.cs b/SampleApplications/SDK/Client/Subscription.cs new file mode 100644 index 0000000000..7438b2c984 --- /dev/null +++ b/SampleApplications/SDK/Client/Subscription.cs @@ -0,0 +1,2111 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Text; +using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; + +namespace Opc.Ua.Client +{ + /// + /// A subscription + /// + [DataContract(Namespace = Namespaces.OpcUaXsd)] + public class Subscription : IDisposable + { + #region Constructors + /// + /// Creates a empty object. + /// + public Subscription() + { + Initialize(); + } + + /// + /// Initializes the subscription from a template. + /// + public Subscription(Subscription template) : this(template, false) + { + } + + /// + /// Initializes the subscription from a template. + /// + /// The template. + /// if set to true the event handlers are copied. + public Subscription(Subscription template, bool copyEventHandlers) + { + Initialize(); + + if (template != null) + { + string displayName = template.DisplayName; + + if (String.IsNullOrEmpty(displayName)) + { + displayName = m_displayName; + } + + // remove any existing numeric suffix. + int index = displayName.LastIndexOf(' '); + + if (index != -1) + { + try + { + displayName = displayName.Substring(0, index); + } + catch + { + // not a numeric suffix. + } + } + + m_displayName = Utils.Format("{0} {1}", displayName, Utils.IncrementIdentifier(ref s_globalSubscriptionCounter)); + m_publishingInterval = template.m_publishingInterval; + m_keepAliveCount = template.m_keepAliveCount; + m_lifetimeCount = template.m_lifetimeCount; + m_minLifetimeInterval = template.m_minLifetimeInterval; + m_maxNotificationsPerPublish = template.m_maxNotificationsPerPublish; + m_publishingEnabled = template.m_publishingEnabled; + m_priority = template.m_priority; + m_timestampsToReturn = template.m_timestampsToReturn; + m_maxMessageCount = template.m_maxMessageCount; + m_defaultItem = (MonitoredItem)template.m_defaultItem.MemberwiseClone(); + m_defaultItem = template.m_defaultItem; + m_handle = template.m_handle; + m_maxMessageCount = template.m_maxMessageCount; + m_disableMonitoredItemCache = template.m_disableMonitoredItemCache; + + if (copyEventHandlers) + { + m_StateChanged = template.m_StateChanged; + m_PublishStatusChanged = template.m_PublishStatusChanged; + m_fastDataChangeCallback = template.m_fastDataChangeCallback; + m_fastEventCallback = template.m_fastEventCallback; + } + + // copy the list of monitored items. + foreach (MonitoredItem monitoredItem in template.MonitoredItems) + { + MonitoredItem clone = new MonitoredItem(monitoredItem, copyEventHandlers); + clone.Subscription = this; + m_monitoredItems.Add(clone.ClientHandle, clone); + } + } + } + + /// + /// Called by the .NET framework during deserialization. + /// + [OnDeserializing] + private void Initialize(StreamingContext context) + { + m_cache = new object(); + Initialize(); + } + + /// + /// Sets the private members to default values. + /// + private void Initialize() + { + m_id = 0; + m_displayName = "Subscription"; + m_publishingInterval = 0; + m_keepAliveCount = 0; + m_lifetimeCount = 0; + m_maxNotificationsPerPublish = 0; + m_publishingEnabled = false; + m_timestampsToReturn = TimestampsToReturn.Both; + m_maxMessageCount = 10; + m_messageCache = new LinkedList(); + m_monitoredItems = new SortedDictionary(); + m_deletedItems = new List(); + + m_defaultItem = new MonitoredItem(); + + m_defaultItem.DisplayName = "MonitoredItem"; + m_defaultItem.SamplingInterval = -1; + m_defaultItem.MonitoringMode = MonitoringMode.Reporting; + m_defaultItem.QueueSize = 0; + m_defaultItem.DiscardOldest = true; + } + #endregion + + #region IDisposable Members + /// + /// Frees any unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + } + + /// + /// An overrideable version of the Dispose. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "m_publishTimer")] + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Utils.SilentDispose(m_publishTimer); + m_publishTimer = null; + } + } + #endregion + + #region Events + /// + /// Raised to indicate that the state of the subscription has changed. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly")] + public event SubscriptionStateChangedEventHandler StateChanged + { + add { m_StateChanged += value; } + remove { m_StateChanged -= value; } + } + + /// + /// Raised to indicate the publishing state for the subscription has stopped or resumed (see PublishingStopped property). + /// + public event EventHandler PublishStatusChanged + { + add + { + lock (m_cache) + { + m_PublishStatusChanged += value; + } + } + + remove + { + lock (m_cache) + { + m_PublishStatusChanged -= value; + } + } + } + #endregion + + #region Persistent Properties + /// + /// A display name for the subscription. + /// + [DataMember(Order = 1)] + public string DisplayName + { + get { return m_displayName; } + + set + { + m_displayName = value; + } + } + + /// + /// The publishing interval. + /// + [DataMember(Order = 2)] + public int PublishingInterval + { + get { return m_publishingInterval; } + set { m_publishingInterval = value; } + } + + /// + /// The keep alive count. + /// + [DataMember(Order = 3)] + public uint KeepAliveCount + { + get { return m_keepAliveCount; } + set { m_keepAliveCount = value; } + } + + /// + /// The maximum number of notifications per publish request. + /// + [DataMember(Order = 4)] + public uint LifetimeCount + { + get { return m_lifetimeCount; } + set { m_lifetimeCount = value; } + } + + /// + /// The maximum number of notifications per publish request. + /// + [DataMember(Order = 5)] + public uint MaxNotificationsPerPublish + { + get { return m_maxNotificationsPerPublish; } + set { m_maxNotificationsPerPublish = value; } + } + + /// + /// Whether publishing is enabled. + /// + [DataMember(Order = 6)] + public bool PublishingEnabled + { + get { return m_publishingEnabled; } + set { m_publishingEnabled = value; } + } + + /// + /// The priority assigned to subscription. + /// + [DataMember(Order = 7)] + public byte Priority + { + get { return m_priority; } + set { m_priority = value; } + } + + /// + /// The timestamps to return with the notification messages. + /// + [DataMember(Order = 8)] + public TimestampsToReturn TimestampsToReturn + { + get { return m_timestampsToReturn; } + set { m_timestampsToReturn = value; } + } + + /// + /// The maximum number of messages to keep in the internal cache. + /// + [DataMember(Order = 9)] + public int MaxMessageCount + { + get + { + lock (m_cache) + { + return m_maxMessageCount; + } + } + + set + { + lock (m_cache) + { + m_maxMessageCount = value; + } + } + } + + /// + /// The default monitored item. + /// + [DataMember(Order = 10)] + public MonitoredItem DefaultItem + { + get { return m_defaultItem; } + set { m_defaultItem = value; } + } + + /// + /// The minimum lifetime for subscriptions in milliseconds. + /// + [DataMember(Order = 11)] + public uint MinLifetimeInterval + { + get { return m_minLifetimeInterval; } + set { m_minLifetimeInterval = value; } + } + + /// + /// Gets or sets a value indicating whether the notifications are cached within the monitored items. + /// + /// + /// true if monitored item cache is disabled; otherwise, false. + /// + /// + /// Applications must process the Session.Notication event if this is set to true. + /// This flag improves performance by eliminating the processing involved in updating the cache. + /// + [DataMember(Order = 12)] + public bool DisableMonitoredItemCache + { + get { return m_disableMonitoredItemCache; } + set { m_disableMonitoredItemCache = value; } + } + + /// + /// Gets or sets the fast data change callback. + /// + /// The fast data change callback. + /// + /// Only one callback is allowed at a time but it is more efficient to call than an event. + /// + public FastDataChangeNotificationEventHandler FastDataChangeCallback + { + get { return m_fastDataChangeCallback; } + set { m_fastDataChangeCallback = value; } + } + + /// + /// Gets or sets the fast event callback. + /// + /// The fast event callback. + /// + /// Only one callback is allowed at a time but it is more efficient to call than an event. + /// + public FastEventNotificationEventHandler FastEventCallback + { + get { return m_fastEventCallback; } + set { m_fastEventCallback = value; } + } + + /// + /// The items to monitor. + /// + public IEnumerable MonitoredItems + { + get + { + lock (m_cache) + { + return new List(m_monitoredItems.Values); + } + } + } + + /// + /// Allows the list of monitored items to be saved/restored when the object is serialized. + /// + [DataMember(Name = "MonitoredItems", Order = 11)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private List SavedMonitoredItems + { + get + { + lock (m_cache) + { + return new List(m_monitoredItems.Values); + } + } + + set + { + if (this.Created) + { + throw new InvalidOperationException("Cannot update a subscription that has been created on the server."); + } + + lock (m_cache) + { + m_monitoredItems.Clear(); + + foreach (MonitoredItem monitoredItem in value) + { + AddItem(monitoredItem); + } + } + } + } + #endregion + + #region Dynamic Properties + /// + /// Returns true if the subscription has changes that need to be applied. + /// + public bool ChangesPending + { + get + { + if (m_deletedItems.Count > 0) + { + return true; + } + + foreach (MonitoredItem monitoredItem in m_monitoredItems.Values) + { + if (Created && !monitoredItem.Status.Created) + { + return true; + } + + if (monitoredItem.AttributesModified) + { + return true; + } + } + + return false; + } + } + + /// + /// Returns the number of monitored items. + /// + public uint MonitoredItemCount + { + get + { + lock (m_cache) + { + return (uint)m_monitoredItems.Count; + } + } + } + + /// + /// The session that owns the subscription item. + /// + public Session Session + { + get { return m_session; } + internal set { m_session = value; } + } + + /// + /// A local handle assigned to the subscription + /// + public object Handle + { + get { return m_handle; } + set { m_handle = value; } + } + + /// + /// The unique identifier assigned by the server. + /// + public uint Id + { + get { return m_id; } + } + + /// + /// Whether the subscription has been created on the server. + /// + public bool Created + { + get { return m_id != 0; } + } + + /// + /// The current publishing interval. + /// + public double CurrentPublishingInterval + { + get { return m_currentPublishingInterval; } + } + + /// + /// The current keep alive count. + /// + public uint CurrentKeepAliveCount + { + get { return m_currentKeepAliveCount; } + } + + /// + /// The current lifetime count. + /// + public uint CurrentLifetimeCount + { + get { return m_currentLifetimeCount; } + } + + /// + /// Whether publishing is currently enabled. + /// + public bool CurrentPublishingEnabled + { + get { return m_currentPublishingEnabled; } + } + + /// + /// The priority assigned to subscription when it was created. + /// + public byte CurrentPriority + { + get { return m_currentPriority; } + } + + /// + /// The when that the last notification received was published. + /// + public DateTime PublishTime + { + get + { + lock (m_cache) + { + if (m_messageCache.Count > 0) + { + return m_messageCache.Last.Value.PublishTime; + } + } + + return DateTime.MinValue; + } + } + + /// + /// The when that the last notification was received. + /// + public DateTime LastNotificationTime + { + get + { + lock (m_cache) + { + return m_lastNotificationTime; + } + } + } + + /// + /// The sequence number assigned to the last notification message. + /// + public uint SequenceNumber + { + get + { + lock (m_cache) + { + if (m_messageCache.Count > 0) + { + return m_messageCache.Last.Value.SequenceNumber; + } + } + + return 0; + } + } + + /// + /// The number of notifications contained in the last notification message. + /// + public uint NotificationCount + { + get + { + lock (m_cache) + { + if (m_messageCache.Count > 0) + { + return (uint)m_messageCache.Last.Value.NotificationData.Count; + } + } + + return 0; + } + } + + /// + /// The last notification received from the server. + /// + public NotificationMessage LastNotification + { + get + { + lock (m_cache) + { + if (m_messageCache.Count > 0) + { + return m_messageCache.Last.Value; + } + + return null; + } + } + } + + /// + /// The cached notifications. + /// + public IEnumerable Notifications + { + get + { + lock (m_cache) + { + // make a copy to ensure the state of the last cannot change during enumeration. + return new List(m_messageCache); + } + } + } + + /// + /// The sequence numbers that are available for republish requests. + /// + public IEnumerable AvailableSequenceNumbers + { + get + { + lock (m_cache) + { + return m_availableSequenceNumbers; + } + } + } + + /// + /// Sends a notification that the state of the subscription has changed. + /// + public void ChangesCompleted() + { + if (m_StateChanged != null) + { + m_StateChanged(this, new SubscriptionStateChangedEventArgs(m_changeMask)); + } + + m_changeMask = SubscriptionChangeMask.None; + } + + /// + /// Returns true if the subscription is not receiving publishes. + /// + public bool PublishingStopped + { + get + { + lock (m_cache) + { + int keepAliveInterval = (int)(m_currentPublishingInterval*m_currentKeepAliveCount); + + if (m_lastNotificationTime.AddMilliseconds(keepAliveInterval+500) < DateTime.UtcNow) + { + return true; + } + + return false; + } + } + } + #endregion + + #region Public Methods + /// + /// Ensures sensible values for the counts. + /// + private void AdjustCounts(ref uint keepAliveCount, ref uint lifetimeCount) + { + // keep alive count must be at least 1. + if (keepAliveCount == 0) + { + keepAliveCount = 1; + } + + // ensure the lifetime is sensible given the sampling interval. + if (m_publishingInterval > 0) + { + uint minLifetimeCount = (uint)(m_minLifetimeInterval/m_publishingInterval); + + if (lifetimeCount < minLifetimeCount) + { + lifetimeCount = minLifetimeCount; + + if (m_minLifetimeInterval%m_publishingInterval != 0) + { + lifetimeCount++; + } + } + } + + // don't know what the sampling interval will be - use something large enough + // to ensure the user does not experience unexpected drop outs. + else + { + lifetimeCount = 1000; + } + + // lifetime must be greater than the keep alive count. + if (lifetimeCount < keepAliveCount) + { + lifetimeCount = keepAliveCount; + } + } + + /// + /// Creates a subscription on the server. + /// + public void Create() + { + VerifySubscriptionState(false); + + // create the subscription. + uint subscriptionId; + double revisedPublishingInterval; + uint revisedKeepAliveCount = m_keepAliveCount; + uint revisedLifetimeCounter = m_lifetimeCount; + + AdjustCounts(ref revisedKeepAliveCount, ref revisedLifetimeCounter); + + m_session.CreateSubscription( + null, + m_publishingInterval, + revisedLifetimeCounter, + revisedKeepAliveCount, + m_maxNotificationsPerPublish, + m_publishingEnabled, + m_priority, + out subscriptionId, + out revisedPublishingInterval, + out revisedLifetimeCounter, + out revisedKeepAliveCount); + + // update current state. + m_id = subscriptionId; + m_currentPublishingInterval = revisedPublishingInterval; + m_currentKeepAliveCount = revisedKeepAliveCount; + m_currentLifetimeCount = revisedLifetimeCounter; + m_currentPublishingEnabled = m_publishingEnabled; + m_currentPriority = m_priority; + + StartKeepAliveTimer(); + + m_changeMask |= SubscriptionChangeMask.Created; + + CreateItems(); + + ChangesCompleted(); + } + + /// + /// Starts a timer to ensure publish requests are sent frequently enough to detect network interruptions. + /// + private void StartKeepAliveTimer() + { + // stop the publish timer. + if (m_publishTimer != null) + { + m_publishTimer.Dispose(); + m_publishTimer = null; + } + + lock (m_cache) + { + m_lastNotificationTime = DateTime.MinValue; + } + + int keepAliveInterval = (int)(m_currentPublishingInterval*m_currentKeepAliveCount); + + m_lastNotificationTime = DateTime.UtcNow; + m_publishTimer = new Timer(OnKeepAlive, keepAliveInterval, keepAliveInterval, keepAliveInterval); + + // send initial publish. + m_session.BeginPublish(keepAliveInterval*3); + } + + /// + /// Checks if a notification has arrived. Sends a publish if it has not. + /// + private void OnKeepAlive(object state) + { + // check if a publish has arrived. + EventHandler callback = null; + + lock (m_cache) + { + if (!PublishingStopped) + { + return; + } + + callback = m_PublishStatusChanged; + m_publishLateCount++; + } + + TraceState("PUBLISHING STOPPED"); + + if (callback != null) + { + try + { + callback(this, null); + } + catch (Exception e) + { + Utils.Trace(e, "Error while raising PublishStateChanged event."); + } + } + } + + /// + /// Dumps the current state of the session queue. + /// + internal void TraceState(string context) + { + if ((Utils.TraceMask & Utils.TraceMasks.Information) == 0) + { + return; + } + + StringBuilder buffer = new StringBuilder(); + + buffer.AppendFormat("Subscription {0}", context); + buffer.AppendFormat(", Id={0}", m_id); + buffer.AppendFormat(", LastNotificationTime={0:HH:mm:ss}", m_lastNotificationTime); + + if (m_session != null) + { + buffer.AppendFormat(", GoodPublishRequestCount={0}", m_session.GoodPublishRequestCount); + } + + buffer.AppendFormat(", PublishingInterval={0}", m_currentPublishingInterval); + buffer.AppendFormat(", KeepAliveCount={0}", m_currentKeepAliveCount); + buffer.AppendFormat(", PublishingEnabled={0}", m_currentPublishingEnabled); + buffer.AppendFormat(", MonitoredItemCount={0}", MonitoredItemCount); + + Utils.Trace("{0}", buffer.ToString()); + } + + /// + /// Deletes a subscription on the server. + /// + public void Delete(bool silent) + { + if (!silent) + { + VerifySubscriptionState(true); + } + + // nothing to do if not created. + if (!this.Created) + { + return; + } + + try + { + // stop the publish timer. + if (m_publishTimer != null) + { + m_publishTimer.Dispose(); + m_publishTimer = null; + } + + // delete the subscription. + UInt32Collection subscriptionIds = new uint[] { m_id }; + + StatusCodeCollection results; + DiagnosticInfoCollection diagnosticInfos; + + ResponseHeader responseHeader = m_session.DeleteSubscriptions( + null, + subscriptionIds, + out results, + out diagnosticInfos); + + // validate response. + ClientBase.ValidateResponse(results, subscriptionIds); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, subscriptionIds); + + if (StatusCode.IsBad(results[0])) + { + throw new ServiceResultException(ClientBase.GetResult(results[0], 0, diagnosticInfos, responseHeader)); + } + } + + // supress exception if silent flag is set. + catch (Exception e) + { + if (!silent) + { + throw new ServiceResultException(e, StatusCodes.BadUnexpectedError); + } + } + + // always put object in disconnected state even if an error occurs. + finally + { + m_id = 0; + m_currentPublishingInterval = 0; + m_currentKeepAliveCount = 0; + m_currentPublishingEnabled = false; + m_currentPriority = 0; + + // update items. + lock (m_cache) + { + foreach (MonitoredItem monitoredItem in m_monitoredItems.Values) + { + monitoredItem.SetDeleteResult(StatusCodes.Good, -1, null, null); + } + } + + m_deletedItems.Clear(); + + m_changeMask |= SubscriptionChangeMask.Deleted; + } + + ChangesCompleted(); + } + + /// + /// Modifies a subscription on the server. + /// + public void Modify() + { + VerifySubscriptionState(true); + + // modify the subscription. + double revisedPublishingInterval; + uint revisedKeepAliveCount = m_keepAliveCount; + uint revisedLifetimeCounter = m_lifetimeCount; + + AdjustCounts(ref revisedKeepAliveCount, ref revisedLifetimeCounter); + + m_session.ModifySubscription( + null, + m_id, + m_publishingInterval, + revisedLifetimeCounter, + revisedKeepAliveCount, + m_maxNotificationsPerPublish, + m_priority, + out revisedPublishingInterval, + out revisedLifetimeCounter, + out revisedKeepAliveCount); + + // update current state. + m_currentPublishingInterval = revisedPublishingInterval; + m_currentKeepAliveCount = revisedKeepAliveCount; + m_currentLifetimeCount = revisedLifetimeCounter; + m_currentPriority = m_priority; + + m_changeMask |= SubscriptionChangeMask.Modified; + ChangesCompleted(); + } + + /// + /// Changes the publishing enabled state for the subscription. + /// + public void SetPublishingMode(bool enabled) + { + VerifySubscriptionState(true); + + // modify the subscription. + UInt32Collection subscriptionIds = new uint[] { m_id }; + + StatusCodeCollection results; + DiagnosticInfoCollection diagnosticInfos; + + ResponseHeader responseHeader = m_session.SetPublishingMode( + null, + enabled, + new uint[] { m_id }, + out results, + out diagnosticInfos); + + // validate response. + ClientBase.ValidateResponse(results, subscriptionIds); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, subscriptionIds); + + if (StatusCode.IsBad(results[0])) + { + throw new ServiceResultException(ClientBase.GetResult(results[0], 0, diagnosticInfos, responseHeader)); + } + + // update current state. + m_currentPublishingEnabled = m_publishingEnabled = enabled; + + m_changeMask |= SubscriptionChangeMask.Modified; + ChangesCompleted(); + } + + /// + /// Republishes the specified notification message. + /// + public NotificationMessage Republish(uint sequenceNumber) + { + VerifySubscriptionState(true); + + NotificationMessage message; + + m_session.Republish( + null, + m_id, + sequenceNumber, + out message); + + return message; + } + + /// + /// Applies any changes to the subscription items. + /// + public void ApplyChanges() + { + DeleteItems(); + ModifyItems(); + CreateItems(); + } + + /// + /// Resolves all relative paths to nodes on the server. + /// + public void ResolveItemNodeIds() + { + VerifySubscriptionState(true); + + // collect list of browse paths. + BrowsePathCollection browsePaths = new BrowsePathCollection(); + List itemsToBrowse = new List(); + + lock (m_cache) + { + foreach (MonitoredItem monitoredItem in m_monitoredItems.Values) + { + if (!String.IsNullOrEmpty(monitoredItem.RelativePath) && NodeId.IsNull(monitoredItem.ResolvedNodeId)) + { + // cannot change the relative path after an item is created. + if (monitoredItem.Created) + { + throw new ServiceResultException(StatusCodes.BadInvalidState, "Cannot modify item path after it is created."); + } + + BrowsePath browsePath = new BrowsePath(); + + browsePath.StartingNode = monitoredItem.StartNodeId; + + // parse the relative path. + try + { + browsePath.RelativePath = RelativePath.Parse(monitoredItem.RelativePath, m_session.TypeTree); + } + catch (Exception e) + { + monitoredItem.SetError(new ServiceResult(e)); + continue; + } + + browsePaths.Add(browsePath); + itemsToBrowse.Add(monitoredItem); + } + } + } + + // nothing to do. + if (browsePaths.Count == 0) + { + return; + } + + // translate browse paths. + BrowsePathResultCollection results; + DiagnosticInfoCollection diagnosticInfos; + + ResponseHeader responseHeader = m_session.TranslateBrowsePathsToNodeIds( + null, + browsePaths, + out results, + out diagnosticInfos); + + ClientBase.ValidateResponse(results, browsePaths); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, browsePaths); + + // update results. + for (int ii = 0; ii < results.Count; ii++) + { + itemsToBrowse[ii].SetResolvePathResult(results[ii], ii, diagnosticInfos, responseHeader); + } + + m_changeMask |= SubscriptionChangeMask.ItemsModified; + } + + /// + /// Creates all items that have not already been created. + /// + public IList CreateItems() + { + VerifySubscriptionState(true); + + ResolveItemNodeIds(); + + MonitoredItemCreateRequestCollection requestItems = new MonitoredItemCreateRequestCollection(); + List itemsToCreate = new List(); + + lock (m_cache) + { + foreach (MonitoredItem monitoredItem in m_monitoredItems.Values) + { + // ignore items that have been created. + if (monitoredItem.Status.Created) + { + continue; + } + + // build item request. + MonitoredItemCreateRequest request = new MonitoredItemCreateRequest(); + + request.ItemToMonitor.NodeId = monitoredItem.ResolvedNodeId; + request.ItemToMonitor.AttributeId = monitoredItem.AttributeId; + request.ItemToMonitor.IndexRange = monitoredItem.IndexRange; + request.ItemToMonitor.DataEncoding = monitoredItem.Encoding; + + request.MonitoringMode = monitoredItem.MonitoringMode; + + request.RequestedParameters.ClientHandle = monitoredItem.ClientHandle; + request.RequestedParameters.SamplingInterval = monitoredItem.SamplingInterval; + request.RequestedParameters.QueueSize = monitoredItem.QueueSize; + request.RequestedParameters.DiscardOldest = monitoredItem.DiscardOldest; + + if (monitoredItem.Filter != null) + { + request.RequestedParameters.Filter = new ExtensionObject(monitoredItem.Filter); + } + + requestItems.Add(request); + itemsToCreate.Add(monitoredItem); + } + } + + if (requestItems.Count == 0) + { + return itemsToCreate; + } + + // modify the subscription. + MonitoredItemCreateResultCollection results; + DiagnosticInfoCollection diagnosticInfos; + + ResponseHeader responseHeader = m_session.CreateMonitoredItems( + null, + m_id, + m_timestampsToReturn, + requestItems, + out results, + out diagnosticInfos); + + ClientBase.ValidateResponse(results, itemsToCreate); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToCreate); + + // update results. + for (int ii = 0; ii < results.Count; ii++) + { + itemsToCreate[ii].SetCreateResult(requestItems[ii], results[ii], ii, diagnosticInfos, responseHeader); + } + + m_changeMask |= SubscriptionChangeMask.ItemsCreated; + ChangesCompleted(); + + // return the list of items affected by the change. + return itemsToCreate; + } + + /// + /// Modies all items that have been changed. + /// + public IList ModifyItems() + { + VerifySubscriptionState(true); + + MonitoredItemModifyRequestCollection requestItems = new MonitoredItemModifyRequestCollection(); + List itemsToModify = new List(); + + lock (m_cache) + { + foreach (MonitoredItem monitoredItem in m_monitoredItems.Values) + { + // ignore items that have been created or modified. + if (!monitoredItem.Status.Created || !monitoredItem.AttributesModified) + { + continue; + } + + // build item request. + MonitoredItemModifyRequest request = new MonitoredItemModifyRequest(); + + request.MonitoredItemId = monitoredItem.Status.Id; + request.RequestedParameters.ClientHandle = monitoredItem.ClientHandle; + request.RequestedParameters.SamplingInterval = monitoredItem.SamplingInterval; + request.RequestedParameters.QueueSize = monitoredItem.QueueSize; + request.RequestedParameters.DiscardOldest = monitoredItem.DiscardOldest; + + if (monitoredItem.Filter != null) + { + request.RequestedParameters.Filter = new ExtensionObject(monitoredItem.Filter); + } + + requestItems.Add(request); + itemsToModify.Add(monitoredItem); + } + } + + if (requestItems.Count == 0) + { + return itemsToModify; + } + + // modify the subscription. + MonitoredItemModifyResultCollection results; + DiagnosticInfoCollection diagnosticInfos; + + ResponseHeader responseHeader = m_session.ModifyMonitoredItems( + null, + m_id, + m_timestampsToReturn, + requestItems, + out results, + out diagnosticInfos); + + ClientBase.ValidateResponse(results, itemsToModify); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToModify); + + // update results. + for (int ii = 0; ii < results.Count; ii++) + { + itemsToModify[ii].SetModifyResult(requestItems[ii], results[ii], ii, diagnosticInfos, responseHeader); + } + + m_changeMask |= SubscriptionChangeMask.ItemsCreated; + ChangesCompleted(); + + // return the list of items affected by the change. + return itemsToModify; + } + + /// + /// Deletes all items that have been marked for deletion. + /// + public IList DeleteItems() + { + VerifySubscriptionState(true); + + if (m_deletedItems.Count == 0) + { + return new List(); + } + + List itemsToDelete = m_deletedItems; + m_deletedItems = new List(); + + UInt32Collection monitoredItemIds = new UInt32Collection(); + + foreach (MonitoredItem monitoredItem in itemsToDelete) + { + monitoredItemIds.Add(monitoredItem.Status.Id); + } + + StatusCodeCollection results; + DiagnosticInfoCollection diagnosticInfos; + + ResponseHeader responseHeader = m_session.DeleteMonitoredItems( + null, + m_id, + monitoredItemIds, + out results, + out diagnosticInfos); + + ClientBase.ValidateResponse(results, monitoredItemIds); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, monitoredItemIds); + + // update results. + for (int ii = 0; ii < results.Count; ii++) + { + itemsToDelete[ii].SetDeleteResult(results[ii], ii, diagnosticInfos, responseHeader); + } + + m_changeMask |= SubscriptionChangeMask.ItemsDeleted; + ChangesCompleted(); + + // return the list of items affected by the change. + return itemsToDelete; + } + + /// + /// Deletes all items that have been marked for deletion. + /// + public List SetMonitoringMode( + MonitoringMode monitoringMode, + IList monitoredItems) + { + if (monitoredItems == null) throw new ArgumentNullException("monitoredItems"); + + VerifySubscriptionState(true); + + if (monitoredItems.Count == 0) + { + return null; + } + + // get list of items to update. + UInt32Collection monitoredItemIds = new UInt32Collection(); + + foreach (MonitoredItem monitoredItem in monitoredItems) + { + monitoredItemIds.Add(monitoredItem.Status.Id); + } + + StatusCodeCollection results; + DiagnosticInfoCollection diagnosticInfos; + + ResponseHeader responseHeader = m_session.SetMonitoringMode( + null, + m_id, + monitoringMode, + monitoredItemIds, + out results, + out diagnosticInfos); + + ClientBase.ValidateResponse(results, monitoredItemIds); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, monitoredItemIds); + + // update results. + bool noErrors = true; + List errors = new List(); + + for (int ii = 0; ii < results.Count; ii++) + { + ServiceResult error = null; + + if (StatusCode.IsBad(results[ii])) + { + error = ClientBase.GetResult(results[ii], ii, diagnosticInfos, responseHeader); + noErrors = false; + } + else + { + monitoredItems[ii].MonitoringMode = monitoringMode; + monitoredItems[ii].Status.SetMonitoringMode(monitoringMode); + } + + errors.Add(error); + } + + // raise state changed event. + m_changeMask |= SubscriptionChangeMask.ItemsModified; + ChangesCompleted(); + + // return null list if no errors occurred. + if (noErrors) + { + return null; + } + + return errors; + } + + /// + /// Adds the notification message to internal cache. + /// + public void SaveMessageInCache( + IList availableSequenceNumbers, + NotificationMessage message, + IList stringTable) + { + EventHandler callback = null; + + lock (m_cache) + { + if (availableSequenceNumbers != null) + { + m_availableSequenceNumbers = availableSequenceNumbers; + } + + if (message == null) + { + return; + } + + // check if a publish error was previously reported. + if (PublishingStopped) + { + callback = m_PublishStatusChanged; + TraceState("PUBLISHING RECOVERED"); + } + + m_lastNotificationTime = DateTime.UtcNow; + + // save the string table that came with notification. + message.StringTable = new List(stringTable); + + // create queue for the first time. + if (m_incomingMessages == null) + { + m_incomingMessages = new LinkedList(); + } + + // find or create an entry for the incoming sequence number. + IncomingMessage entry = null; + LinkedListNode node = m_incomingMessages.Last; + + while (node != null) + { + entry = node.Value; + LinkedListNode previous = node.Previous; + + if (entry.SequenceNumber == message.SequenceNumber) + { + entry.Timestamp = DateTime.UtcNow; + break; + } + + if (entry.SequenceNumber < message.SequenceNumber) + { + entry = new IncomingMessage(); + entry.SequenceNumber = message.SequenceNumber; + entry.Timestamp = DateTime.UtcNow; + m_incomingMessages.AddAfter(node, entry); + break; + } + + node = previous; + entry = null; + } + + if (entry == null) + { + entry = new IncomingMessage(); + entry.SequenceNumber = message.SequenceNumber; + entry.Timestamp = DateTime.UtcNow; + m_incomingMessages.AddLast(entry); + } + + // check for keep alive. + if (message.NotificationData.Count > 0) + { + entry.Message = message; + entry.Processed = false; + } + + // fill in any gaps in the queue + node = m_incomingMessages.First; + + while (node != null) + { + entry = node.Value; + LinkedListNode next = node.Next; + + if (next != null && next.Value.SequenceNumber > entry.SequenceNumber+1) + { + IncomingMessage placeholder = new IncomingMessage(); + placeholder.SequenceNumber = entry.SequenceNumber+1; + placeholder.Timestamp = DateTime.UtcNow; + node = m_incomingMessages.AddAfter(node, placeholder); + continue; + } + + node = next; + } + + // clean out processed values. + node = m_incomingMessages.First; + + while (node != null) + { + entry = node.Value; + LinkedListNode next = node.Next; + + // can only pull off processed or expired messages. + if (!entry.Processed && !(entry.Republished && entry.Timestamp.AddSeconds(10) < DateTime.UtcNow)) + { + break; + } + + if (next != null) + { + m_incomingMessages.Remove(node); + } + + node = next; + } + + // process messages. + Task.Run(() => + { + OnMessageRecieved(null); + }); + } + + // send notification that publishing has recovered. + if (callback != null) + { + try + { + callback(this, null); + } + catch (Exception e) + { + Utils.Trace(e, "Error while raising PublishStateChanged event."); + } + } + } + + /// + /// Processes the incoming messages. + /// + private void OnMessageRecieved(object state) + { + try + { + Session session = null; + uint subscriptionId = 0; + EventHandler callback = null; + + // get list of new messages to process. + List messagesToProcess = null; + + // get list of new messages to republish. + List messagesToRepublish = null; + + lock (m_cache) + { + for (LinkedListNode ii = m_incomingMessages.First; ii != null; ii = ii.Next) + { + // update monitored items with unprocessed messages. + if (ii.Value.Message != null && !ii.Value.Processed) + { + if (messagesToProcess == null) + { + messagesToProcess = new List(); + } + + messagesToProcess.Add(ii.Value.Message); + + // remove the oldest items. + while (m_messageCache.Count > m_maxMessageCount) + { + m_messageCache.RemoveFirst(); + } + + m_messageCache.AddLast(ii.Value.Message); + ii.Value.Processed = true; + } + + // check for missing messages. + if (ii.Next != null && ii.Value.Message == null && !ii.Value.Processed && !ii.Value.Republished) + { + if (ii.Value.Timestamp.AddSeconds(2) < DateTime.UtcNow) + { + if (messagesToRepublish == null) + { + messagesToRepublish = new List(); + } + + messagesToRepublish.Add(ii.Value); + ii.Value.Republished = true; + } + } + } + + session = m_session; + subscriptionId = m_id; + callback = m_PublishStatusChanged; + } + + if (callback != null) + { + try + { + callback(this, null); + } + catch (Exception e) + { + Utils.Trace(e, "Error while raising PublishStateChanged event."); + } + } + + // process new messages. + if (messagesToProcess != null) + { + FastDataChangeNotificationEventHandler datachangeCallback = m_fastDataChangeCallback; + FastEventNotificationEventHandler eventCallback = m_fastEventCallback; + + for (int ii = 0; ii < messagesToProcess.Count; ii++) + { + NotificationMessage message = messagesToProcess[ii]; + + try + { + for (int jj = 0; jj < message.NotificationData.Count; jj++) + { + DataChangeNotification datachange = message.NotificationData[jj].Body as DataChangeNotification; + + if (datachange != null) + { + if (!m_disableMonitoredItemCache) + { + SaveDataChange(message, datachange, message.StringTable); + } + + if (datachangeCallback != null) + { + datachangeCallback(this, datachange, message.StringTable); + } + } + + EventNotificationList events = message.NotificationData[jj].Body as EventNotificationList; + + if (events != null) + { + if (!m_disableMonitoredItemCache) + { + SaveEvents(message, events, message.StringTable); + } + + if (eventCallback != null) + { + eventCallback(this, events, message.StringTable); + } + } + } + } + catch (Exception e) + { + Utils.Trace(e, "Error while processing incoming message #{0}.", message.SequenceNumber); + } + } + } + + // do any re-publishes. + if (messagesToRepublish != null && session != null && subscriptionId != 0) + { + for (int ii = 0; ii < messagesToRepublish.Count; ii++) + { + if (!session.Republish(subscriptionId, messagesToRepublish[ii].SequenceNumber)) + { + messagesToRepublish[ii].Republished = false; + } + } + } + } + catch (Exception e) + { + Utils.Trace(e, "Error while processing incoming messages."); + } + } + + /// + /// Adds an item to the subscription. + /// + public void AddItem(MonitoredItem monitoredItem) + { + if (monitoredItem == null) throw new ArgumentNullException("monitoredItem"); + + lock (m_cache) + { + if (m_monitoredItems.ContainsKey(monitoredItem.ClientHandle)) + { + return; + } + + m_monitoredItems.Add(monitoredItem.ClientHandle, monitoredItem); + monitoredItem.Subscription = this; + } + + m_changeMask |= SubscriptionChangeMask.ItemsAdded; + ChangesCompleted(); + } + + /// + /// Adds an item to the subscription. + /// + public void AddItems(IEnumerable monitoredItems) + { + if (monitoredItems == null) throw new ArgumentNullException("monitoredItems"); + + bool added = false; + + lock (m_cache) + { + foreach (MonitoredItem monitoredItem in monitoredItems) + { + if (!m_monitoredItems.ContainsKey(monitoredItem.ClientHandle)) + { + m_monitoredItems.Add(monitoredItem.ClientHandle, monitoredItem); + monitoredItem.Subscription = this; + added = true; + } + } + } + + if (added) + { + m_changeMask |= SubscriptionChangeMask.ItemsAdded; + ChangesCompleted(); + } + } + + /// + /// Removes an item from the subscription. + /// + public void RemoveItem(MonitoredItem monitoredItem) + { + if (monitoredItem == null) throw new ArgumentNullException("monitoredItem"); + + lock (m_cache) + { + if (!m_monitoredItems.Remove(monitoredItem.ClientHandle)) + { + return; + } + + monitoredItem.Subscription = null; + } + + if (monitoredItem.Status.Created) + { + m_deletedItems.Add(monitoredItem); + } + + m_changeMask |= SubscriptionChangeMask.ItemsRemoved; + ChangesCompleted(); + } + + /// + /// Removes an item from the subscription. + /// + public void RemoveItems(IEnumerable monitoredItems) + { + if (monitoredItems == null) throw new ArgumentNullException("monitoredItems"); + + bool changed = false; + + lock (m_cache) + { + foreach (MonitoredItem monitoredItem in monitoredItems) + { + if (m_monitoredItems.Remove(monitoredItem.ClientHandle)) + { + monitoredItem.Subscription = null; + + if (monitoredItem.Status.Created) + { + m_deletedItems.Add(monitoredItem); + } + + changed = true; + } + } + } + + if (changed) + { + m_changeMask |= SubscriptionChangeMask.ItemsRemoved; + ChangesCompleted(); + } + } + + /// + /// Returns the monitored item identified by the client handle. + /// + public MonitoredItem FindItemByClientHandle(uint clientHandle) + { + lock (m_cache) + { + MonitoredItem monitoredItem = null; + + if (m_monitoredItems.TryGetValue(clientHandle, out monitoredItem)) + { + return monitoredItem; + } + + return null; + } + } + + /// + /// Tells the server to refresh all conditions being monitored by the subscription. + /// + public void ConditionRefresh() + { + VerifySubscriptionState(true); + + m_session.Call( + ObjectTypeIds.ConditionType, + MethodIds.ConditionType_ConditionRefresh, + m_id); + } + #endregion + + #region Private Methods + /// + /// Throws an exception if the subscription is not in the correct state. + /// + private void VerifySubscriptionState(bool created) + { + if (created && m_id == 0) + { + throw new ServiceResultException(StatusCodes.BadInvalidState, "Subscription has not been created."); + } + + if (!created && m_id != 0) + { + throw new ServiceResultException(StatusCodes.BadInvalidState, "Subscription has alredy been created."); + } + } + + /// + /// Saves a data change in the monitored item cache. + /// + private void SaveDataChange(NotificationMessage message, DataChangeNotification notifications, IList stringTable) + { + for (int ii = 0; ii < notifications.MonitoredItems.Count; ii++) + { + MonitoredItemNotification notification = notifications.MonitoredItems[ii]; + + // lookup monitored item, + MonitoredItem monitoredItem = null; + + lock (m_cache) + { + if (!m_monitoredItems.TryGetValue(notification.ClientHandle, out monitoredItem)) + { + continue; + } + } + + // save the message. + notification.Message = message; + + // get diagnostic info. + if (notifications.DiagnosticInfos.Count > ii) + { + notification.DiagnosticInfo = notifications.DiagnosticInfos[ii]; + } + + // save in cache. + monitoredItem.SaveValueInCache(notification); + } + } + + /// + /// Saves events in the monitored item cache. + /// + private void SaveEvents(NotificationMessage message, EventNotificationList notifications, IList stringTable) + { + for (int ii = 0; ii < notifications.Events.Count; ii++) + { + EventFieldList eventFields = notifications.Events[ii]; + + MonitoredItem monitoredItem = null; + + lock (m_cache) + { + if (!m_monitoredItems.TryGetValue(eventFields.ClientHandle, out monitoredItem)) + { + continue; + } + } + + // save the message. + eventFields.Message = message; + + // save in cache. + monitoredItem.SaveValueInCache(eventFields); + } + } + #endregion + + #region Private Fields + private string m_displayName; + private int m_publishingInterval; + private uint m_keepAliveCount; + private uint m_lifetimeCount; + private uint m_minLifetimeInterval; + private uint m_maxNotificationsPerPublish; + private bool m_publishingEnabled; + private byte m_priority; + private TimestampsToReturn m_timestampsToReturn; + private List m_deletedItems; + private event SubscriptionStateChangedEventHandler m_StateChanged; + private MonitoredItem m_defaultItem; + private SubscriptionChangeMask m_changeMask; + + private Session m_session; + private object m_handle; + private uint m_id; + private double m_currentPublishingInterval; + private uint m_currentKeepAliveCount; + private uint m_currentLifetimeCount; + private bool m_currentPublishingEnabled; + private byte m_currentPriority; + private Timer m_publishTimer; + private DateTime m_lastNotificationTime; + private int m_publishLateCount; + private event EventHandler m_PublishStatusChanged; + + private object m_cache = new object(); + private LinkedList m_messageCache; + private IList m_availableSequenceNumbers; + private int m_maxMessageCount; + private SortedDictionary m_monitoredItems; + private bool m_disableMonitoredItemCache; + private FastDataChangeNotificationEventHandler m_fastDataChangeCallback; + private FastEventNotificationEventHandler m_fastEventCallback; + + /// + /// A message received from the server cached until is processed or discarded. + /// + private class IncomingMessage + { + public uint SequenceNumber; + public DateTime Timestamp; + public NotificationMessage Message; + public bool Processed; + public bool Republished; + } + + private LinkedList m_incomingMessages; + + private static long s_globalSubscriptionCounter; + #endregion + } + + #region SubscriptionChangeMask Enumeration + /// + /// Flags indicating what has changed in a subscription. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1714:FlagsEnumsShouldHavePluralNames"), Flags] + public enum SubscriptionChangeMask + { + /// + /// The subscription has not changed. + /// + None = 0x00, + + /// + /// The subscription was created on the server. + /// + Created = 0x01, + + /// + /// The subscription was deleted on the server. + /// + Deleted = 0x02, + + /// + /// The subscription was modified on the server. + /// + Modified = 0x04, + + /// + /// Monitored items were added to the subscription (but not created on the server) + /// + ItemsAdded = 0x08, + + /// + /// Monitored items were removed to the subscription (but not deleted on the server) + /// + ItemsRemoved = 0x10, + + /// + /// Monitored items were created on the server. + /// + ItemsCreated = 0x20, + + /// + /// Monitored items were deleted on the server. + /// + ItemsDeleted = 0x40, + + /// + /// Monitored items were modified on the server. + /// + ItemsModified = 0x80 + } + #endregion + + /// + /// The delegate used to receive data change notifications via a direct function call instead of a .NET Event. + /// + public delegate void FastDataChangeNotificationEventHandler(Subscription subscription, DataChangeNotification notification, IList stringTable); + + /// + /// The delegate used to receive event notifications via a direct function call instead of a .NET Event. + /// + public delegate void FastEventNotificationEventHandler(Subscription subscription, EventNotificationList notification, IList stringTable); + + #region SubscriptionStateChangedEventArgs Class + /// + /// The event arguments provided when the state of a subscription changes. + /// + public class SubscriptionStateChangedEventArgs : EventArgs + { + #region Constructors + /// + /// Creates a new instance. + /// + internal SubscriptionStateChangedEventArgs(SubscriptionChangeMask changeMask) + { + m_changeMask = changeMask; + } + #endregion + + #region Public Properties + /// + /// The changes that have affected the subscription. + /// + public SubscriptionChangeMask Status + { + get { return m_changeMask; } + } + #endregion + + #region Private Fields + private SubscriptionChangeMask m_changeMask; + #endregion + } + + /// + /// The delegate used to receive subscription state change notifications. + /// + public delegate void SubscriptionStateChangedEventHandler(Subscription subscription, SubscriptionStateChangedEventArgs e); + #endregion + + /// + /// A collection of subscriptions. + /// + [CollectionDataContract(Name = "ListOfSubscription", Namespace = Namespaces.OpcUaXsd, ItemName = "Subscription")] + public partial class SubscriptionCollection : List + { + #region Constructors + /// + /// Initializes an empty collection. + /// + public SubscriptionCollection() {} + + /// + /// Initializes the collection from another collection. + /// + /// The existing collection to use as the basis of creating this collection + public SubscriptionCollection(IEnumerable collection) : base(collection) {} + + /// + /// Initializes the collection with the specified capacity. + /// + /// The max. capacity of the collection + public SubscriptionCollection(int capacity) : base(capacity) {} + #endregion + } +} diff --git a/SampleApplications/SDK/Client/project.json b/SampleApplications/SDK/Client/project.json new file mode 100644 index 0000000000..ae7afafb44 --- /dev/null +++ b/SampleApplications/SDK/Client/project.json @@ -0,0 +1,16 @@ +{ + "dependencies": { + "Microsoft.NETCore.UniversalWindowsPlatform": "5.0.0" + }, + "frameworks": { + "uap10.0": {} + }, + "runtimes": { + "win10-arm": {}, + "win10-arm-aot": {}, + "win10-x86": {}, + "win10-x86-aot": {}, + "win10-x64": {}, + "win10-x64-aot": {} + } +} \ No newline at end of file diff --git a/SampleApplications/SDK/Configuration/AccountInfo.cs b/SampleApplications/SDK/Configuration/AccountInfo.cs new file mode 100644 index 0000000000..a5722be28f --- /dev/null +++ b/SampleApplications/SDK/Configuration/AccountInfo.cs @@ -0,0 +1,424 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Text; +using System.IO; +using System.Runtime.InteropServices; +using System.Diagnostics; +using Windows.System; + +namespace Opc.Ua.Configuration +{ + /// + /// Stores information about an account. + /// + public class AccountInfo : IComparable + { + #region Public Properties + /// + /// The name of the account. + /// + public string Name + { + get { return m_name; } + set { m_name = value; } + } + + /// + /// The domain that the account belongs to. + /// + public string Domain + { + get { return m_domain; } + set { m_domain = value; } + } + + /// + /// The SID for the account. + /// + public string Sid + { + get { return m_sid; } + set { m_sid = value; } + } + + /// + /// The type of SID used by the account. + /// + public AccountSidType SidType + { + get { return m_sidType; } + set { m_sidType = value; } + } + + /// + /// Thr description for the account. + /// + public string Description + { + get { return m_description; } + set { m_description = value; } + } + + /// + /// Thr current status for the account. + /// + public string Status + { + get { return m_status; } + set { m_status = value; } + } + #endregion + + #region Overridden Methods + /// + public override string ToString() + { + if (String.IsNullOrEmpty(m_name)) + { + return m_sid; + } + + if (!String.IsNullOrEmpty(m_domain)) + { + return Utils.Format(@"{0}\{1}", m_domain, m_name); + } + + return m_name; + } + #endregion + + #region IComparable Members + /// + /// Compares the obj. + /// + public int CompareTo(object obj) + { + AccountInfo target = obj as AccountInfo; + + if (Object.ReferenceEquals(target, null)) + { + return -1; + } + + if (Object.ReferenceEquals(target, this)) + { + return 0; + } + + if (m_domain == null) + { + return (target.m_domain == null)?0:-1; + } + + int result = m_domain.CompareTo(target.m_domain); + + if (result != 0) + { + return result; + } + + if (m_name == null) + { + return (target.m_name == null)?0:-1; + } + + result = m_name.CompareTo(target.m_name); + + if (result != 0) + { + return result; + } + + if (m_sid == null) + { + return (target.m_sid == null)?0:-1; + } + + return m_sid.CompareTo(target.m_sid); + } + #endregion + + #region Public Methods + /// + /// Creates an account info object from an identity name. + /// + public static AccountInfo Create(string identityName) + { + Debug.WriteLine("CONFIGURATION CONSOLE AccountInfo {0}", identityName); + + // create account info object. + AccountInfo account = new AccountInfo(); + + account.Name = KnownUserProperties.DisplayName; + account.SidType = AccountSidType.User; + account.Sid = KnownUserProperties.AccountName; + account.Domain = KnownUserProperties.DomainName; + account.Description = String.Empty; + account.Status = String.Empty; + + return account; + } + + /// + /// Applies the filters to the accounts. + /// + public static IList ApplyFilters(AccountFilters filters, IList accounts) + { + if (filters == null || accounts == null) + { + return accounts; + } + + List filteredAccounts = new List(); + + for (int ii = 0; ii < accounts.Count; ii++) + { + if (accounts[ii].ApplyFilters(filters)) + { + filteredAccounts.Add(accounts[ii]); + } + } + + return filteredAccounts; + } + + /// + /// Applies the filters to the account + /// + public bool ApplyFilters(AccountFilters filters) + { + // filter on name. + if (!String.IsNullOrEmpty(filters.Name)) + { + if (!Utils.Match(this.Name, filters.Name, false)) + { + return false; + } + } + + // filter on domain. + if (!String.IsNullOrEmpty(filters.Domain)) + { + if (String.Compare(this.Domain, filters.Domain, true) != 0) + { + return false; + } + } + + // exclude non-user related accounts. + if (this.SidType == AccountSidType.Domain || this.SidType > AccountSidType.BuiltIn) + { + return false; + } + + // apply account type filter. + if (filters.AccountTypeMask != AccountTypeMask.None) + { + if ((1<<((int)this.SidType-1) & (int)filters.AccountTypeMask) == 0) + { + return false; + } + } + + return true; + } + #endregion + + #region Private Fields + private string m_name; + private string m_domain; + private string m_sid; + private AccountSidType m_sidType; + private string m_description; + private string m_status; + #endregion + } + + #region AccountSidType Enumeration + /// + /// The type of SID used by the account. + /// + public enum AccountSidType : byte + { + /// + /// An interactive user account. + /// + User = 0x1, + + /// + /// An group of users. + /// + Group = 0x2, + + /// + /// A domain. + /// + Domain = 0x3, + + /// + /// An alias for a group or user. + /// + Alias = 0x4, + + /// + /// Built-in identity principals. + /// + BuiltIn = 0x5 + } + #endregion + + #region AccountFilters Class + /// + /// Filters that can be used to restrict the set of accounts returned. + /// + public class AccountFilters + { + #region Public Properties + /// + /// The name of the account (supports the '*' wildcard). + /// + public string Name + { + get { return m_name; } + set { m_name = value; } + } + + /// + /// The domain that the account belongs to. + /// + public string Domain + { + get { return m_domain; } + set { m_domain = value; } + } + + + /// + /// The types of accounts. + /// + public AccountTypeMask AccountTypeMask + { + get { return m_accountTypeMask; } + set { m_accountTypeMask = value; } + } + #endregion + + #region Private Fields + private string m_name; + private string m_domain; + private AccountTypeMask m_accountTypeMask; + #endregion + } + #endregion + + #region AccountTypeMask Enumeration + /// + /// The masks that can be use to filter a list of accounts. + /// + [Flags] + public enum AccountTypeMask + { + /// + /// Mask not specified. + /// + None = 0x0, + + /// + /// An interactive user account. + /// + User = 0x1, + + /// + /// An NT user group. + /// + Group = 0xA, + + /// + /// Well-known groups. + /// + WellKnownGroup = 0x10 + } + #endregion + + #region WellKnownSids Class + /// + /// The well known NT security identifiers. + /// + public static class WellKnownSids + { + /// + /// Interactive users. + /// + public const string Interactive = "S-1-5-4"; + + /// + /// Authenticated users. + /// + public const string AuthenticatedUser = "S-1-5-11"; + + /// + /// Anonymous Logons + /// + public const string AnonymousLogon = "S-1-5-7"; + + /// + /// The local system account. + /// + public const string LocalSystem = "S-1-5-18"; + + /// + /// The local service account. + /// + public const string LocalService = "S-1-5-19"; + + /// + /// The network service account. + /// + public const string NetworkService = "S-1-5-20"; + + /// + /// The administrators group. + /// + public const string Administrators = "S-1-5-32-544"; + + /// + /// The users group. + /// + public const string Users = "S-1-5-32-545"; + + /// + /// The guests group. + /// + public const string Guests = "S-1-5-32-546"; + } + #endregion +} diff --git a/SampleApplications/SDK/Configuration/ApplicationInstance.cs b/SampleApplications/SDK/Configuration/ApplicationInstance.cs new file mode 100644 index 0000000000..f974a2e930 --- /dev/null +++ b/SampleApplications/SDK/Configuration/ApplicationInstance.cs @@ -0,0 +1,1762 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Text; +using System.Reflection; +using System.IO; +using System.Runtime.Serialization; +using System.Security.Cryptography.X509Certificates; +using Windows.UI.Popups; +using System.Threading.Tasks; +using Windows.UI.Xaml; +using Windows.Storage; +using System.Net; + +namespace Opc.Ua.Configuration +{ + public abstract class IApplicationMessageDlg + { + public abstract void Message(string text, Boolean ask=false); + public abstract Task ShowAsync(); + } + + /// + /// A class that install, configures and runs a UA application. + /// + public class ApplicationInstance + { + #region Ctors + /// + /// Initializes a new instance of the class. + /// + public ApplicationInstance() + { } + + /// + /// Initializes a new instance of the class. + /// + /// The application configuration. + public ApplicationInstance(ApplicationConfiguration applicationConfiguration) + { + m_applicationConfiguration = applicationConfiguration; + } + #endregion + + #region Public Properties + /// + /// Gets or sets the name of the application. + /// + /// The name of the application. + public string ApplicationName + { + get { return m_applicationName; } + set { m_applicationName = value; } + } + + /// + /// Gets or sets the type of the application. + /// + /// The type of the application. + public ApplicationType ApplicationType + { + get { return m_applicationType; } + set { m_applicationType = value; } + } + + /// + /// Gets or sets the name of the config section containing the path to the application configuration file. + /// + /// The name of the config section. + public string ConfigSectionName + { + get { return m_configSectionName; } + set { m_configSectionName = value; } + } + + /// + /// Gets or sets the type of configuration file. + /// + /// The type of configuration file. + public Type ConfigurationType + { + get { return m_configurationType; } + set { m_configurationType = value; } + } + + /// + /// Gets or sets the installation configuration. + /// + /// The installation configuration. + public InstalledApplication InstallConfig + { + get { return m_installConfig; } + set { m_installConfig = value; } + } + + /// + /// Gets the server. + /// + /// The server. + public ServerBase Server + { + get { return m_server; } + } + + /// + /// Gets the application configuration used when the Start() method was called. + /// + /// The application configuration. + public ApplicationConfiguration ApplicationConfiguration + { + get { return m_applicationConfiguration; } + set { m_applicationConfiguration = value; } + } + + /// + /// Gets or sets a flag that indicates whether the application will be set up for management with the GDS agent. + /// + /// If true the application will not be visible to the GDS local agent after installation. + public bool NoGdsAgentAdmin { get; set; } + public static IApplicationMessageDlg MessageDlg { get; set; } + #endregion + + #region InstallConfig Handling + /// + /// Loads the installation configuration from a file. + /// + public InstalledApplication LoadInstallConfigFromFile(string filePath) + { + if (filePath == null) throw new ArgumentNullException("filePath"); + + Stream istrm = null; + + try + { + istrm = File.Open(filePath, FileMode.Open, FileAccess.Read); + } + catch (Exception e) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, e, "Could not open file: {0}", filePath); + } + + return LoadInstallConfigFromStream(istrm); + } + + /// + /// Loads the installation configuration from an embedded resource. + /// + public InstalledApplication LoadInstallConfigFromResource(string resourcePath, Assembly assembly) + { + if (resourcePath == null) throw new ArgumentNullException("resourcePath"); + + Stream istrm = assembly.GetManifestResourceStream(resourcePath); + + if (istrm == null) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, "Could not find resource file: {0}", resourcePath); + } + + return LoadInstallConfigFromStream(istrm); + } + + /// + /// Loads the installation configuration from a stream. + /// + public InstalledApplication LoadInstallConfigFromStream(Stream istrm) + { + try + { + DataContractSerializer serializer = new DataContractSerializer(typeof(InstalledApplication)); + return (InstalledApplication)serializer.ReadObject(istrm); + + } + catch (Exception e) + { + throw ServiceResultException.Create(StatusCodes.BadDecodingError, e, "Could not parse install configuration."); + } + } + + /// + /// Loads the installation configuration. + /// + /// The config file (may be null). + public virtual void LoadInstallConfig(string configFile) + { + // load configuration from command line. + if (!String.IsNullOrEmpty(configFile)) + { + InstallConfig = LoadInstallConfigFromFile(configFile); + } + + // load it from a resource if not already loaded. + else if (InstallConfig == null) + { + foreach (string resourcePath in this.GetType().GetTypeInfo().Assembly.GetManifestResourceNames()) + { + if (resourcePath.EndsWith("InstallConfig.xml")) + { + InstallConfig = LoadInstallConfigFromResource(resourcePath, this.GetType().GetTypeInfo().Assembly); + break; + } + } + + if (InstallConfig == null) + { + throw new ServiceResultException(StatusCodes.BadConfigurationError, "Could not load default installation config file."); + } + } + + // override the application name. + if (String.IsNullOrEmpty(InstallConfig.ApplicationName)) + { + InstallConfig.ApplicationName = ApplicationName; + } + else + { + ApplicationName = InstallConfig.ApplicationName; + } + + // update fixed fields in the installation config. + InstallConfig.ApplicationType = (Opc.Ua.Security.ApplicationType)(int)ApplicationType; + InstallConfig.ExecutableFile = ApplicationData.Current.LocalFolder.Path; + + if (InstallConfig.TraceConfiguration != null) + { + InstallConfig.TraceConfiguration.ApplySettings(); + } + } + #endregion + + #region Public Methods + /// + /// Starts the UA server. + /// + /// The server. + public async Task Start(ServerBase server) + { + m_server = server; + + if (m_applicationConfiguration == null) + { + await LoadApplicationConfiguration(false); + } + + if (m_applicationConfiguration.SecurityConfiguration != null && m_applicationConfiguration.SecurityConfiguration.AutoAcceptUntrustedCertificates) + { + m_applicationConfiguration.CertificateValidator.CertificateValidation += CertificateValidator_CertificateValidation; + } + + server.Start(m_applicationConfiguration); + } + + /// + /// Stops the UA server. + /// + public void Stop() + { + m_server.Stop(); + } + #endregion + + #region WindowsService Class + /// + /// Manages the interface between the UA server and the Windows SCM. + /// + protected class WindowsService + { + #region Constructors + /// + /// Initializes a new instance of the class. + /// + /// The server. + /// Name of the config section. + /// Type of the application. + /// Type of the configuration. + public WindowsService(ServerBase server, string configSectionName, ApplicationType applicationType, Type configurationType) + { + m_server = server; + m_configSectionName = configSectionName; + m_applicationType = applicationType; + m_configurationType = configurationType; + } + #endregion + + #region Private Methods + /// + /// Runs the service in a background thread. + /// + private async Task OnBackgroundStart(object state) + { + string filePath = null; + ApplicationConfiguration configuration = null; + + try + { + filePath = ApplicationConfiguration.GetFilePathFromAppConfig(m_configSectionName); + configuration = await ApplicationInstance.LoadAppConfig(false, filePath, m_applicationType, m_configurationType, true); + } + catch (Exception e) + { + ServiceResult error = ServiceResult.Create(e, StatusCodes.BadConfigurationError, "Could not load UA Service configuration file.\r\nPATH={0}", filePath); + } + + try + { + if (configuration.SecurityConfiguration != null && configuration.SecurityConfiguration.AutoAcceptUntrustedCertificates) + { + configuration.CertificateValidator.CertificateValidation += CertificateValidator_CertificateValidation; + } + + m_server.Start(configuration); + } + catch (Exception e) + { + ServiceResult error = ServiceResult.Create(e, StatusCodes.BadConfigurationError, "Could not start UA Service."); + Utils.Trace((int)Utils.TraceMasks.Error, error.ToLongString()); + } + } + + #endregion + + #region Private Fields + private ServerBase m_server; + private string m_configSectionName; + private ApplicationType m_applicationType; + private Type m_configurationType; + #endregion + } + #endregion + + #region ArgumentDescription Class + /// + /// Stores the description of an argument. + /// + protected class ArgumentDescription + { + /// + /// The argument name. + /// + public string Name; + + /// + /// The argument description. + /// + public string Description; + + /// + /// Whether the argument requires a value. + /// + public bool ValueRequired; + + /// + /// Whether the argument allows a value. + /// + public bool ValueAllowed; + + /// + /// Initializes a new instance of the class. + /// + /// The name. + /// if set to true a value is required. + /// if set to true a value is allowed. + /// The description. + public ArgumentDescription( + string name, + bool valueRequired, + bool valueAllowed, + string description) + { + Name = name; + ValueRequired = valueRequired; + ValueAllowed = valueAllowed; + Description = description; + } + } + + private static ArgumentDescription[] s_SupportedArguments = new ArgumentDescription[] + { + new ArgumentDescription("/start", false, false, "Starts the application as a service (/start [/silent] [/configFile:])."), + new ArgumentDescription("/install", false, false, "Installs the application (/install [/silent] [/configFile:])."), + new ArgumentDescription("/uninstall", false, false, "Uninstalls the application (/uninstall [/silent] [/configFile:])."), + new ArgumentDescription("/silent", false, false, "Performs operations without prompting user to confirm or displaying errors."), + new ArgumentDescription("/configFile", true, true, "Specifies the installation configuration file."), + }; + #endregion + + #region Protected Methods + /// + /// Gets the descriptions for the supported arguments. + /// + protected virtual ArgumentDescription[] GetArgumentDescriptions() + { + return s_SupportedArguments; + } + + /// + /// Gets the help string. + /// + protected virtual string GetHelpString(ArgumentDescription[] commands) + { + StringBuilder text = new StringBuilder(); + text.Append("These are the supported arguments:\r\n"); + + for (int ii = 0; ii < commands.Length; ii++) + { + ArgumentDescription command = commands[ii]; + + text.Append("\r\n"); + + if (command.ValueRequired) + { + text.AppendFormat("{0}: {1}", command.Name, command.Description); + } + else if (command.ValueAllowed) + { + text.AppendFormat("{0}[:] {1}", command.Name, command.Description); + } + else + { + text.AppendFormat("{0} {1}", command.Name, command.Description); + } + } + + text.Append("\r\n"); + return text.ToString(); + } + + /// + /// Validates the arguments. + /// + protected virtual string ValidateArguments(bool ignoreUnknownArguments, Dictionary args) + { + ArgumentDescription[] commands = GetArgumentDescriptions(); + + // check if help was requested. + if (args.ContainsKey("/?")) + { + return GetHelpString(commands); + } + + // validate the arguments. + StringBuilder error = new StringBuilder(); + + foreach (KeyValuePair arg in args) + { + ArgumentDescription command = null; + + for (int ii = 0; ii < commands.Length; ii++) + { + if (String.Compare(commands[ii].Name, arg.Key, StringComparison.OrdinalIgnoreCase) == 0) + { + command = commands[ii]; + break; + } + } + + if (command == null) + { + if (!ignoreUnknownArguments) + { + if (error.Length > 0) + { + error.Append("\r\n"); + } + + error.AppendFormat("Unrecognized argument: {0}", arg.Key); + } + + continue; + } + + if (command.ValueRequired && String.IsNullOrEmpty(arg.Value)) + { + if (error.Length > 0) + { + error.Append("\r\n"); + } + + error.AppendFormat("{0} requires a value to be specified (syntax {0}:).", arg.Key); + continue; + } + + if (!command.ValueAllowed && !String.IsNullOrEmpty(arg.Value)) + { + if (error.Length > 0) + { + error.Append("\r\n"); + } + + error.AppendFormat("{0} does not allow a value to be specified.", arg.Key); + continue; + } + } + + // return any error text. + return error.ToString(); + } + + /// + /// Updates the application configuration with the values from the installation configuration. + /// + /// The configuration to update. + protected virtual async Task UpdateAppConfigWithInstallConfig(ApplicationConfiguration configuration) + { + // override the application name. + if (InstallConfig.ApplicationName != null) + { + if (configuration.SecurityConfiguration != null && configuration.SecurityConfiguration.ApplicationCertificate != null) + { + if (configuration.SecurityConfiguration.ApplicationCertificate.SubjectName == configuration.ApplicationName) + { + configuration.SecurityConfiguration.ApplicationCertificate.SubjectName = InstallConfig.ApplicationName; + } + } + + configuration.ApplicationName = InstallConfig.ApplicationName; + } + + if (InstallConfig.ApplicationUri != null) + { + configuration.ApplicationUri = InstallConfig.ApplicationUri; + } + + // replace localhost with the current machine name. + if (configuration.ApplicationUri != null) + { + int index = configuration.ApplicationUri.IndexOf("localhost", StringComparison.OrdinalIgnoreCase); + + if (index != -1) + { + StringBuilder buffer = new StringBuilder(); + buffer.Append(configuration.ApplicationUri.Substring(0, index)); + buffer.Append(Utils.GetHostName()); + buffer.Append(configuration.ApplicationUri.Substring(index + "localhost".Length)); + configuration.ApplicationUri = buffer.ToString(); + } + } + + ServerBaseConfiguration serverConfiguration = null; + + if (configuration.ServerConfiguration != null) + { + serverConfiguration = configuration.ServerConfiguration; + } + else if (configuration.DiscoveryServerConfiguration != null) + { + serverConfiguration = configuration.DiscoveryServerConfiguration; + } + + if (serverConfiguration != null) + { + if (InstallConfig.BaseAddresses != null && InstallConfig.BaseAddresses.Count > 0) + { + Dictionary addresses = new Dictionary(); + serverConfiguration.BaseAddresses.Clear(); + + for (int ii = 0; ii < InstallConfig.BaseAddresses.Count; ii++) + { + Uri url = Utils.ParseUri(InstallConfig.BaseAddresses[ii]); + + if (url != null) + { + if (!addresses.ContainsKey(url.Scheme)) + { + serverConfiguration.BaseAddresses.Add(url.ToString()); + addresses.Add(url.Scheme, String.Empty); + } + else + { + serverConfiguration.AlternateBaseAddresses.Add(url.ToString()); + } + } + } + } + + if (InstallConfig.SecurityProfiles != null && InstallConfig.SecurityProfiles.Count > 0) + { + ServerSecurityPolicyCollection securityPolicies = new ServerSecurityPolicyCollection(); + + for (int ii = 0; ii < InstallConfig.SecurityProfiles.Count; ii++) + { + for (int jj = 0; jj < serverConfiguration.SecurityPolicies.Count; jj++) + { + if (serverConfiguration.SecurityPolicies[jj].SecurityPolicyUri == InstallConfig.SecurityProfiles[ii].ProfileUri) + { + securityPolicies.Add(serverConfiguration.SecurityPolicies[jj]); + } + } + } + + serverConfiguration.SecurityPolicies = securityPolicies; + } + } + + if (InstallConfig.ApplicationCertificate != null) + { + configuration.SecurityConfiguration.ApplicationCertificate.StoreType = InstallConfig.ApplicationCertificate.StoreType; + configuration.SecurityConfiguration.ApplicationCertificate.StorePath = InstallConfig.ApplicationCertificate.StorePath; + + if (String.IsNullOrEmpty(InstallConfig.ApplicationCertificate.SubjectName)) + { + configuration.SecurityConfiguration.ApplicationCertificate.SubjectName = InstallConfig.ApplicationCertificate.SubjectName; + } + } + + if (InstallConfig.RejectedCertificatesStore != null) + { + configuration.SecurityConfiguration.RejectedCertificateStore = Opc.Ua.Security.SecuredApplication.FromCertificateStoreIdentifier(InstallConfig.RejectedCertificatesStore); + } + + if (InstallConfig.IssuerCertificateStore != null) + { + configuration.SecurityConfiguration.TrustedIssuerCertificates.StoreType = InstallConfig.IssuerCertificateStore.StoreType; + configuration.SecurityConfiguration.TrustedIssuerCertificates.StorePath = InstallConfig.IssuerCertificateStore.StorePath; + configuration.SecurityConfiguration.TrustedIssuerCertificates.ValidationOptions = (CertificateValidationOptions)(int)InstallConfig.IssuerCertificateStore.ValidationOptions; + } + + if (InstallConfig.TrustedCertificateStore != null) + { + configuration.SecurityConfiguration.TrustedPeerCertificates.StoreType = InstallConfig.TrustedCertificateStore.StoreType; + configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath = InstallConfig.TrustedCertificateStore.StorePath; + configuration.SecurityConfiguration.TrustedPeerCertificates.ValidationOptions = (CertificateValidationOptions)(int)InstallConfig.TrustedCertificateStore.ValidationOptions; + } + + await configuration.CertificateValidator.Update(configuration); + } + + /// + /// Installs the service. + /// + /// if set to true no dialogs such be displayed. + /// Additional arguments provided on the command line. + protected virtual async Task Install(bool silent, Dictionary args) + { + Utils.Trace(Utils.TraceMasks.Information, "Installing application."); + + // check the configuration. + string filePath = Utils.GetAbsoluteFilePath(InstallConfig.ConfigurationFile, true, false, false); + + if (filePath == null) + { + Utils.Trace("WARNING: Could not load config file specified in the installation configuration: {0}", InstallConfig.ConfigurationFile); + filePath = ApplicationConfiguration.GetFilePathFromAppConfig(ConfigSectionName); + InstallConfig.ConfigurationFile = filePath; + } + + ApplicationConfiguration configuration = await LoadAppConfig(silent, filePath, Opc.Ua.Security.SecuredApplication.FromApplicationType(InstallConfig.ApplicationType), ConfigurationType, false); + + if (configuration == null) + { + return; + } + + // update the configuration. + await UpdateAppConfigWithInstallConfig(configuration); + ApplicationConfiguration = configuration; + + // update configuration with information form the install config. + // check the certificate. + X509Certificate2 certificate = await configuration.SecurityConfiguration.ApplicationCertificate.Find(true); + + if (certificate != null) + { + if (!silent) + { + bool result = await CheckApplicationInstanceCertificate(configuration, certificate, silent, InstallConfig.MinimumKeySize); + if (!result) + { + certificate = null; + } + } + } + + // create a new certificate. + if (certificate == null) + { + certificate = await CreateApplicationInstanceCertificate(configuration, InstallConfig.MinimumKeySize, InstallConfig.LifeTimeInMonths); + } + + // ensure the certificate is trusted. + await AddToTrustedStore(configuration, certificate); + + // add to discovery server. + if (configuration.ApplicationType == ApplicationType.Server || configuration.ApplicationType == ApplicationType.ClientAndServer) + { + try + { + await AddToDiscoveryServerTrustList(certificate, null, null, configuration.SecurityConfiguration.TrustedPeerCertificates); + } + catch (Exception e) + { + Utils.Trace(e, "Could not add certificate to LDS trust list."); + } + } + + // configure HTTP access. + ConfigureHttpAccess(configuration, false); + + // configure access to the executable, the configuration file and the private key. + await ConfigureFileAccess(configuration); + + // update configuration file. + ConfigUtils.UpdateConfigurationLocation(InstallConfig.ExecutableFile, InstallConfig.ConfigurationFile); + + try + { + // ensure the RawData does not get serialized. + certificate = configuration.SecurityConfiguration.ApplicationCertificate.Certificate; + + configuration.SecurityConfiguration.ApplicationCertificate.Certificate = null; + configuration.SecurityConfiguration.ApplicationCertificate.SubjectName = certificate.Subject; + configuration.SecurityConfiguration.ApplicationCertificate.Thumbprint = certificate.Thumbprint; + + configuration.SaveToFile(configuration.SourceFilePath); + + // restore the configuration. + configuration.SecurityConfiguration.ApplicationCertificate.Certificate = certificate; + } + catch (Exception e) + { + Utils.Trace(e, "Could not save configuration file. FilePath={0}", configuration.SourceFilePath); + } + + if (!NoGdsAgentAdmin) + { + try + { + // install the GDS agent configuration file + string agentPath = Utils.GetAbsoluteDirectoryPath(ApplicationData.Current.LocalFolder.Path + "\\OPC Foundation\\GDS\\Applications", false, false, true); + + if (agentPath != null) + { + Opc.Ua.Security.SecuredApplication export = new Opc.Ua.Security.SecurityConfigurationManager().ReadConfiguration(configuration.SourceFilePath); + export.ExecutableFile = InstallConfig.ExecutableFile; + + DataContractSerializer serializer = new DataContractSerializer(typeof(Opc.Ua.Security.SecuredApplication)); + + using (FileStream ostrm = File.Open(agentPath + "\\" + configuration.ApplicationName + ".xml", FileMode.Create)) + { + serializer.WriteObject(ostrm, export); + Utils.Trace(Utils.TraceMasks.Information, "Created GDS agent configuration file."); + } + } + } + catch (Exception e) + { + Utils.Trace(Utils.TraceMasks.Error, "Could not create GDS agent configuration file: {0}", e.Message); + } + } + } + + /// + /// Uninstalls the service. + /// + /// if set to true no dialogs such be displayed. + /// Additional arguments provided on the command line. + protected virtual async Task Uninstall(bool silent, Dictionary args) + { + // check the configuration. + string filePath = Utils.GetAbsoluteFilePath(InstallConfig.ConfigurationFile, true, false, false); + + if (filePath == null) + { + Utils.Trace("WARNING: Could not load config file specified in the installation configuration: {0}", InstallConfig.ConfigurationFile); + filePath = ApplicationConfiguration.GetFilePathFromAppConfig(ConfigSectionName); + InstallConfig.ConfigurationFile = filePath; + } + + ApplicationConfiguration configuration = await LoadAppConfig(silent, filePath, Opc.Ua.Security.SecuredApplication.FromApplicationType(InstallConfig.ApplicationType), ConfigurationType, false); + ApplicationConfiguration = configuration; + + if (configuration != null) + { + // configure HTTP access. + ConfigureHttpAccess(configuration, true); + + // delete certificate. + if (InstallConfig.DeleteCertificatesOnUninstall) + { + await DeleteApplicationInstanceCertificate(configuration); + } + } + + if (!NoGdsAgentAdmin) + { + try + { + string agentPath = Utils.GetAbsoluteDirectoryPath(ApplicationData.Current.LocalFolder.Path + "\\OPC Foundation\\GDS\\Applications", false, false, false); + + if (agentPath != null) + { + File.Delete(agentPath + "\\" + configuration.ApplicationName + ".xml"); + } + } + catch (Exception e) + { + Utils.Trace(Utils.TraceMasks.Error, "Could not create GDS agent configuration file: {0}", e.Message); + } + } + } + #endregion + + #region Static Methods + /// + /// Loads the configuration. + /// + public static async Task LoadAppConfig( + bool silent, + string filePath, + ApplicationType applicationType, + Type configurationType, + bool applyTraceSettings) + { + Utils.Trace(Utils.TraceMasks.Information, "Loading application configuration file. {0}", filePath); + + try + { + // load the configuration file. + ApplicationConfiguration configuration = await ApplicationConfiguration.Load( + new System.IO.FileInfo(filePath), + applicationType, + configurationType, + applyTraceSettings); + + if (configuration == null) + { + return null; + } + + return configuration; + } + catch (Exception e) + { + // warn user. + if (!silent && MessageDlg != null) + { + MessageDlg.Message("Load Application Configuration: " + e.Message); + await MessageDlg.ShowAsync(); + } + + Utils.Trace(e, "Could not load configuration file. {0}", filePath); + return null; + } + } + + /// + /// Loads the application configuration. + /// + public async Task LoadApplicationConfiguration(string filePath, bool silent) + { + ApplicationConfiguration configuration = await LoadAppConfig(silent, filePath, ApplicationType, ConfigurationType, true); + + if (configuration == null) + { + throw ServiceResultException.Create(StatusCodes.BadConfigurationError, "Could not load configuration file."); + } + + m_applicationConfiguration = configuration; + + return configuration; + } + + /// + /// Loads the application configuration. + /// + public async Task LoadApplicationConfiguration(bool silent) + { + string filePath = ApplicationConfiguration.GetFilePathFromAppConfig(ConfigSectionName); + ApplicationConfiguration configuration = await LoadAppConfig(silent, filePath, ApplicationType, ConfigurationType, true); + + if (configuration == null) + { + throw ServiceResultException.Create(StatusCodes.BadConfigurationError, "Could not load configuration file."); + } + + m_applicationConfiguration = configuration; + + return configuration; + } + + /// + /// Checks for a valid application instance certificate. + /// + /// if set to true no dialogs will be displayed. + /// Minimum size of the key. + public async Task CheckApplicationInstanceCertificate( + bool silent, + ushort minimumKeySize) + { + Utils.Trace(Utils.TraceMasks.Information, "Checking application instance certificate."); + + ApplicationConfiguration configuration = null; + + if (m_applicationConfiguration == null) + { + await LoadApplicationConfiguration(silent); + } + + configuration = m_applicationConfiguration; + bool dontCreateNewCertificate = true; + + // find the existing certificate. + CertificateIdentifier id = configuration.SecurityConfiguration.ApplicationCertificate; + + if (id == null) + { + throw ServiceResultException.Create(StatusCodes.BadConfigurationError, "Configuration file does not specify a certificate."); + } + + X509Certificate2 certificate = await id.Find(true); + + // check that it is ok. + if (certificate != null) + { + dontCreateNewCertificate = await CheckApplicationInstanceCertificate(configuration, certificate, silent, minimumKeySize); + } + else + { + // check for missing private key. + certificate = await id.Find(false); + + if (certificate != null) + { + throw ServiceResultException.Create(StatusCodes.BadConfigurationError, "Cannot access certificate private key. Subject={0}", certificate.Subject); + } + + // check for missing thumbprint. + if (!String.IsNullOrEmpty(id.Thumbprint)) + { + if (!String.IsNullOrEmpty(id.SubjectName)) + { + CertificateIdentifier id2 = new CertificateIdentifier(); + id2.StoreType = id.StoreType; + id2.StorePath = id.StorePath; + id2.SubjectName = id.SubjectName; + + certificate = await id2.Find(true); + } + + if (certificate != null) + { + string message = Utils.Format( + "Thumbprint was explicitly specified in the configuration." + + "\r\nAnother certificate with the same subject name was found." + + "\r\nUse it instead?\r\n" + + "\r\nRequested: {0}" + + "\r\nFound: {1}", + id.SubjectName, + certificate.Subject); + + throw ServiceResultException.Create(StatusCodes.BadConfigurationError, message); + } + else + { + string message = Utils.Format("Thumbprint was explicitly specified in the configuration. Cannot generate a new certificate."); + throw ServiceResultException.Create(StatusCodes.BadConfigurationError, message); + } + } + } + + // create a new certificate. + if (!dontCreateNewCertificate) + { + certificate = await CreateApplicationInstanceCertificate(configuration, minimumKeySize, 600); + } + else + { + if (certificate == null) + { + string message = Utils.Format( + "There is no cert with subject {0} in the configuration." + + "\r\n Copy cert to this location:" + + "\r\n{1}", + id.SubjectName, + id.StorePath); + throw ServiceResultException.Create(StatusCodes.BadConfigurationError, message); + } + + // ensure it is trusted. + await AddToTrustedStore(configuration, certificate); + } + + // add to discovery server. + if (configuration.ApplicationType == ApplicationType.Server || configuration.ApplicationType == ApplicationType.ClientAndServer) + { + try + { + await AddToDiscoveryServerTrustList(certificate, null, null, configuration.SecurityConfiguration.TrustedPeerCertificates); + } + catch (Exception e) + { + Utils.Trace(e, "Could not add certificate to LDS trust list."); + } + } + + return true; + } + #endregion + + #region HTTPS Support + /// + /// Uses the UA validation logic for HTTPS certificates. + /// + /// The validator. + public static void SetUaValidationForHttps(CertificateValidator validator) + { + m_validator = validator; + } + + /// + /// Remotes the certificate validate. + /// + private static bool HttpsCertificateValidation( + object sender, + X509Certificate2 cert, + System.Net.Security.SslPolicyErrors error) + { + try + { + m_validator.Validate(new X509Certificate2(cert.RawData)); + return true; + } + catch (Exception e) + { + Utils.Trace(e, "Could not verify SSL certificate: {0}", cert.Subject); + return false; + } + } + + private static CertificateValidator m_validator; + #endregion + + #region Private Methods + /// + /// Handles a certificate validation error. + /// + private static void CertificateValidator_CertificateValidation(CertificateValidator validator, CertificateValidationEventArgs e) + { + try + { + if (e.Error != null && e.Error.Code == StatusCodes.BadCertificateUntrusted) + { + e.Accept = true; + Utils.Trace((int)Utils.TraceMasks.Security, "Automatically accepted certificate: {0}", e.Certificate.Subject); + } + } + catch (Exception exception) + { + Utils.Trace(exception, "Error accepting certificate."); + } + } + + /// + /// Creates an application instance certificate if one does not already exist. + /// + private static async Task CheckApplicationInstanceCertificate( + ApplicationConfiguration configuration, + X509Certificate2 certificate, + bool silent, + ushort minimumKeySize) + { + if (certificate == null) + { + return false; + } + + Utils.Trace(Utils.TraceMasks.Information, "Checking application instance certificate. {0}", certificate.Subject); + + // validate certificate. + configuration.CertificateValidator.Validate(certificate); + + // check key size. + if (minimumKeySize > certificate.GetRSAPublicKey().KeySize) + { + bool valid = false; + + string message = Utils.Format( + "The key size ({0}) in the certificate is less than the minimum provided ({1}). Update certificate?", + certificate.GetRSAPublicKey().KeySize, + minimumKeySize); + + if (!silent && MessageDlg!=null) + { + MessageDlg.Message(message, true); + if (!await MessageDlg.ShowAsync()) + { + valid = true; + } + } + + Utils.Trace(message); + + if (!valid) + { + return false; + } + } + + // check domains. + if (configuration.ApplicationType != ApplicationType.Client) + { + return await CheckDomainsInCertificate(configuration, certificate, silent); + } + + // check uri. + string applicationUri = Utils.GetApplicationUriFromCertficate(certificate); + + if (String.IsNullOrEmpty(applicationUri)) + { + bool valid = false; + + string message = "The Application URI could not be read from the certificate. Use certificate anyway?"; + + if (!silent && MessageDlg != null) + { + MessageDlg.Message(message, true); + if (!await MessageDlg.ShowAsync()) + { + valid = true; + } + } + + Utils.Trace(message); + + if (!valid) + { + return false; + } + } + + // update configuration. + configuration.ApplicationUri = applicationUri; + configuration.SecurityConfiguration.ApplicationCertificate.Certificate = certificate; + + return true; + } + + /// + /// Checks that the domains in the server addresses match the domains in the certificates. + /// + private static async Task CheckDomainsInCertificate( + ApplicationConfiguration configuration, + X509Certificate2 certificate, + bool silent) + { + Utils.Trace(Utils.TraceMasks.Information, "Checking domains in certificate. {0}", certificate.Subject); + + bool valid = true; + IList serverDomainNames = configuration.GetServerDomainNames(); + IList certificateDomainNames = Utils.GetDomainsFromCertficate(certificate); + + // get computer name. + string computerName = Utils.GetHostName(); + + // get IP addresses. + IPAddress[] addresses = await Utils.GetHostAddresses(computerName); + + for (int ii = 0; ii < serverDomainNames.Count; ii++) + { + if (Utils.FindStringIgnoreCase(certificateDomainNames, serverDomainNames[ii])) + { + continue; + } + + if (String.Compare(serverDomainNames[ii], "localhost", StringComparison.OrdinalIgnoreCase) == 0) + { + if (Utils.FindStringIgnoreCase(certificateDomainNames, computerName)) + { + continue; + } + + // check for aliases. + bool found = false; + + // check for ip addresses. + for (int jj = 0; jj < addresses.Length; jj++) + { + if (Utils.FindStringIgnoreCase(certificateDomainNames, addresses[jj].ToString())) + { + found = true; + break; + } + } + + if (found) + { + continue; + } + } + + string message = Utils.Format( + "The server is configured to use domain '{0}' which does not appear in the certificate. Use certificate?", + serverDomainNames[ii]); + + valid = false; + + if (!silent && MessageDlg != null) + { + MessageDlg.Message(message, true); + if (await MessageDlg.ShowAsync()) + { + valid = true; + continue; + } + } + + Utils.Trace(message); + break; + } + + return valid; + } + + /// + /// Creates the application instance certificate. + /// + /// The configuration. + /// Size of the key. + /// The lifetime in months. + /// The new certificate + private static async Task CreateApplicationInstanceCertificate( + ApplicationConfiguration configuration, + ushort keySize, + ushort lifetimeInMonths) + { + Utils.Trace(Utils.TraceMasks.Information, "Creating application instance certificate. KeySize={0}, Lifetime={1}", keySize, lifetimeInMonths); + + // delete existing any existing certificate. + await DeleteApplicationInstanceCertificate(configuration); + + CertificateIdentifier id = configuration.SecurityConfiguration.ApplicationCertificate; + + // get the domains from the configuration file. + IList serverDomainNames = configuration.GetServerDomainNames(); + + if (serverDomainNames.Count == 0) + { + serverDomainNames.Add(Utils.GetHostName()); + } + + // ensure the certificate store directory exists. + if (id.StoreType == CertificateStoreType.Directory) + { + Utils.GetAbsoluteDirectoryPath(id.StorePath, true, true, true); + } + + X509Certificate2 certificate = await Opc.Ua.CertificateFactory.CreateCertificate( + id.StoreType, + id.StorePath, + configuration.ApplicationUri, + configuration.ApplicationName, + null, + serverDomainNames, + keySize, + lifetimeInMonths); + + id.Certificate = certificate; + await AddToTrustedStore(configuration, certificate); + + await configuration.CertificateValidator.Update(configuration.SecurityConfiguration); + + Utils.Trace(Utils.TraceMasks.Information, "Certificate created. Thumbprint={0}", certificate.Thumbprint); + + // reload the certificate from disk. + return await configuration.SecurityConfiguration.ApplicationCertificate.LoadPrivateKey(null); + } + + /// + /// Deletes an existing application instance certificate. + /// + /// The configuration instance that stores the configurable information for a UA application. + private static async Task DeleteApplicationInstanceCertificate(ApplicationConfiguration configuration) + { + Utils.Trace(Utils.TraceMasks.Information, "Deleting application instance certificate."); + + // create a default certificate id none specified. + CertificateIdentifier id = configuration.SecurityConfiguration.ApplicationCertificate; + + if (id == null) + { + return; + } + + // delete private key. + X509Certificate2 certificate = await id.Find(); + + // delete trusted peer certificate. + if (configuration.SecurityConfiguration != null && configuration.SecurityConfiguration.TrustedPeerCertificates != null) + { + string thumbprint = id.Thumbprint; + + if (certificate != null) + { + thumbprint = certificate.Thumbprint; + } + + using (ICertificateStore store = configuration.SecurityConfiguration.TrustedPeerCertificates.OpenStore()) + { + await store.Delete(thumbprint); + } + } + + // delete private key. + if (certificate != null) + { + using (ICertificateStore store = id.OpenStore()) + { + await store.Delete(certificate.Thumbprint); + } + } + } + + /// + /// Adds the application certificate to the discovery server trust list. + /// + public static async Task AddToDiscoveryServerTrustList( + X509Certificate2 certificate, + string oldThumbprint, + IList issuers, + CertificateStoreIdentifier trustedCertificateStore) + { + Utils.Trace(Utils.TraceMasks.Information, "Adding certificate to discovery server trust list."); + + try + { + string configurationPath = Utils.GetAbsoluteFilePath(ApplicationData.Current.LocalFolder.Path + @"\OPC Foundation\Config\Opc.Ua.DiscoveryServer.Config.xml", true, false, false); + + if (configurationPath == null) + { + throw new ServiceResultException("Could not find the discovery server configuration file. Please confirm that it is installed."); + } + + Opc.Ua.Security.SecuredApplication ldsConfiguration = new Opc.Ua.Security.SecurityConfigurationManager().ReadConfiguration(configurationPath); + CertificateStoreIdentifier csid = Opc.Ua.Security.SecuredApplication.FromCertificateStoreIdentifier(ldsConfiguration.TrustedCertificateStore); + await AddApplicationCertificateToStore(csid, certificate, oldThumbprint); + + if (issuers != null && ldsConfiguration.IssuerCertificateStore != null) + { + csid = Opc.Ua.Security.SecuredApplication.FromCertificateStoreIdentifier(ldsConfiguration.IssuerCertificateStore); + AddIssuerCertificatesToStore(csid, issuers); + } + + CertificateIdentifier cid = Opc.Ua.Security.SecuredApplication.FromCertificateIdentifier(ldsConfiguration.ApplicationCertificate); + X509Certificate2 ldsCertificate = await cid.Find(false); + + // add LDS certificate to application trust list. + if (ldsCertificate != null && trustedCertificateStore != null) + { + await AddApplicationCertificateToStore(csid, ldsCertificate, null); + } + } + catch (Exception e) + { + Utils.Trace(e, "Could not add certificate to discovery server trust list."); + } + } + + /// + /// Adds an application certificate to a store. + /// + private static async Task AddApplicationCertificateToStore( + CertificateStoreIdentifier csid, + X509Certificate2 certificate, + string oldThumbprint) + { + ICertificateStore store = csid.OpenStore(); + + try + { + // delete the old certificate. + if (oldThumbprint != null) + { + await store.Delete(oldThumbprint); + } + + // delete certificates with the same application uri. + if (store.FindByThumbprint(certificate.Thumbprint) == null) + { + string applicationUri = Utils.GetApplicationUriFromCertficate(certificate); + + // delete any existing certificates. + X509Certificate2Collection collection = await store.Enumerate(); + foreach (X509Certificate2 target in collection) + { + if (Utils.CompareDistinguishedName(target.Subject, certificate.Subject)) + { + if (Utils.GetApplicationUriFromCertficate(target) == applicationUri) + { + await store.Delete(target.Thumbprint); + } + } + } + + // add new certificate. + await store.Add(new X509Certificate2(certificate.RawData)); + } + } + finally + { + store.Close(); + } + } + + /// + /// Adds an application certificate to a store. + /// + private static void AddIssuerCertificatesToStore(CertificateStoreIdentifier csid, IList issuers) + { + ICertificateStore store = csid.OpenStore(); + + try + { + foreach (X509Certificate2 issuer in issuers) + { + if (store.FindByThumbprint(issuer.Thumbprint) == null) + { + store.Add(issuer); + } + } + } + finally + { + store.Close(); + } + } + + /// + /// Adds the certificate to the Trusted Certificate Store + /// + /// The application's configuration which specifies the location of the TrustedStore. + /// The certificate to register. + private static async Task AddToTrustedStore(ApplicationConfiguration configuration, X509Certificate2 certificate) + { + if (certificate == null) throw new ArgumentNullException("certificate"); + + string storePath = null; + + if (configuration != null && configuration.SecurityConfiguration != null && configuration.SecurityConfiguration.TrustedPeerCertificates != null) + { + storePath = configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath; + } + + if (String.IsNullOrEmpty(storePath)) + { + Utils.Trace(Utils.TraceMasks.Information, "WARNING: Trusted peer store not specified."); + return; + } + + try + { + ICertificateStore store = configuration.SecurityConfiguration.TrustedPeerCertificates.OpenStore(); + + if (store == null) + { + Utils.Trace("Could not open trusted peer store. StorePath={0}", storePath); + return; + } + + try + { + // check if it already exists. + X509Certificate2Collection existingCertificates = await store.FindByThumbprint(certificate.Thumbprint); + + if (existingCertificates.Count > 0) + { + return; + } + + Utils.Trace(Utils.TraceMasks.Information, "Adding certificate to trusted peer store. StorePath={0}", storePath); + + List subjectName = Utils.ParseDistinguishedName(certificate.Subject); + + // check for old certificate. + X509Certificate2Collection certificates = await store.Enumerate(); + + for (int ii = 0; ii < certificates.Count; ii++) + { + if (Utils.CompareDistinguishedName(certificates[ii], subjectName)) + { + if (certificates[ii].Thumbprint == certificate.Thumbprint) + { + return; + } + + await store.Delete(certificates[ii].Thumbprint); + break; + } + } + + // add new certificate. + X509Certificate2 publicKey = new X509Certificate2(certificate.RawData); + await store.Add(publicKey); + } + finally + { + store.Close(); + } + } + catch (Exception e) + { + Utils.Trace(e, "Could not add certificate to trusted peer store. StorePath={0}", storePath); + } + } + + /// + /// Configures the HTTP access. + /// + /// The configuration. + /// if set to true then the HTTP access should be removed. + private void ConfigureHttpAccess(ApplicationConfiguration configuration, bool remove) + { + Utils.Trace(Utils.TraceMasks.Information, "Configuring HTTP access."); + + // check for HTTP endpoints which need configuring. + StringCollection baseAddresses = new StringCollection(); + + if (configuration.DiscoveryServerConfiguration != null) + { + baseAddresses = configuration.DiscoveryServerConfiguration.BaseAddresses; + } + + if (configuration.ServerConfiguration != null) + { + baseAddresses = configuration.ServerConfiguration.BaseAddresses; + } + + // configure WCF http access. + for (int ii = 0; ii < baseAddresses.Count; ii++) + { + string url = GetHttpUrlForAccessRule(baseAddresses[ii]); + + if (url != null) + { + SetHttpAccessRules(url, remove); + } + } + } + + /// + /// Gets the HTTP URL to use for HTTP access rules. + /// + public static string GetHttpUrlForAccessRule(string baseAddress) + { + Uri url = Utils.ParseUri(baseAddress); + + if (url == null) + { + return null; + } + + UriBuilder builder = new UriBuilder(url); + + switch (url.Scheme) + { + case Utils.UriSchemeHttps: + { + builder.Path = String.Empty; + builder.Query = String.Empty; + break; + } + + case Utils.UriSchemeNoSecurityHttp: + { + builder.Scheme = Utils.UriSchemeHttp; + builder.Path = String.Empty; + builder.Query = String.Empty; + break; + } + + case Utils.UriSchemeHttp: + { + break; + } + + default: + { + return null; + } + } + + return builder.ToString(); + } + + /// + /// Gets the access rules to use for the application. + /// + private List GetAccessRules() + { + List rules = new List(); + + // check for rules specified in the installer configuration. + bool hasAdmin = false; + + if (InstallConfig.AccessRules != null) + { + for (int ii = 0; ii < InstallConfig.AccessRules.Count; ii++) + { + ApplicationAccessRule rule = InstallConfig.AccessRules[ii]; + + if (rule.Right == ApplicationAccessRight.Configure && rule.RuleType == AccessControlType.Allow) + { + hasAdmin = true; + break; + } + } + + rules = InstallConfig.AccessRules; + } + + // provide some default rules. + if (rules.Count == 0) + { + // give user run access. + ApplicationAccessRule rule = new ApplicationAccessRule(); + rule.RuleType = AccessControlType.Allow; + rule.Right = ApplicationAccessRight.Run; + rule.IdentityName = WellKnownSids.Users; + rules.Add(rule); + + // ensure service can access. + if (InstallConfig.InstallAsService) + { + rule = new ApplicationAccessRule(); + rule.RuleType = AccessControlType.Allow; + rule.Right = ApplicationAccessRight.Run; + rule.IdentityName = WellKnownSids.NetworkService; + rules.Add(rule); + + rule = new ApplicationAccessRule(); + rule.RuleType = AccessControlType.Allow; + rule.Right = ApplicationAccessRight.Run; + rule.IdentityName = WellKnownSids.LocalService; + rules.Add(rule); + } + } + + // ensure someone can change the configuration later. + if (!hasAdmin) + { + ApplicationAccessRule rule = new ApplicationAccessRule(); + rule.RuleType = AccessControlType.Allow; + rule.Right = ApplicationAccessRight.Configure; + rule.IdentityName = WellKnownSids.Administrators; + rules.Add(rule); + } + + return rules; + } + + /// + /// Sets the HTTP access rules for the URL. + /// + private void SetHttpAccessRules(string url, bool remove) + { + try + { + List rules = new List(); + + if (!remove) + { + rules = GetAccessRules(); + } + + HttpAccessRule.SetAccessRules(new Uri(url), rules, false); + } + catch (Exception e) + { + Utils.Trace(e, "Unexpected configuring the HTTP access rules."); + } + } + + /// + /// Configures access to the executable, the configuration file and the private key. + /// + private async Task ConfigureFileAccess(ApplicationConfiguration configuration) + { + Utils.Trace(Utils.TraceMasks.Information, "Configuring file access."); + + List rules = GetAccessRules(); + + // apply access rules to the excutable file. + try + { + if (InstallConfig.SetExecutableFilePermissions) + { + ApplicationAccessRule.SetAccessRules(InstallConfig.ExecutableFile, rules, true); + } + } + catch (Exception e) + { + Utils.Trace(e, "Could not set executable file permissions."); + } + + // apply access rules to the configuration file. + try + { + if (InstallConfig.SetConfigurationFilePermisions) + { + ApplicationAccessRule.SetAccessRules(configuration.SourceFilePath, rules, true); + } + } + catch (Exception e) + { + Utils.Trace(e, "Could not set configuration file permissions."); + } + + // apply access rules to the private key file. + try + { + X509Certificate2 certificate = await configuration.SecurityConfiguration.ApplicationCertificate.Find(true); + + if (certificate != null) + { + ICertificateStore store = configuration.SecurityConfiguration.ApplicationCertificate.OpenStore(); + store.SetAccessRules(certificate.Thumbprint, rules, true); + } + } + catch (Exception e) + { + Utils.Trace(e, "Could not set private key file permissions."); + } + } + #endregion + + #region Private Fields + private string m_applicationName; + private ApplicationType m_applicationType; + private string m_configSectionName; + private Type m_configurationType; + private InstalledApplication m_installConfig; + private ServerBase m_server; + private ApplicationConfiguration m_applicationConfiguration; + #endregion + } +} diff --git a/SampleApplications/SDK/Configuration/ConfigUtils.cs b/SampleApplications/SDK/Configuration/ConfigUtils.cs new file mode 100644 index 0000000000..a922541092 --- /dev/null +++ b/SampleApplications/SDK/Configuration/ConfigUtils.cs @@ -0,0 +1,1186 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Text; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; +using System.IO; +using System.Xml; +using Windows.Storage; +using System.Threading.Tasks; + +namespace Opc.Ua.Configuration +{ + /// + /// Utility functions used by COM applications. + /// + public static class ConfigUtils + { + /// + /// Gets or sets a directory which contains files representing users roles. + /// + /// + /// The write permissions on these files are used to determine which users are allowed to act in the role. + /// + public static string UserRoleDirectory { get; set; } + + /// + /// Gets the log file directory and ensures it is writeable. + /// + public static string GetLogFileDirectory() + { + // try the program data directory. + string logFileDirectory = ApplicationData.Current.LocalFolder.Path; + logFileDirectory += "\\OPC Foundation\\Logs"; + + try + { + // create the directory. + if (!Directory.Exists(logFileDirectory)) + { + Directory.CreateDirectory(logFileDirectory); + } + + // ensure everyone has write access to it. + List rules = new List(); + + ApplicationAccessRule rule = new ApplicationAccessRule(); + + rule.IdentityName = WellKnownSids.Users; + rule.Right = ApplicationAccessRight.Configure; + rule.RuleType = AccessControlType.Allow; + + rules.Add(rule); + + rule = new ApplicationAccessRule(); + + rule.IdentityName = WellKnownSids.NetworkService; + rule.Right = ApplicationAccessRight.Configure; + rule.RuleType = AccessControlType.Allow; + + rules.Add(rule); + + rule = new ApplicationAccessRule(); + + rule.IdentityName = WellKnownSids.LocalService; + rule.Right = ApplicationAccessRight.Configure; + rule.RuleType = AccessControlType.Allow; + + rules.Add(rule); + + ApplicationAccessRule.SetAccessRules(logFileDirectory, rules, false); + } + catch (Exception) + { + // try the MyDocuments directory instead. + logFileDirectory = ApplicationData.Current.LocalFolder.Path; + logFileDirectory += "OPC Foundation\\Logs"; + + if (!Directory.Exists(logFileDirectory)) + { + Directory.CreateDirectory(logFileDirectory); + } + } + + return logFileDirectory; + } + + /// + /// Finds the first child element with the specified name. + /// + private static XmlElement FindFirstElement(XmlElement parent, string localName, string namespaceUri) + { + if (parent == null) + { + return null; + } + + for (XmlNode child = parent.FirstChild; child != null; child = child.NextSibling) + { + XmlElement element = child as XmlElement; + + if (element != null) + { + if (element.LocalName == localName && element.NamespaceURI == namespaceUri) + { + return element; + } + + break; + } + } + + return null; + } + + /// + /// Updates the configuration location for the specified + /// + public static void UpdateConfigurationLocation(string executablePath, string configurationPath) + { + string configFilePath = Utils.Format("{0}.config", executablePath); + + // not all apps have an app.config file. + if (!File.Exists(configFilePath)) + { + return; + } + + // load from file. + XmlDocument document = new XmlDocument(); + document.Load(new FileStream(configFilePath, FileMode.Open)); + + for (XmlNode child = document.DocumentElement.FirstChild; child != null; child = child.NextSibling) + { + // ignore non-element. + XmlElement element = child as XmlElement; + + if (element == null) + { + continue; + } + + // look for the configuration location. + XmlElement location = FindFirstElement(element, "ConfigurationLocation", Namespaces.OpcUaConfig); + + if (location == null) + { + continue; + } + + // find the file path. + XmlElement filePath = FindFirstElement(location, "FilePath", Namespaces.OpcUaConfig); + + if (filePath == null) + { + filePath = location.OwnerDocument.CreateElement("FilePath", Namespaces.OpcUaConfig); + location.InsertBefore(filePath, location.FirstChild); + } + + filePath.InnerText = configurationPath; + break; + } + + // save configuration file. + Stream ostrm = File.Open(configFilePath, FileMode.Create, FileAccess.Write); + StreamWriter writer = new StreamWriter(ostrm, System.Text.Encoding.UTF8); + + try + { + document.Save(writer); + } + finally + { + writer.Flush(); + writer.Dispose(); + } + } + + /// + /// Sets the defaults for all fields. + /// + /// The application. + private static void SetDefaults(InstalledApplication application) + { + // create a default product name. + if (String.IsNullOrEmpty(application.ProductName)) + { + application.ProductName = application.ApplicationName; + } + + // create a default uri. + if (String.IsNullOrEmpty(application.ApplicationUri)) + { + application.ApplicationUri = Utils.Format("http://localhost/{0}/{1}", application.ApplicationName, Guid.NewGuid()); + } + + // make the uri specify the local machine. + application.ApplicationUri = Utils.ReplaceLocalhost(application.ApplicationUri); + + // set a default application store. + if (application.ApplicationCertificate == null) + { + application.ApplicationCertificate = new Opc.Ua.Security.CertificateIdentifier(); + application.ApplicationCertificate.StoreType = Utils.DefaultStoreType; + application.ApplicationCertificate.StorePath = ApplicationData.Current.LocalFolder.Path + "\\OPC Foundation\\CertificateStores\\MachineDefault"; + application.ApplicationCertificate.SubjectName = application.ApplicationName; + } + + if (application.IssuerCertificateStore == null) + { + application.IssuerCertificateStore = new Opc.Ua.Security.CertificateStoreIdentifier(); + application.IssuerCertificateStore.StoreType = Utils.DefaultStoreType; + application.IssuerCertificateStore.StorePath = ApplicationData.Current.LocalFolder.Path + "\\OPC Foundation\\CertificateStores\\MachineDefault"; + } + + if (application.TrustedCertificateStore == null) + { + application.TrustedCertificateStore = new Opc.Ua.Security.CertificateStoreIdentifier(); + application.TrustedCertificateStore.StoreType = Utils.DefaultStoreType; + application.TrustedCertificateStore.StorePath = ApplicationData.Current.LocalFolder.Path + "\\OPC Foundation\\CertificateStores\\MachineDefault"; + } + + try + { + Utils.GetAbsoluteDirectoryPath(application.ApplicationCertificate.StorePath, true, true, true); + } + catch (Exception e) + { + Utils.Trace("Could not access the machine directory: {0} '{1}'", application.ApplicationCertificate.StorePath, e); + } + + if (application.RejectedCertificatesStore == null) + { + application.RejectedCertificatesStore = new Opc.Ua.Security.CertificateStoreIdentifier(); + application.RejectedCertificatesStore.StoreType = CertificateStoreType.Directory; + application.RejectedCertificatesStore.StorePath = ApplicationData.Current.LocalFolder.Path + "\\OPC Foundation\\CertificateStores\\RejectedCertificates"; + } + + if (application.RejectedCertificatesStore.StoreType == CertificateStoreType.Directory) + { + try + { + Utils.GetAbsoluteDirectoryPath(application.RejectedCertificatesStore.StorePath, true, true, true); + } + catch (Exception e) + { + Utils.Trace("Could not access rejected certificates directory: {0} '{1}'", application.RejectedCertificatesStore.StorePath, e); + } + } + } + + /// + /// Installs a UA application. + /// + public static async Task InstallApplication( + InstalledApplication application, + bool autostart, + bool configureFirewall) + { + // validate the executable file. + string executableFile = Utils.GetAbsoluteFilePath(application.ExecutableFile, true, true, false); + + // get the default application name from the executable file. + FileInfo executableFileInfo = new FileInfo(executableFile); + + string applicationName = executableFileInfo.Name.Substring(0, executableFileInfo.Name.Length-4); + + // choose a default configuration file. + if (String.IsNullOrEmpty(application.ConfigurationFile)) + { + application.ConfigurationFile = Utils.Format( + "{0}\\{1}.Config.xml", + executableFileInfo.DirectoryName, + applicationName); + } + + // validate the configuration file. + string configurationFile = Utils.GetAbsoluteFilePath(application.ConfigurationFile, true, false, false); + + // create a new file if one does not exist. + bool useExisting = true; + + if (configurationFile == null) + { + configurationFile = Utils.GetAbsoluteFilePath(application.ConfigurationFile, true, true, true); + useExisting = false; + } + + // create the default configuration file. + + if (useExisting) + { + try + { + Opc.Ua.Security.SecuredApplication existingSettings = new Opc.Ua.Security.SecurityConfigurationManager().ReadConfiguration(configurationFile); + + // copy current settings + application.ApplicationType = existingSettings.ApplicationType; + application.BaseAddresses = existingSettings.BaseAddresses; + application.ApplicationCertificate = existingSettings.ApplicationCertificate; + application.ApplicationName = existingSettings.ApplicationName; + application.ProductName = existingSettings.ProductName; + application.RejectedCertificatesStore = existingSettings.RejectedCertificatesStore; + application.TrustedCertificateStore = existingSettings.TrustedCertificateStore; + application.TrustedCertificates = existingSettings.TrustedCertificates; + application.IssuerCertificateStore = existingSettings.IssuerCertificateStore; + application.IssuerCertificates = application.IssuerCertificates; + application.UseDefaultCertificateStores = false; + } + catch (Exception e) + { + useExisting = false; + Utils.Trace("WARNING. Existing configuration file could not be loaded: {0}.\r\nReplacing with default: {1}", e.Message, configurationFile); + File.Copy(configurationFile, configurationFile + ".bak", true); + } + } + + // create the configuration file from the default. + if (!useExisting) + { + try + { + string installationFile = Utils.Format( + "{0}\\Install\\{1}.Config.xml", + executableFileInfo.Directory.Parent.FullName, + applicationName); + + if (!File.Exists(installationFile)) + { + Utils.Trace("Could not find default configuation at: {0}", installationFile); + } + + File.Copy(installationFile, configurationFile, true); + Utils.Trace("File.Copy({0}, {1})", installationFile, configurationFile); + } + catch (Exception e) + { + Utils.Trace("Could not copy default configuation to: {0}. Error={1}.", configurationFile, e.Message); + } + } + + // create a default application name. + if (String.IsNullOrEmpty(application.ApplicationName)) + { + application.ApplicationName = applicationName; + } + + // create a default product name. + if (String.IsNullOrEmpty(application.ProductName)) + { + application.ProductName = application.ApplicationName; + } + + // create a default uri. + if (String.IsNullOrEmpty(application.ApplicationUri)) + { + application.ApplicationUri = Utils.Format("http://localhost/{0}/{1}", applicationName, Guid.NewGuid()); + } + + // make the uri specify the local machine. + application.ApplicationUri = Utils.ReplaceLocalhost(application.ApplicationUri); + + // set a default application store. + if (application.ApplicationCertificate == null) + { + application.ApplicationCertificate = new Opc.Ua.Security.CertificateIdentifier(); + application.ApplicationCertificate.StoreType = Utils.DefaultStoreType; + application.ApplicationCertificate.StorePath = ApplicationData.Current.LocalFolder.Path + "\\OPC Foundation\\CertificateStores\\MachineDefault"; + } + + if (application.UseDefaultCertificateStores) + { + if (application.IssuerCertificateStore == null) + { + application.IssuerCertificateStore = new Opc.Ua.Security.CertificateStoreIdentifier(); + application.IssuerCertificateStore.StoreType = Utils.DefaultStoreType; + application.IssuerCertificateStore.StorePath = ApplicationData.Current.LocalFolder.Path + "\\OPC Foundation\\CertificateStores\\MachineDefault"; + } + + if (application.TrustedCertificateStore == null) + { + application.TrustedCertificateStore = new Opc.Ua.Security.CertificateStoreIdentifier(); + application.TrustedCertificateStore.StoreType = Utils.DefaultStoreType; + application.TrustedCertificateStore.StorePath = ApplicationData.Current.LocalFolder.Path + "\\OPC Foundation\\CertificateStores\\MachineDefault"; + } + + try + { + Utils.GetAbsoluteDirectoryPath(application.TrustedCertificateStore.StorePath, true, true, true); + } + catch (Exception e) + { + Utils.Trace("Could not access the machine directory: {0} '{1}'", application.RejectedCertificatesStore.StorePath, e); + } + + if (application.RejectedCertificatesStore == null) + { + application.RejectedCertificatesStore = new Opc.Ua.Security.CertificateStoreIdentifier(); + application.RejectedCertificatesStore.StoreType = CertificateStoreType.Directory; + application.RejectedCertificatesStore.StorePath = ApplicationData.Current.LocalFolder.Path + "\\OPC Foundation\\CertificateStores\\RejectedCertificates"; + + StringBuilder buffer = new StringBuilder(); + + buffer.Append(ApplicationData.Current.LocalFolder.Path); + buffer.Append("\\OPC Foundation"); + buffer.Append("\\RejectedCertificates"); + + string folderPath = buffer.ToString(); + + if (!Directory.Exists(folderPath)) + { + Directory.CreateDirectory(folderPath); + } + } + } + + // check for valid certificate (discard invalid certificates). + CertificateIdentifier applicationCertificate = Opc.Ua.Security.SecuredApplication.FromCertificateIdentifier(application.ApplicationCertificate); + X509Certificate2 certificate = await applicationCertificate.Find(true); + + if (certificate == null) + { + certificate = await applicationCertificate.Find(false); + + if (certificate != null) + { + Utils.Trace( + "Found existing certificate but it does not have a private key: Store={0}, Certificate={1}", + application.ApplicationCertificate.StorePath, + application.ApplicationCertificate); + } + else + { + Utils.Trace( + "Existing certificate could not be found: Store={0}, Certificate={1}", + application.ApplicationCertificate.StorePath, + application.ApplicationCertificate); + } + } + + // check if no certificate exists. + if (certificate == null) + { + certificate = await CreateCertificateForApplication(application); + } + + // ensure the application certificate is in the trusted peers store. + try + { + CertificateStoreIdentifier certificateStore = Opc.Ua.Security.SecuredApplication.FromCertificateStoreIdentifier(application.TrustedCertificateStore); + + using (ICertificateStore store = certificateStore.OpenStore()) + { + X509Certificate2Collection peerCertificates = await store.FindByThumbprint(certificate.Thumbprint); + + if (peerCertificates.Count == 0) + { + await store.Add(new X509Certificate2(certificate.RawData)); + } + } + } + catch (Exception e) + { + Utils.Trace( + "Could not add certificate '{0}' to trusted peer store '{1}'. Error={2}", + certificate.Subject, + application.TrustedCertificateStore, + e.Message); + } + + // update configuration file location. + UpdateConfigurationLocation(executableFile, configurationFile); + + // update configuration file. + new Opc.Ua.Security.SecurityConfigurationManager().WriteConfiguration(configurationFile, application); + + ApplicationAccessRuleCollection accessRules = application.AccessRules; + bool noRulesDefined = application.AccessRules == null || application.AccessRules.Count == 0; + + // add the default access rules. + if (noRulesDefined) + { + ApplicationAccessRule rule = new ApplicationAccessRule(); + + rule.IdentityName = WellKnownSids.Administrators; + rule.RuleType = AccessControlType.Allow; + rule.Right = ApplicationAccessRight.Configure; + + accessRules.Add(rule); + + rule = new ApplicationAccessRule(); + + rule.IdentityName = WellKnownSids.Users; + rule.RuleType = AccessControlType.Allow; + rule.Right = ApplicationAccessRight.Update; + + accessRules.Add(rule); + } + + // ensure the service account has priviledges. + if (application.InstallAsService) + { + // check if a specific account is assigned. + AccountInfo accountInfo = null; + + if (!String.IsNullOrEmpty(application.ServiceUserName)) + { + accountInfo = AccountInfo.Create(application.ServiceUserName); + } + + // choose a built-in service account. + if (accountInfo == null) + { + accountInfo = AccountInfo.Create(WellKnownSids.NetworkService); + + if (accountInfo == null) + { + accountInfo = AccountInfo.Create(WellKnownSids.LocalSystem); + } + } + + ApplicationAccessRule rule = new ApplicationAccessRule(); + + rule.IdentityName = accountInfo.ToString(); + rule.RuleType = AccessControlType.Allow; + rule.Right = ApplicationAccessRight.Run; + + accessRules.Add(rule); + } + + // set the permissions for the HTTP endpoints used by the application. + if (configureFirewall && application.BaseAddresses != null && application.BaseAddresses.Count > 0) + { + for (int ii = 0; ii < application.BaseAddresses.Count; ii++) + { + Uri url = Utils.ParseUri(application.BaseAddresses[ii]); + + if (url != null) + { + try + { + HttpAccessRule.SetAccessRules(url, accessRules, true); + Utils.Trace("Added HTTP access rules for URL: {0}", url); + } + catch (Exception e) + { + Utils.Trace("Could not set HTTP access rules for URL: {0}. Error={1}", url, e.Message); + + for (int jj = 0; jj < accessRules.Count; jj++) + { + ApplicationAccessRule rule = accessRules[jj]; + + Utils.Trace( + (int)Utils.TraceMasks.Error, + "IdentityName={0}, Right={1}, RuleType={2}", + rule.IdentityName, + rule.Right, + rule.RuleType); + } + } + } + } + } + + // set permissions on the local certificate store. + SetCertificatePermissions( + application, + applicationCertificate, + accessRules, + false); + + // set permissions on the local certificate store. + if (application.RejectedCertificatesStore != null) + { + // need to grant full control to certificates in the RejectedCertificatesStore. + foreach (ApplicationAccessRule rule in accessRules) + { + if (rule.RuleType == AccessControlType.Allow) + { + rule.Right = ApplicationAccessRight.Configure; + } + } + + CertificateStoreIdentifier rejectedCertificates = Opc.Ua.Security.SecuredApplication.FromCertificateStoreIdentifier(application.RejectedCertificatesStore); + + using (ICertificateStore store = rejectedCertificates.OpenStore()) + { + if (store.SupportsAccessControl) + { + store.SetAccessRules(accessRules, false); + } + } + } + + + } + + /// + /// Creates a new certificate for application. + /// + /// The application. + private static async Task CreateCertificateForApplication(InstalledApplication application) + { + // build list of domains. + List domains = new List(); + + if (application.BaseAddresses != null) + { + foreach (string baseAddress in application.BaseAddresses) + { + Uri uri = Utils.ParseUri(baseAddress); + + if (uri != null) + { + string domain = uri.DnsSafeHost; + + if (String.Compare(domain, "localhost", StringComparison.OrdinalIgnoreCase) == 0) + { + domain = Utils.GetHostName(); + } + + if (!Utils.FindStringIgnoreCase(domains, domain)) + { + domains.Add(domain); + } + } + } + } + + // must at least of the localhost. + if (domains.Count == 0) + { + domains.Add(Utils.GetHostName()); + } + + // create the certificate. + X509Certificate2 certificate = await Opc.Ua.CertificateFactory.CreateCertificate( + application.ApplicationCertificate.StoreType, + application.ApplicationCertificate.StorePath, + application.ApplicationUri, + application.ApplicationName, + Utils.Format("CN={0}/DC={1}", application.ApplicationName, domains[0]), + domains, + 1024, + 300); + + CertificateIdentifier applicationCertificate = Opc.Ua.Security.SecuredApplication.FromCertificateIdentifier(application.ApplicationCertificate); + return await applicationCertificate.LoadPrivateKey(null); + } + + /// + /// Updates the access permissions for the certificate store. + /// + private static void SetCertificatePermissions( + Opc.Ua.Security.SecuredApplication application, + CertificateIdentifier id, + IList accessRules, + bool replaceExisting) + { + if (id == null || accessRules == null || accessRules.Count == 0) + { + return; + } + + try + { + using (ICertificateStore store = id.OpenStore()) + { + if (store.SupportsCertificateAccessControl) + { + store.SetAccessRules(id.Thumbprint, accessRules, replaceExisting); + } + } + } + catch (Exception e) + { + Utils.Trace("Could not set permissions for certificate store: {0}. Error={1}", id, e.Message); + + for (int jj = 0; jj < accessRules.Count; jj++) + { + ApplicationAccessRule rule = accessRules[jj]; + + Utils.Trace( + (int)Utils.TraceMasks.Error, + "IdentityName={0}, Right={1}, RuleType={2}", + rule.IdentityName, + rule.Right, + rule.RuleType); + } + } + } + + /// + /// Uninstalls a UA application. + /// + public static async Task UninstallApplication(InstalledApplication application) + { + // validate the executable file. + string executableFile = Utils.GetAbsoluteFilePath(application.ExecutableFile, true, true, false); + + // get the default application name from the executable file. + FileInfo executableFileInfo = new FileInfo(executableFile); + string applicationName = executableFileInfo.Name.Substring(0, executableFileInfo.Name.Length-4); + + // choose a default configuration file. + if (String.IsNullOrEmpty(application.ConfigurationFile)) + { + application.ConfigurationFile = Utils.Format( + "{0}\\{1}.Config.xml", + executableFileInfo.DirectoryName, + applicationName); + } + + // validate the configuration file. + string configurationFile = Utils.GetAbsoluteFilePath(application.ConfigurationFile, true, false, false); + + if (configurationFile != null) + { + // load the current configuration. + Opc.Ua.Security.SecuredApplication security = new Opc.Ua.Security.SecurityConfigurationManager().ReadConfiguration(configurationFile); + + // delete the application certificates. + if (application.DeleteCertificatesOnUninstall) + { + CertificateIdentifier id = Opc.Ua.Security.SecuredApplication.FromCertificateIdentifier(security.ApplicationCertificate); + + // delete public key from trusted peers certificate store. + try + { + CertificateStoreIdentifier certificateStore = Opc.Ua.Security.SecuredApplication.FromCertificateStoreIdentifier(security.TrustedCertificateStore); + + using (ICertificateStore store = certificateStore.OpenStore()) + { + X509Certificate2Collection peerCertificates = await store.FindByThumbprint(id.Thumbprint); + + if (peerCertificates.Count > 0) + { + await store.Delete(peerCertificates[0].Thumbprint); + } + } + } + catch (Exception e) + { + Utils.Trace("Could not delete certificate '{0}' from store. Error={1}", id, e.Message); + } + + // delete private key from application certificate store. + try + { + using (ICertificateStore store = id.OpenStore()) + { + await store.Delete(id.Thumbprint); + } + } + catch (Exception e) + { + Utils.Trace("Could not delete certificate '{0}' from store. Error={1}", id, e.Message); + } + + // permentently delete any UA defined stores if they are now empty. + try + { + WindowsCertificateStore store = new WindowsCertificateStore(); + await store.Open("LocalMachine\\UA Applications"); + + X509Certificate2Collection collection = await store.Enumerate(); + if (collection.Count == 0) + { + store.PermanentlyDeleteStore(); + } + } + catch (Exception e) + { + Utils.Trace("Could not delete certificate '{0}' from store. Error={1}", id, e.Message); + } + } + + // remove the permissions for the HTTP endpoints used by the application. + if (application.BaseAddresses != null && application.BaseAddresses.Count > 0) + { + List noRules = new List(); + + for (int ii = 0; ii < application.BaseAddresses.Count; ii++) + { + Uri url = Utils.ParseUri(application.BaseAddresses[ii]); + + if (url != null) + { + try + { + HttpAccessRule.SetAccessRules(url, noRules, true); + Utils.Trace("Removed HTTP access rules for URL: {0}", url); + } + catch (Exception e) + { + Utils.Trace("Could not remove HTTP access rules for URL: {0}. Error={1}", url, e.Message); + } + } + } + } + } + } + + /// + /// The category identifier for UA servers that are registered as COM servers on a machine. + /// + public static readonly Guid CATID_PseudoComServers = new Guid("899A3076-F94E-4695-9DF8-0ED25B02BDBA"); + + /// + /// The CLSID for the UA COM DA server host process (note: will be eventually replaced the proxy server). + /// + public static readonly Guid CLSID_UaComDaProxyServer = new Guid("B25384BD-D0DD-4d4d-805C-6E9F309F27C1"); + + /// + /// The CLSID for the UA COM AE server host process (note: will be eventually replaced the proxy server). + /// + public static readonly Guid CLSID_UaComAeProxyServer = new Guid("4DF1784C-085A-403d-AF8A-B140639B10B3"); + + /// + /// The CLSID for the UA COM HDA server host process (note: will be eventually replaced the proxy server). + /// + public static readonly Guid CLSID_UaComHdaProxyServer = new Guid("2DA58B69-2D85-4de0-A934-7751322132E2"); + + /// + /// COM servers that support the DA 2.0 specification. + /// + public static readonly Guid CATID_OPCDAServer20 = new Guid("63D5F432-CFE4-11d1-B2C8-0060083BA1FB"); + + /// + /// COM servers that support the DA 3.0 specification. + /// + public static readonly Guid CATID_OPCDAServer30 = new Guid("CC603642-66D7-48f1-B69A-B625E73652D7"); + + /// + /// COM servers that support the AE 1.0 specification. + /// + public static readonly Guid CATID_OPCAEServer10 = new Guid("58E13251-AC87-11d1-84D5-00608CB8A7E9"); + + /// + /// COM servers that support the HDA 1.0 specification. + /// + public static readonly Guid CATID_OPCHDAServer10 = new Guid("7DE5B060-E089-11d2-A5E6-000086339399"); + + /// + /// Creates an instance of a COM server. + /// + public static object CreateServer(Guid clsid) + { + COSERVERINFO coserverInfo = new COSERVERINFO(); + + coserverInfo.pwszName = null; + coserverInfo.pAuthInfo = IntPtr.Zero; + coserverInfo.dwReserved1 = 0; + coserverInfo.dwReserved2 = 0; + + GCHandle hIID = GCHandle.Alloc(IID_IUnknown, GCHandleType.Pinned); + + MULTI_QI[] results = new MULTI_QI[1]; + + results[0].iid = hIID.AddrOfPinnedObject(); + results[0].pItf = null; + results[0].hr = 0; + + try + { + // create an instance. + CoCreateInstanceEx( + ref clsid, + null, + CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER, + ref coserverInfo, + 1, + results); + } + finally + { + hIID.Free(); + } + + if (results[0].hr != 0) + { + throw new Exception("CoCreateInstanceEx: 0x{0:X8}" + results[0].hr); + } + + return results[0].pItf; + } + + /// + /// Registers the classes in the specified category. + /// + public static void RegisterClassInCategory(Guid clsid, Guid catid) + { + RegisterClassInCategory(clsid, catid, null); + } + + /// + /// Registers the classes in the specified category. + /// + public static void RegisterClassInCategory(Guid clsid, Guid catid, string description) + { + ICatRegister manager = (ICatRegister)CreateServer(CLSID_StdComponentCategoriesMgr); + + string existingDescription = null; + + try + { + ((ICatInformation)manager).GetCategoryDesc(catid, 0, out existingDescription); + } + catch (Exception e) + { + existingDescription = description; + + if (String.IsNullOrEmpty(existingDescription)) + { + if (catid == CATID_OPCDAServer20) + { + existingDescription = CATID_OPCDAServer20_Description; + } + else if (catid == CATID_OPCDAServer30) + { + existingDescription = CATID_OPCDAServer30_Description; + } + else if (catid == CATID_OPCAEServer10) + { + existingDescription = CATID_OPCAEServer10_Description; + } + else if (catid == CATID_OPCHDAServer10) + { + existingDescription = CATID_OPCHDAServer10_Description; + } + else + { + throw new Exception("No description for category available", e); + } + } + + CATEGORYINFO info; + + info.catid = catid; + info.lcid = 0; + info.szDescription = existingDescription; + + // register category. + manager.RegisterCategories(1, new CATEGORYINFO[] { info }); + } + + // register class in category. + manager.RegisterClassImplCategories(clsid, 1, new Guid[] { catid }); + } + + /// + /// Unregisters the classes in the specified category. + /// + public static void UnregisterClassInCategory(Guid clsid, Guid catid) + { + ICatRegister manager = (ICatRegister)CreateServer(CLSID_StdComponentCategoriesMgr); + manager.UnRegisterClassImplCategories(clsid, 1, new Guid[] { catid }); + } + + #region COM Interop Declarations + [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] + private struct COSERVERINFO + { + public uint dwReserved1; + [MarshalAs(UnmanagedType.LPWStr)] + public string pwszName; + public IntPtr pAuthInfo; + public uint dwReserved2; + }; + + [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] + private struct MULTI_QI + { + public IntPtr iid; + [MarshalAs(UnmanagedType.IUnknown)] + public object pItf; + public uint hr; + } + + private const uint CLSCTX_INPROC_SERVER = 0x1; + private const uint CLSCTX_INPROC_HANDLER = 0x2; + private const uint CLSCTX_LOCAL_SERVER = 0x4; + private const uint CLSCTX_REMOTE_SERVER = 0x10; + + private static readonly Guid IID_IUnknown = new Guid("00000000-0000-0000-C000-000000000046"); + + [DllImport("ole32.dll")] + private static extern void CoCreateInstanceEx( + ref Guid clsid, + [MarshalAs(UnmanagedType.IUnknown)] + object punkOuter, + uint dwClsCtx, + [In] + ref COSERVERINFO pServerInfo, + uint dwCount, + [In, Out] + MULTI_QI[] pResults); + + [ComImport] + [GuidAttribute("0002E000-0000-0000-C000-000000000046")] + [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)] + private interface IEnumGUID + { + void Next( + [MarshalAs(UnmanagedType.I4)] + int celt, + [Out] + IntPtr rgelt, + [Out][MarshalAs(UnmanagedType.I4)] + out int pceltFetched); + + void Skip( + [MarshalAs(UnmanagedType.I4)] + int celt); + + void Reset(); + + void Clone( + [Out] + out IEnumGUID ppenum); + } + + [ComImport] + [GuidAttribute("0002E013-0000-0000-C000-000000000046")] + [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)] + private interface ICatInformation + { + void EnumCategories( + int lcid, + [MarshalAs(UnmanagedType.Interface)] + [Out] out object ppenumCategoryInfo); + + void GetCategoryDesc( + [MarshalAs(UnmanagedType.LPStruct)] + Guid rcatid, + int lcid, + [MarshalAs(UnmanagedType.LPWStr)] + [Out] out string pszDesc); + + void EnumClassesOfCategories( + int cImplemented, + [MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.LPStruct, SizeParamIndex=0)] + Guid[] rgcatidImpl, + int cRequired, + [MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.LPStruct, SizeParamIndex=2)] + Guid[] rgcatidReq, + [MarshalAs(UnmanagedType.Interface)] + [Out] out object ppenumClsid); + + void IsClassOfCategories( + [MarshalAs(UnmanagedType.LPStruct)] + Guid rclsid, + int cImplemented, + [MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.LPStruct, SizeParamIndex=1)] + Guid[] rgcatidImpl, + int cRequired, + [MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.LPStruct, SizeParamIndex=3)] + Guid[] rgcatidReq); + + void EnumImplCategoriesOfClass( + [MarshalAs(UnmanagedType.LPStruct)] + Guid rclsid, + [MarshalAs(UnmanagedType.Interface)] + [Out] out object ppenumCatid); + + void EnumReqCategoriesOfClass( + [MarshalAs(UnmanagedType.LPStruct)] + Guid rclsid, + [MarshalAs(UnmanagedType.Interface)] + [Out] out object ppenumCatid); + } + + [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] + struct CATEGORYINFO + { + public Guid catid; + public int lcid; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst=127)] + public string szDescription; + } + + [ComImport] + [GuidAttribute("0002E012-0000-0000-C000-000000000046")] + [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)] + private interface ICatRegister + { + void RegisterCategories( + int cCategories, + [MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.LPStruct, SizeParamIndex=0)] + CATEGORYINFO[] rgCategoryInfo); + + void UnRegisterCategories( + int cCategories, + [MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.LPStruct, SizeParamIndex=0)] + Guid[] rgcatid); + + void RegisterClassImplCategories( + [MarshalAs(UnmanagedType.LPStruct)] + Guid rclsid, + int cCategories, + [MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.LPStruct, SizeParamIndex=1)] + Guid[] rgcatid); + + void UnRegisterClassImplCategories( + [MarshalAs(UnmanagedType.LPStruct)] + Guid rclsid, + int cCategories, + [MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.LPStruct, SizeParamIndex=1)] + Guid[] rgcatid); + + void RegisterClassReqCategories( + [MarshalAs(UnmanagedType.LPStruct)] + Guid rclsid, + int cCategories, + [MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.LPStruct, SizeParamIndex=1)] + Guid[] rgcatid); + + void UnRegisterClassReqCategories( + [MarshalAs(UnmanagedType.LPStruct)] + Guid rclsid, + int cCategories, + [MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.LPStruct, SizeParamIndex=1)] + Guid[] rgcatid); + } + + private static readonly Guid CLSID_StdComponentCategoriesMgr = new Guid("0002E005-0000-0000-C000-000000000046"); + + private const string CATID_OPCDAServer20_Description = "OPC Data Access Servers Version 2.0"; + private const string CATID_OPCDAServer30_Description = "OPC Data Access Servers Version 3.0"; + private const string CATID_OPCAEServer10_Description = "OPC Alarm & Event Server Version 1.0"; + private const string CATID_OPCHDAServer10_Description = "OPC History Data Access Servers Version 1.0"; + #endregion + + private const Int32 CRYPT_OID_INFO_OID_KEY = 1; + private const Int32 CRYPT_INSTALL_OID_INFO_BEFORE_FLAG = 1; + + [StructLayout(LayoutKind.Sequential)] + private class CRYPT_DATA_BLOB + { + public uint cbData; + public IntPtr pbData; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private class CRYPT_OID_INFO + { + public UInt32 cbSize; + [MarshalAs(UnmanagedType.LPStr)] + public String pszOID; + [MarshalAs(UnmanagedType.LPWStr)] + public String pwszName; + public UInt32 dwGroupId; + public Int32 dwValue; // Use either dwValue or Algid or dwLen - they are a union + public CRYPT_DATA_BLOB ExtraInfo; + }; + } +} diff --git a/SampleApplications/SDK/Configuration/Enum.cs b/SampleApplications/SDK/Configuration/Enum.cs new file mode 100644 index 0000000000..e625c8023e --- /dev/null +++ b/SampleApplications/SDK/Configuration/Enum.cs @@ -0,0 +1,344 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Text; +using System.Runtime.Serialization; + +namespace Opc.Ua.Configuration +{ + #region ServiceStatus Enum + /// + /// Represents the service status. + /// + public enum ServiceStatus + { + /// + /// The service is stopped + /// + Stopped, + /// + /// The service is going to process a start request + /// + StartPending, + /// + /// The service is going to process a stop request + /// + StopPending, + /// + /// The service started + /// + Running, + /// + /// The service is going to process a continue request + /// + ContinuePending, + /// + /// The service is going to process a pause request + /// + PausePending, + /// + /// The service is paused + /// + Paused, + /// + /// Unknown status + /// + Unknown + } + #endregion + + #region internal + + #region ServiceAccess Enum + + /// + /// Access to the service. Before granting the requested access, the + /// system checks the access token of the calling process. + /// + [Flags] + internal enum ServiceAccess : uint + { + /// + /// Required to call the QueryServiceConfig and + /// QueryServiceConfig2 functions to query the service configuration. + /// + QueryConfig = 0x00001, + + /// + /// Required to call the ChangeServiceConfig or ChangeServiceConfig2 function + /// to change the service configuration. Because this grants the caller + /// the right to change the executable file that the system runs, + /// it should be granted only to administrators. + /// + ChangeConfig = 0x00002, + + /// + /// Required to call the QueryServiceStatusEx function to ask the service + /// control manager about the status of the service. + /// + QueryStatus = 0x00004, + + /// + /// Required to call the EnumDependentServices function to enumerate all + /// the services dependent on the service. + /// + EnumerateDependents = 0x00008, + + /// + /// Required to call the StartService function to start the service. + /// + Start = 0x00010, + + /// + /// Required to call the ControlService function to stop the service. + /// + Stop = 0x00020, + + /// + /// Required to call the ControlService function to pause or continue + /// the service. + /// + PauseContinue = 0x00040, + + /// + /// Required to call the EnumDependentServices function to enumerate all + /// the services dependent on the service. + /// + Interrogate = 0x00080, + + /// + /// Required to call the ControlService function to specify a user-defined + /// control code. + /// + UserDefinedControl = 0x00100, + + /// + /// Includes STANDARD_RIGHTS_REQUIRED in addition to all access rights in this table. + /// + AllAccess = (ACCESS_MASK.STANDARD_RIGHTS_REQUIRED | + QueryConfig | + ChangeConfig | + QueryStatus | + EnumerateDependents | + Start | + Stop | + PauseContinue | + Interrogate | + UserDefinedControl), + + /// + /// Generic read + /// + GenericRead = ACCESS_MASK.STANDARD_RIGHTS_READ | + QueryConfig | + QueryStatus | + Interrogate | + EnumerateDependents, + + /// + /// Generic Write + /// + GenericWrite = ACCESS_MASK.STANDARD_RIGHTS_WRITE | + ChangeConfig, + + /// + /// Generic Execute + /// + GenericExecute = ACCESS_MASK.STANDARD_RIGHTS_EXECUTE | + Start | + Stop | + PauseContinue | + UserDefinedControl, + + /// + /// Required to call the QueryServiceObjectSecurity or + /// SetServiceObjectSecurity function to access the SACL. The proper + /// way to obtain this access is to enable the SE_SECURITY_NAME + /// privilege in the caller's current access token, open the handle + /// for ACCESS_SYSTEM_SECURITY access, and then disable the privilege. + /// + SystemSecurity = ACCESS_MASK.ACCESS_SYSTEM_SECURITY, + + /// + /// Required to call the DeleteService function to delete the service. + /// + Delete = ACCESS_MASK.DELETE, + + /// + /// Required to call the QueryServiceObjectSecurity function to query + /// the security descriptor of the service object. + /// + ReadControl = ACCESS_MASK.READ_CONTROL, + + /// + /// Required to call the SetServiceObjectSecurity function to modify + /// the Dacl member of the service object's security descriptor. + /// + WriteDac = ACCESS_MASK.WRITE_DAC, + + /// + /// Required to call the SetServiceObjectSecurity function to modify + /// the Owner and Group members of the service object's security + /// descriptor. + /// + WriteOwner = ACCESS_MASK.WRITE_OWNER, + } + + [Flags] + internal enum ACCESS_MASK : uint + { + DELETE = 0x00010000, + READ_CONTROL = 0x00020000, + WRITE_DAC = 0x00040000, + WRITE_OWNER = 0x00080000, + SYNCHRONIZE = 0x00100000, + + STANDARD_RIGHTS_REQUIRED = 0x000f0000, + + STANDARD_RIGHTS_READ = 0x00020000, + STANDARD_RIGHTS_WRITE = 0x00020000, + STANDARD_RIGHTS_EXECUTE = 0x00020000, + + STANDARD_RIGHTS_ALL = 0x001f0000, + + SPECIFIC_RIGHTS_ALL = 0x0000ffff, + + ACCESS_SYSTEM_SECURITY = 0x01000000, + + MAXIMUM_ALLOWED = 0x02000000, + + GENERIC_READ = 0x80000000, + GENERIC_WRITE = 0x40000000, + GENERIC_EXECUTE = 0x20000000, + GENERIC_ALL = 0x10000000, + + DESKTOP_READOBJECTS = 0x00000001, + DESKTOP_CREATEWINDOW = 0x00000002, + DESKTOP_CREATEMENU = 0x00000004, + DESKTOP_HOOKCONTROL = 0x00000008, + DESKTOP_JOURNALRECORD = 0x00000010, + DESKTOP_JOURNALPLAYBACK = 0x00000020, + DESKTOP_ENUMERATE = 0x00000040, + DESKTOP_WRITEOBJECTS = 0x00000080, + DESKTOP_SWITCHDESKTOP = 0x00000100, + + WINSTA_ENUMDESKTOPS = 0x00000001, + WINSTA_READATTRIBUTES = 0x00000002, + WINSTA_ACCESSCLIPBOARD = 0x00000004, + WINSTA_CREATEDESKTOP = 0x00000008, + WINSTA_WRITEATTRIBUTES = 0x00000010, + WINSTA_ACCESSGLOBALATOMS = 0x00000020, + WINSTA_EXITWINDOWS = 0x00000040, + WINSTA_ENUMERATE = 0x00000100, + WINSTA_READSCREEN = 0x00000200, + + WINSTA_ALL_ACCESS = 0x0000037f + } + + #endregion + + #region ServiceType Enum + /// + /// Service types. + /// + [Flags] + internal enum ServiceType : uint + { + /// + /// Driver service. + /// + KernelDriver = 0x00000001, + + /// + /// File system driver service. + /// + FileSystemDriver = 0x00000002, + + /// + /// Service that runs in its own process. + /// + OwnProcess = 0x00000010, + + /// + /// Service that shares a process with one or more other services. + /// + ShareProcess = 0x00000020, + + /// + /// The service can interact with the desktop. + /// + InteractiveProcess = 0x00000100, + } + + #endregion + + #region ServiceError Enum + + /// + /// Severity of the error, and action taken, if this service fails + /// to start. + /// + internal enum ServiceError + { + /// + /// The startup program ignores the error and continues the startup + /// operation. + /// + ErrorIgnore = 0x00000000, + + /// + /// The startup program logs the error in the event log but continues + /// the startup operation. + /// + ErrorNormal = 0x00000001, + + /// + /// The startup program logs the error in the event log. If the + /// last-known-good configuration is being started, the startup + /// operation continues. Otherwise, the system is restarted with + /// the last-known-good configuration. + /// + ErrorSevere = 0x00000002, + + /// + /// The startup program logs the error in the event log, if possible. + /// If the last-known-good configuration is being started, the startup + /// operation fails. Otherwise, the system is restarted with the + /// last-known good configuration. + /// + ErrorCritical = 0x00000003, + } + + + #endregion + + #endregion +} diff --git a/SampleApplications/SDK/Configuration/HostEnumerator.cs b/SampleApplications/SDK/Configuration/HostEnumerator.cs new file mode 100644 index 0000000000..75ce3d2aaa --- /dev/null +++ b/SampleApplications/SDK/Configuration/HostEnumerator.cs @@ -0,0 +1,234 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Text; +using System.Threading; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace Opc.Ua.Configuration +{ + /// + /// Enumerates the hosts available on the network. + /// + public class HostEnumerator + { + #region Constructors + /// + /// Creates the object. + /// + public HostEnumerator() + { + } + #endregion + + #region Public Methods + /// + /// Raised when a batch of hosts has been discovered (called from a background thread). + /// + public event EventHandler HostsDiscovered + { + add { m_HostsDiscovered += value; } + remove { m_HostsDiscovered -= value; } + } + + /// + /// Starts enumerating the hosts. + /// + public void Start(string domain) + { + Interlocked.Exchange(ref m_stopped, 0); + Interlocked.Exchange(ref m_domain, domain); + Task.Run(() => + { + OnEnumerate(null); + }); + } + + /// + /// Stops enumerating the hosts. + /// + public void Stop() + { + Interlocked.Exchange(ref m_stopped, 1); + } + #endregion + + #region Private Methods + [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] + private struct SERVER_INFO_100 + { + public uint sv100_platform_id; + [MarshalAs(UnmanagedType.LPWStr)] + public string sv100_name; + } + + private const uint LEVEL_SERVER_INFO_100 = 100; + private const uint LEVEL_SERVER_INFO_101 = 101; + + private const int MAX_PREFERRED_LENGTH = -1; + + private const uint SV_TYPE_WORKSTATION = 0x00000001; + private const uint SV_TYPE_SERVER = 0x00000002; + + [DllImport("Netapi32.dll")] + private static extern int NetServerEnum( + IntPtr servername, + uint level, + out IntPtr bufptr, + int prefmaxlen, + out int entriesread, + out int totalentries, + uint servertype, + [MarshalAs(UnmanagedType.LPWStr)] + string domain, + IntPtr resume_handle); + + [DllImport("Netapi32.dll")] + private static extern int NetApiBufferFree(IntPtr buffer); + + private const int ERROR_MORE_DATA = 234; + private const int NERR_Success = 0; + + /// + /// Enumerates computers on the local network. + /// + private void OnEnumerate(object state) + { + IntPtr pInfo; + + int entriesRead = 0; + int totalEntries = 0; + int resumeHandle = 0; + int result = ERROR_MORE_DATA; + + GCHandle dwResumeHandle = GCHandle.Alloc(resumeHandle, GCHandleType.Pinned); + + try + { + while (m_stopped == 0 && result == ERROR_MORE_DATA) + { + // enumerate the first batch of servers. + result = NetServerEnum( + IntPtr.Zero, + LEVEL_SERVER_INFO_100, + out pInfo, + MAX_PREFERRED_LENGTH, + out entriesRead, + out totalEntries, + SV_TYPE_WORKSTATION | SV_TYPE_SERVER, + m_domain, + dwResumeHandle.AddrOfPinnedObject()); + + // check for fatal error. + if ((result != NERR_Success) && (result != ERROR_MORE_DATA)) + { + Utils.Trace("Could not enumerate hosts on network. Error = {0}", result); + return; + } + + // copy host names from the returned structures. + string[] hostnames = new string[entriesRead]; + + IntPtr pos = pInfo; + + for (int ii = 0; ii < entriesRead; ii++) + { + SERVER_INFO_100 info = (SERVER_INFO_100)Marshal.PtrToStructure(pos); + + hostnames[ii] = info.sv100_name; + + pos = (IntPtr)(pos.ToInt64() + Marshal.SizeOf()); + } + + NetApiBufferFree(pInfo); + + // raise an event. + if (m_stopped == 0 && m_HostsDiscovered != null) + { + try + { + m_HostsDiscovered(this, new HostEnumeratorEventArgs(hostnames)); + } + catch (Exception e) + { + Utils.Trace(e, "Unexpected exception raising HostsDiscovered event."); + } + } + } + } + catch (Exception e) + { + Utils.Trace(e, "Unexpected exception calling NetServerEnum."); + } + finally + { + if (dwResumeHandle.IsAllocated) + { + dwResumeHandle.Free(); + } + } + } + #endregion + + #region HostEnumeratorEventArgs Class + private int m_stopped; + private string m_domain; + private event EventHandler m_HostsDiscovered; + #endregion + } + + #region HostEnumeratorEventArgs Class + /// + /// The arguments provided when a batch of hosts is discovered. + /// + public class HostEnumeratorEventArgs : EventArgs + { + /// + /// Initializes the object with a batch of host names. + /// + public HostEnumeratorEventArgs(IList hostnames) + { + m_hostnames = hostnames; + } + + /// + /// The list of hostnames found. + /// + public IList Hostnames + { + get { return m_hostnames; } + } + + private IList m_hostnames; + } + #endregion +} diff --git a/SampleApplications/SDK/Configuration/LocalSecurityPolicy.cs b/SampleApplications/SDK/Configuration/LocalSecurityPolicy.cs new file mode 100644 index 0000000000..9a3c61b81d --- /dev/null +++ b/SampleApplications/SDK/Configuration/LocalSecurityPolicy.cs @@ -0,0 +1,298 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Text; +using System.Runtime.InteropServices; +using System.ComponentModel; + +namespace Opc.Ua.Configuration +{ + /// + /// Allows to add privileges to Local Security Policy. + /// You can use this class to add the LogOn as service privilege to an account. + /// + public class LocalSecurityPolicy : IDisposable + { + #region DllImport + + [DllImport("advapi32.dll", PreserveSig = true)] + private static extern UInt32 LsaOpenPolicy( + ref LSA_UNICODE_STRING SystemName, + ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, + Int32 DesiredAccess, + out IntPtr PolicyHandle + ); + + [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] + private static extern long LsaAddAccountRights( + IntPtr PolicyHandle, + IntPtr AccountSid, + LSA_UNICODE_STRING[] UserRights, + long CountOfRights); + + [DllImport("advapi32")] + private static extern void FreeSid(IntPtr pSid); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true, PreserveSig = true)] + private static extern bool LookupAccountName( + string lpSystemName, string lpAccountName, + IntPtr psid, + ref int cbsid, + StringBuilder domainName, ref int cbdomainLength, ref int use); + + [DllImport("advapi32.dll")] + private static extern bool IsValidSid(IntPtr pSid); + + [DllImport("advapi32.dll")] + private static extern long LsaClose(IntPtr ObjectHandle); + + [DllImport("kernel32.dll")] + private static extern int GetLastError(); + + [DllImport("advapi32.dll")] + private static extern long LsaNtStatusToWinError(long status); + + + #endregion + + #region Struct + + [StructLayout(LayoutKind.Sequential)] + private struct LSA_UNICODE_STRING + { + public UInt16 Length; + public UInt16 MaximumLength; + public IntPtr Buffer; + } + + [StructLayout(LayoutKind.Sequential)] + private struct LSA_OBJECT_ATTRIBUTES + { + public int Length; + public IntPtr RootDirectory; + public LSA_UNICODE_STRING ObjectName; + public UInt32 Attributes; + public IntPtr SecurityDescriptor; + public IntPtr SecurityQualityOfService; + } + + #endregion + + #region Enum + + private enum LSA_AccessPolicy : long + { + POLICY_VIEW_LOCAL_INFORMATION = 0x00000001L, + POLICY_VIEW_AUDIT_INFORMATION = 0x00000002L, + POLICY_GET_PRIVATE_INFORMATION = 0x00000004L, + POLICY_TRUST_ADMIN = 0x00000008L, + POLICY_CREATE_ACCOUNT = 0x00000010L, + POLICY_CREATE_SECRET = 0x00000020L, + POLICY_CREATE_PRIVILEGE = 0x00000040L, + POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080L, + POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100L, + POLICY_AUDIT_LOG_ADMIN = 0x00000200L, + POLICY_SERVER_ADMIN = 0x00000400L, + POLICY_LOOKUP_NAMES = 0x00000800L, + POLICY_NOTIFICATION = 0x00001000L + } + + #endregion + + #region Const + + const uint STATUS_ACCESS_DENIED = 0xc0000022; + const uint STATUS_INSUFFICIENT_RESOURCES = 0xc000009a; + const uint STATUS_NO_MEMORY = 0xc0000017; + + #endregion + + private IntPtr lsaHandle; + + /// + /// Constructor for + /// + public LocalSecurityPolicy() + : this(null) + { } + + /// + /// Constructor for + /// + /// local system if systemName is null + public LocalSecurityPolicy(string systemName) + { + lsaHandle = IntPtr.Zero; + LSA_UNICODE_STRING system = InitLsaString(systemName); + + + //combine all policies + int access = (int)( + LSA_AccessPolicy.POLICY_AUDIT_LOG_ADMIN | + LSA_AccessPolicy.POLICY_CREATE_ACCOUNT | + LSA_AccessPolicy.POLICY_CREATE_PRIVILEGE | + LSA_AccessPolicy.POLICY_CREATE_SECRET | + LSA_AccessPolicy.POLICY_GET_PRIVATE_INFORMATION | + LSA_AccessPolicy.POLICY_LOOKUP_NAMES | + LSA_AccessPolicy.POLICY_NOTIFICATION | + LSA_AccessPolicy.POLICY_SERVER_ADMIN | + LSA_AccessPolicy.POLICY_SET_AUDIT_REQUIREMENTS | + LSA_AccessPolicy.POLICY_SET_DEFAULT_QUOTA_LIMITS | + LSA_AccessPolicy.POLICY_TRUST_ADMIN | + LSA_AccessPolicy.POLICY_VIEW_AUDIT_INFORMATION | + LSA_AccessPolicy.POLICY_VIEW_LOCAL_INFORMATION + ); + //initialize a pointer for the policy handle + IntPtr policyHandle = IntPtr.Zero; + + //these attributes are not used, but LsaOpenPolicy wants them to exists + LSA_OBJECT_ATTRIBUTES ObjectAttributes = new LSA_OBJECT_ATTRIBUTES(); + ObjectAttributes.Length = 0; + ObjectAttributes.RootDirectory = IntPtr.Zero; + ObjectAttributes.Attributes = 0; + ObjectAttributes.SecurityDescriptor = IntPtr.Zero; + ObjectAttributes.SecurityQualityOfService = IntPtr.Zero; + + //get a policy handle + uint ret = LsaOpenPolicy(ref system, ref ObjectAttributes, access, out lsaHandle); + if (ret == 0) + return; + if (ret == STATUS_ACCESS_DENIED) + { + throw new UnauthorizedAccessException(); + } + if ((ret == STATUS_INSUFFICIENT_RESOURCES) || (ret == STATUS_NO_MEMORY)) + { + throw new OutOfMemoryException(); + } + throw new Win32Exception((int)LsaNtStatusToWinError(ret)); + } + + + /// + /// Add privileges for the given account + /// + /// The account name (domain\userName) + /// The name of the privilege to add + public void AddPrivilege(string account, string privilege) + { + IntPtr pSid = GetSIDInformation(account); + LSA_UNICODE_STRING[] privileges = new LSA_UNICODE_STRING[1]; + privileges[0] = InitLsaString(privilege); + long ret = LsaAddAccountRights(lsaHandle, pSid, privileges, 1); + if (ret != 0)//ret = 0 Success + { + if (ret == STATUS_ACCESS_DENIED) + throw new UnauthorizedAccessException(); + if ((ret == STATUS_INSUFFICIENT_RESOURCES) || (ret == STATUS_NO_MEMORY)) + throw new OutOfMemoryException(); + + throw new Win32Exception((int)LsaNtStatusToWinError((int)ret)); + } + } + + /// + /// Add the privilege for the given account to logon as service. + /// + /// The account name (domain\userName) + public void AddLogonAsServicePrivilege(string account) + { + AddPrivilege(account, "SeServiceLogonRight"); + } + + + /// + /// Release all unmanaged resources. + /// + public void Dispose() + { + if (lsaHandle != IntPtr.Zero) + { + LsaClose(lsaHandle); + lsaHandle = IntPtr.Zero; + } + + GC.SuppressFinalize(this); + } + /// + /// + /// + ~LocalSecurityPolicy() + { Dispose(); } + + + #region helper functions + + IntPtr GetSIDInformation(string account) + { + //pointer an size for the SID + IntPtr sid = IntPtr.Zero; + int sidSize = 0; + //StringBuilder and size for the domain name + StringBuilder domainName = new StringBuilder(); + int nameSize = 0; + //account-type variable for lookup + int accountType = 0; + + //get required buffer size + LookupAccountName(String.Empty, account, sid, ref sidSize, domainName, ref nameSize, ref accountType); + + //allocate buffers + domainName = new StringBuilder(nameSize); + sid = Marshal.AllocHGlobal(sidSize); + + //lookup the SID for the account + bool result = LookupAccountName(String.Empty, account, sid, ref sidSize, domainName, ref nameSize, ref accountType); + if(!result) + { + Marshal.ThrowExceptionForHR(GetLastError()); + } + return sid; + } + + private static LSA_UNICODE_STRING InitLsaString(string s) + { + if (string.IsNullOrEmpty(s)) + return new LSA_UNICODE_STRING(); + + // Unicode strings max. 32KB + if (s.Length > 0x7ffe) + throw new ArgumentException("String too long"); + LSA_UNICODE_STRING lus = new LSA_UNICODE_STRING(); + lus.Buffer = Marshal.StringToHGlobalUni(s); + lus.Length = (UInt16)(s.Length * sizeof(char)); + lus.MaximumLength = (UInt16)((s.Length + 1) * sizeof(char)); + return lus; + } + + #endregion + } + +} diff --git a/SampleApplications/SDK/Configuration/ManagedApplication.cs b/SampleApplications/SDK/Configuration/ManagedApplication.cs new file mode 100644 index 0000000000..410f544b02 --- /dev/null +++ b/SampleApplications/SDK/Configuration/ManagedApplication.cs @@ -0,0 +1,431 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Text; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization; +using System.Xml; +using Windows.Storage; +using System.Threading.Tasks; + +namespace Opc.Ua.Configuration +{ + /// + /// An application that is managed by the configuration tool. + /// + [DataContract(Namespace = Namespaces.OpcUaConfig)] + public class ManagedApplication + { + /// + /// Initializes a new instance of the class. + /// + public ManagedApplication() + { + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + if (!String.IsNullOrEmpty(m_displayName)) + { + return m_displayName; + } + + if (!String.IsNullOrEmpty(m_executablePath)) + { + return new FileInfo(m_executablePath).Name; + } + + if (!String.IsNullOrEmpty(m_configurationPath)) + { + return new FileInfo(m_configurationPath).Name; + } + + return String.Empty; + } + + /// + /// Gets the source file. + /// + /// The source file. + public FileInfo SourceFile + { + get { return m_sourceFile; } + } + + /// + /// Gets a value indicating whether this application is SDK compatible. + /// + /// + /// true if this application is SDK compatible; otherwise, false. + /// + public bool IsSdkCompatible + { + get { return m_isSdkCompatible; } + } + + /// + /// Gets the application. + /// + /// The application. + public Opc.Ua.Security.SecuredApplication Application + { + get { return m_application; } + } + + /// + /// Gets or sets the display name. + /// + /// The display name. + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 0)] + public string DisplayName + { + get { return m_displayName; } + set { m_displayName = value; } + } + + /// + /// Gets or sets the executable path. + /// + /// The executable path. + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 1)] + public string ExecutablePath + { + get { return m_executablePath; } + set { m_executablePath = value; } + } + + /// + /// Gets or sets the configuration path. + /// + /// The configuration path. + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 2)] + public string ConfigurationPath + { + get { return m_configurationPath; } + set { m_configurationPath = value; } + } + + /// + /// Gets or sets the certificate. + /// + /// The certificate. + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 3)] + public CertificateIdentifier Certificate + { + get { return m_certificate; } + set { m_certificate = value; } + } + + /// + /// Gets or sets the trust list. + /// + /// The trust list. + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 4)] + public CertificateStoreIdentifier TrustList + { + get { return m_trustList; } + set { m_trustList = value; } + } + + /// + /// Gets or sets the trust list. + /// + /// The trust list. + [DataMember(IsRequired = false, EmitDefaultValue = false, Order = 5)] + public StringCollection BaseAddresses + { + get { return m_baseAddresses; } + set { m_baseAddresses = value; } + } + + /// + /// Loads the specified file path. + /// + /// The file path. + /// + public static ManagedApplication Load(string filePath) + { + using (Stream istrm = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite)) + { + DataContractSerializer serializer = new DataContractSerializer(typeof(ManagedApplication)); + ManagedApplication application = (ManagedApplication)serializer.ReadObject(istrm); + application.m_sourceFile = new FileInfo(filePath); + + if (String.IsNullOrEmpty(application.DisplayName)) + { + string name = application.m_sourceFile.Name; + int index = name.LastIndexOf('.'); + + if (index > 0) + { + name = name.Substring(0, index); + } + + application.DisplayName = name; + } + + application.LoadSdkConfigFile(); + return application; + } + } + + /// + /// Saves the specified file path. + /// + /// The file path. Uses the original source file path if not provided. + public void Save(string filePath) + { + if (String.IsNullOrEmpty(filePath)) + { + if (m_sourceFile == null) + { + filePath = Utils.GetAbsoluteDirectoryPath("%LocalApplicationData%\\OPC Foundation\\Applications\\", false, false, true); + filePath += m_displayName; + filePath += "*.xml"; + } + else + { + filePath = m_sourceFile.FullName; + } + } + + using (Stream ostrm = File.Open(filePath, FileMode.Create)) + { + DataContractSerializer serializer = new DataContractSerializer(typeof(ManagedApplication)); + serializer.WriteObject(ostrm, this); + } + + m_sourceFile = new FileInfo(filePath); + } + + /// + /// Sets the executable file. + /// + /// The file path. + public void SetExecutableFile(string filePath) + { + if (String.IsNullOrEmpty(filePath)) + { + m_executablePath = null; + return; + } + + m_executablePath = filePath; + m_configurationPath = null; + m_isSdkCompatible = false; + m_application = null; + + FileInfo executableFile = new FileInfo(m_executablePath); + m_displayName = executableFile.Name.Substring(0, executableFile.Name.Length-4); + + FileInfo configFile = new FileInfo(executableFile.FullName + ".config"); + Utils.Trace(1, "APPCONFIG={0}", configFile); + + if (configFile.Exists) + { + // save the .NET config file. + m_configurationPath = configFile.FullName; + + // look for the UA SDK config file. + string configurationPath = GetConfigFileFromAppConfig(configFile); + Utils.Trace(1, "UACONFIG={0}", configurationPath); + + if (configurationPath != null) + { + m_configurationPath = configurationPath; + } + else + { + m_configurationPath = configFile.FullName; + } + + LoadSdkConfigFile(); + } + + // set display name. + if (m_sourceFile == null || String.IsNullOrEmpty(m_displayName)) + { + string name = executableFile.Name; + int index = name.LastIndexOf('.'); + + if (index > 0) + { + name = name.Substring(0, index); + } + + m_displayName = name; + } + } + + /// + /// Tries to loads the SDK config file. + /// + private void LoadSdkConfigFile() + { + m_isSdkCompatible = false; + m_application = null; + + if (String.IsNullOrEmpty(m_configurationPath)) + { + return; + } + + FileInfo executablePath = new FileInfo(m_executablePath); + string currentDirectory = ApplicationData.Current.LocalFolder.Path; + + try + { + m_application = GetApplicationSettings(m_configurationPath); + + if (m_application != null) + { + m_isSdkCompatible = true; + m_certificate = Opc.Ua.Security.SecuredApplication.FromCertificateIdentifier(m_application.ApplicationCertificate); + m_trustList = Opc.Ua.Security.SecuredApplication.FromCertificateStoreIdentifier(m_application.TrustedCertificateStore); + m_application.ExecutableFile = m_executablePath; + m_configurationPath = m_application.ConfigurationFile; + } + } + + // ignore errors. + catch (Exception) + { + m_application = null; + } + } + + /// + /// Sets the configuration file. + /// + /// The file path. + public void SetConfigurationFile(string filePath) + { + if (String.IsNullOrEmpty(filePath)) + { + m_configurationPath = null; + return; + } + + m_configurationPath = filePath; + + FileInfo configFile = new FileInfo(filePath); + m_isSdkCompatible = false; + m_application = null; + m_configurationPath = configFile.FullName; + + if (configFile.Exists) + { + LoadSdkConfigFile(); + } + } + + /// + /// Reloads the configuration from disk. + /// + public void Reload() + { + LoadSdkConfigFile(); + } + + /// + /// Gets the application secuirty settings from a file. + /// + private Opc.Ua.Security.SecuredApplication GetApplicationSettings(string filePath) + { + string absolutePath = Utils.GetAbsoluteFilePath(filePath, true, false, false); + + if (absolutePath == null) + { + return null; + } + + return new Opc.Ua.Security.SecurityConfigurationManager().ReadConfiguration(absolutePath); + } + + /// + /// Gets the config file location from app config. + /// + private string GetConfigFileFromAppConfig(FileInfo appConfigFile) + { + try + { + StreamReader reader = new StreamReader(appConfigFile.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); + XmlDocument doc = new XmlDocument(); + doc.Load(reader); + + try + { + foreach( XmlNode node1 in doc.ChildNodes) + { + if (node1.Name == "ConfigurationLocation") + { + foreach (XmlNode node2 in node1.ChildNodes) + { + if (node2.Name == "FilePath") + { + return node2.InnerXml; + } + } + } + } + + return null; + } + finally + { + reader.Dispose(); + } + } + catch (Exception) + { + return null; + } + } + + #region Private Fields + private FileInfo m_sourceFile; + private bool m_isSdkCompatible; + private Opc.Ua.Security.SecuredApplication m_application; + private string m_displayName; + private string m_executablePath; + private string m_configurationPath; + private CertificateIdentifier m_certificate; + private CertificateStoreIdentifier m_trustList; + private StringCollection m_baseAddresses; + #endregion + } +} diff --git a/SampleApplications/SDK/Configuration/Opc.Ua.Configuration.csproj b/SampleApplications/SDK/Configuration/Opc.Ua.Configuration.csproj new file mode 100644 index 0000000000..02ea042bb4 --- /dev/null +++ b/SampleApplications/SDK/Configuration/Opc.Ua.Configuration.csproj @@ -0,0 +1,151 @@ + + + + + Debug + AnyCPU + {26A56753-9326-48C3-95BB-11594293B7A8} + Library + Properties + Opc.Ua.Configuration + Opc.Ua.Configuration + en-US + UAP + 10.0.10240.0 + 10.0.10240.0 + 14 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + ARM + true + bin\ARM\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + ARM + false + prompt + true + + + ARM + bin\ARM\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + ARM + false + prompt + true + + + x64 + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x64 + false + prompt + true + + + x64 + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x64 + false + prompt + true + + + x86 + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x86 + false + prompt + true + + + x86 + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x86 + false + prompt + true + + + + + + Designer + + + + + + + + + + + + + + InstalledApplication.xsd + + + + + + + {aa9d8d17-5dbd-4e77-8496-e32177573bf3} + Opc.Ua.Core + + + + 14.0 + + + + \ No newline at end of file diff --git a/SampleApplications/SDK/Configuration/Properties/AssemblyInfo.cs b/SampleApplications/SDK/Configuration/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..49aacac409 --- /dev/null +++ b/SampleApplications/SDK/Configuration/Properties/AssemblyInfo.cs @@ -0,0 +1,64 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Opc.Ua.Configuration.dll")] +[assembly: AssemblyDescription("UA Configuration Library")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("OPC Foundation")] +[assembly: AssemblyProduct("UA Configuration Library")] +[assembly: AssemblyCopyright(AssemblyVersionInfo.Copyright)] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("e16782b4-68c1-4f61-8ac2-1cf5465406e3")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion(AssemblyVersionInfo.CurrentVersion)] +[assembly: AssemblyFileVersion(AssemblyVersionInfo.CurrentFileVersion)] diff --git a/SampleApplications/SDK/Configuration/Properties/AssemblyVersionInfo.cs b/SampleApplications/SDK/Configuration/Properties/AssemblyVersionInfo.cs new file mode 100644 index 0000000000..0b0b5ea13e --- /dev/null +++ b/SampleApplications/SDK/Configuration/Properties/AssemblyVersionInfo.cs @@ -0,0 +1,47 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 The OPC Foundation, Inc. All rights reserved. + * + * OPC Reciprocal Community License ("RCL") Version 1.00 + * + * Unless explicitly acquired and licensed from Licensor under another + * license, the contents of this file are subject to the Reciprocal + * Community License ("RCL") Version 1.00, or subsequent versions + * as allowed by the RCL, and You may not copy or use this file in either + * source code or executable form, except in compliance with the terms and + * conditions of the RCL. + * + * All software distributed under the RCL is provided strictly on an + * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, + * AND LICENSOR HEREBY DISCLAIMS ALL SUCH WARRANTIES, INCLUDING WITHOUT + * LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE, QUIET ENJOYMENT, OR NON-INFRINGEMENT. See the RCL for specific + * language governing rights and limitations under the RCL. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/RCL/1.00/ + * ======================================================================*/ + +using System; +using System.Collections.Generic; +using System.Text; + +/// +/// Defines string constants for SDK version information. +/// +internal static class AssemblyVersionInfo +{ + /// + /// The current copy right notice. + /// + public const string Copyright = "Copyright © 2004-2013 OPC Foundation, Inc"; + + /// + /// The current build version. + /// + public const string CurrentVersion = "1.02.334.6"; + + /// + /// The current build file version. + /// + public const string CurrentFileVersion = "1.02.334.6"; +} diff --git a/SampleApplications/SDK/Configuration/Schema/InstalledApplication.cs b/SampleApplications/SDK/Configuration/Schema/InstalledApplication.cs new file mode 100644 index 0000000000..ae76dc1421 --- /dev/null +++ b/SampleApplications/SDK/Configuration/Schema/InstalledApplication.cs @@ -0,0 +1,242 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Text; +using System.Runtime.Serialization; +using System.IO; +using System.Xml; +using System.Reflection; + +namespace Opc.Ua.Configuration +{ + /// + /// Specifies how to configure an application during installation. + /// + [DataContract(Namespace = Namespaces.OpcUaSdk + "Installation.xsd")] + public partial class InstalledApplication : Opc.Ua.Security.SecuredApplication + { + #region Constructors + /// + /// The default constructor. + /// + public InstalledApplication() + { + Initialize(); + } + + /// + /// Called by the .NET framework during deserialization. + /// + [OnDeserializing] + private void Initialize(StreamingContext context) + { + Initialize(); + } + + /// + /// Sets private members to default values. + /// + private void Initialize() + { + UseDefaultCertificateStores = true; + DeleteCertificatesOnUninstall = true; + ConfigureFirewall = false; + SetConfigurationFilePermisions = true; + SetExecutableFilePermissions = true; + InstallAsService = false; + ServiceStartMode = StartMode.Manual; + ServiceUserName = null; + ServicePassword = null; + ServiceDescription = null; + LocallyRegisterOIDs = false; + MinimumKeySize = 1024; + LifeTimeInMonths = 300; + } + #endregion + + #region Persistent Properties + /// + /// Whether to use the default stores. + /// + [DataMember(IsRequired = false, Order = 1)] + public bool UseDefaultCertificateStores { get; set; } + + /// + /// Whether to delete certificates on uninstall. + /// + [DataMember(IsRequired = false, Order = 2)] + public bool DeleteCertificatesOnUninstall { get; set; } + + /// + /// Whether to configure the firewall. + /// + [DataMember(IsRequired = false, Order = 3)] + public bool ConfigureFirewall { get; set; } + + /// + /// Whether to set configuration file permissions. + /// + [DataMember(IsRequired = false, Order = 4)] + public bool SetConfigurationFilePermisions { get; set; } + + /// + /// Whether to set configuration file permissions. + /// + [DataMember(IsRequired = false, Order = 5)] + public bool SetExecutableFilePermissions { get; set; } + + /// + /// Whether to install as a service. + /// + [DataMember(IsRequired = false, Order = 6)] + public bool InstallAsService { get; set; } + + /// + /// The start mode for the service. + /// + [DataMember(IsRequired = false, Order = 7)] + public StartMode ServiceStartMode { get; set; } + + /// + /// The user name for the service. + /// + [DataMember(IsRequired = false, Order = 8)] + public string ServiceUserName { get; set; } + + /// + /// The password for the service. + /// + [DataMember(IsRequired = false, Order = 9)] + public string ServicePassword { get; set; } + + /// + /// A human readable description for the service. + /// + [DataMember(IsRequired = false, Order = 10)] + public string ServiceDescription { get; set; } + + /// + /// Whether to locally register OIDs (use to work around a windows bug when in a domain). + /// + [DataMember(IsRequired = false, Order = 11)] + public bool LocallyRegisterOIDs { get; set; } + + /// + /// The minimum key size for the new certificate. + /// + [DataMember(IsRequired = false, Order = 12)] + public ushort MinimumKeySize { get; set; } + + /// + /// The lifetime for the new certificate. + /// + [DataMember(IsRequired = false, Order = 13)] + public ushort LifeTimeInMonths { get; set; } + + /// + /// Who has access to the critical files. + /// + [DataMember(IsRequired = false, Order = 14)] + public ApplicationAccessRuleCollection AccessRules { get; set; } + + /// + /// The trace configuration for the installed process. + /// + [DataMember(IsRequired = false, Order = 15)] + public TraceConfiguration TraceConfiguration { get; set; } + #endregion + } + + #region InstalledApplicationCollection Class + /// + /// A collection of InstalledApplication objects. + /// + [CollectionDataContract(Name = "ListOfInstalledApplication", Namespace = Namespaces.OpcUaConfig, ItemName = "InstalledApplication")] + public partial class InstalledApplicationCollection : List + { + #region Constructors + /// + /// Initializes the collection with default values. + /// + public InstalledApplicationCollection() { } + + /// + /// Initializes the collection with an initial capacity. + /// + public InstalledApplicationCollection(int capacity) : base(capacity) { } + + /// + /// Initializes the collection with another collection. + /// + public InstalledApplicationCollection(IEnumerable collection) : base(collection) { } + #endregion + } + #endregion + + #region StartMode Enum + /// + /// Start mode of the Windows service + /// + [DataContract(Namespace = Namespaces.OpcUaSdk + "Installation.xsd")] + public enum StartMode : uint + { + /// + /// Device driver started by the operating system loader (valid only for driver services). + /// + [EnumMember] + Boot = 0x00000000, + + /// + /// Device driver started by the operating system initialization process. This value is valid only for driver services. + /// + [EnumMember] + System = 0x00000001, + + /// + /// Service to be started automatically during system startup. + /// + [EnumMember] + Auto = 0x00000002, + + /// + /// Service to be started manually by a call to the StartService method. + /// + [EnumMember] + Manual = 0x00000003, + + /// + /// Service that can no longer be started. + /// + [EnumMember] + Disabled = 0x00000004 + } + #endregion +} diff --git a/SampleApplications/SDK/Configuration/Schema/InstalledApplication.xsd b/SampleApplications/SDK/Configuration/Schema/InstalledApplication.xsd new file mode 100644 index 0000000000..bf65797078 --- /dev/null +++ b/SampleApplications/SDK/Configuration/Schema/InstalledApplication.xsd @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SampleApplications/SDK/Configuration/Schema/InstalledApplicationHelper.cs b/SampleApplications/SDK/Configuration/Schema/InstalledApplicationHelper.cs new file mode 100644 index 0000000000..d6be7a05eb --- /dev/null +++ b/SampleApplications/SDK/Configuration/Schema/InstalledApplicationHelper.cs @@ -0,0 +1,93 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Runtime.Serialization; +using System.IO; +using Windows.Storage; + +namespace Opc.Ua.Configuration +{ + /// + /// Specifies how to configure an application during installation. + /// + public partial class InstalledApplication + { + #region Public Methods + /// + /// Loads the application configuration from a configuration section. + /// + public static InstalledApplicationCollection Load(string filePath) + { + FileInfo file = new FileInfo(filePath); + + // look in current directory. + if (!file.Exists) + { + file = new FileInfo(Utils.Format("{0}\\{1}", ApplicationData.Current.LocalFolder.Path, filePath)); + } + + // look in executable directory. + if (!file.Exists) + { + file = new FileInfo(Utils.GetAbsoluteFilePath(filePath)); + } + + // file not found. + if (!file.Exists) + { + throw ServiceResultException.Create( + StatusCodes.BadConfigurationError, + "File does not exist: {0}\r\nCurrent directory is: {1}", + filePath, + ApplicationData.Current.LocalFolder.Path); + } + + return Load(file); + } + + /// + /// Loads a collection of security applications. + /// + public static InstalledApplicationCollection Load(FileInfo file) + { + FileStream reader = file.Open(FileMode.Open, FileAccess.Read); + + try + { + DataContractSerializer serializer = new DataContractSerializer(typeof(InstalledApplicationCollection)); + return serializer.ReadObject(reader) as InstalledApplicationCollection; + } + finally + { + reader.Dispose(); + } + } + #endregion + } +} diff --git a/SampleApplications/SDK/Configuration/Service.cs b/SampleApplications/SDK/Configuration/Service.cs new file mode 100644 index 0000000000..51a3e6a692 --- /dev/null +++ b/SampleApplications/SDK/Configuration/Service.cs @@ -0,0 +1,194 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Text; +using System.Runtime.Serialization; + +namespace Opc.Ua.Configuration +{ + #region Service class + /// + /// Represents a windows service + /// + public class Service + { + #region Constructor + /// + /// Constructor for + /// + public Service() + { + } + + /// + /// Constructor for + /// + /// The service name. + public Service(string name) + { + this.m_name = name; + } + #endregion + + #region Public Properties + /// + /// The service name (Windows identifier for the service) + /// + public string Name + { + get { return m_name; } + set { m_name = value; } + } + + /// + /// The service Display name (the friendly name showed by the Windows Service manager). + /// + public string DisplayName + { + get { return m_displayName; } + set { m_displayName = value; } + } + + /// + /// The service caption (usually equals to display name) + /// + public string Caption + { + get { return m_caption; } + set { m_caption = value; } + } + + /// + /// The service local path + /// + public string Path + { + get { return m_path; } + set { m_path = value; } + } + + /// + /// The service start mode. + /// + public StartMode StartMode + { + get { return m_startMode; } + set { m_startMode = value; } + } + + /// + /// Account name under which a service runs. + /// Depending on the service type, the account name may be in the form of DomainName\Username + /// + public string Account + { + get { return m_account; } + set { m_account = value; } + } + + /// + /// The service description. + /// + public string Description + { + get { return m_description; } + set { m_description = value; } + } + + /// + /// The processor affinity for this service. + /// + /// + /// If the system has 2 processor and the service is running on processor 2 the affinity bit mask will be : [true][false] + /// If the system has 2 processor and the service is running on both processors the affinity bit mask will be : [true][true] + /// + public bool[] ProcessorAffinity + { + get { return m_processorAffinity; } + set { m_processorAffinity = value; } + } + + /// + /// Indicates whether the service can be paused + /// + public bool AcceptPause + { + get { return m_acceptPause; } + set { m_acceptPause = value; } + } + + /// + /// Indicates whether the service can be stopped + /// + public bool AcceptStop + { + get { return m_acceptStop; } + set { m_acceptStop = value; } + } + #endregion + + #region Dynamic Properties + /// + /// The service process. Zero if not running. + /// + public int ProcessId + { + get { return m_processId; } + set { m_processId = value; } + } + + /// + /// The service status. + /// + public ServiceStatus Status + { + get { return m_status; } + set { m_status = value; } + } + #endregion + + #region Private Fields + private string m_name = null; + private string m_displayName = null; + private string m_caption = null; + private string m_path = null; + private int m_processId = 0; + private StartMode m_startMode = StartMode.Auto; + private ServiceStatus m_status = ServiceStatus.Unknown; + private string m_account = null; + private string m_description = null; + private bool[] m_processorAffinity = null; + private bool m_acceptPause = true; + private bool m_acceptStop = true; + #endregion + } + #endregion +} diff --git a/SampleApplications/SDK/Configuration/project.json b/SampleApplications/SDK/Configuration/project.json new file mode 100644 index 0000000000..ae7afafb44 --- /dev/null +++ b/SampleApplications/SDK/Configuration/project.json @@ -0,0 +1,16 @@ +{ + "dependencies": { + "Microsoft.NETCore.UniversalWindowsPlatform": "5.0.0" + }, + "frameworks": { + "uap10.0": {} + }, + "runtimes": { + "win10-arm": {}, + "win10-arm-aot": {}, + "win10-x86": {}, + "win10-x86-aot": {}, + "win10-x64": {}, + "win10-x64-aot": {} + } +} \ No newline at end of file diff --git a/SampleApplications/SDK/Controls/Common (OLD)/BaseListCtrl.xaml b/SampleApplications/SDK/Controls/Common (OLD)/BaseListCtrl.xaml new file mode 100644 index 0000000000..b8f4913a9f --- /dev/null +++ b/SampleApplications/SDK/Controls/Common (OLD)/BaseListCtrl.xaml @@ -0,0 +1,12 @@ + + + + + diff --git a/SampleApplications/SDK/Controls/Common (OLD)/BaseListCtrl.xaml.cs b/SampleApplications/SDK/Controls/Common (OLD)/BaseListCtrl.xaml.cs new file mode 100644 index 0000000000..768a587f0c --- /dev/null +++ b/SampleApplications/SDK/Controls/Common (OLD)/BaseListCtrl.xaml.cs @@ -0,0 +1,631 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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; +using System.Collections.Generic; +using System.ComponentModel; +using Windows.Foundation; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; + +namespace Opc.Ua.Client.Controls +{ + /// + /// A base class for list controls. + /// + public partial class BaseListCtrl : UserControl + { + /// + /// The ListView contained in the control. + /// + protected ListView ItemsLV; + + #region Public Interface + /// + /// Initializes a new instance of the class. + /// + public BaseListCtrl() + { + InitializeComponent(); + ItemsLV = _ItemsLV; + } + + /// + /// Whether the control should allow items to be dragged. + /// + [DefaultValue(false)] + public bool EnableDragging + { + get { return m_enableDragging; } + set { m_enableDragging = value; } + } + + /// + /// The instructions to display when no items are in the list. + /// + public string Instructions + { + get { return m_instructions; } + set { m_instructions = value; } + } + + /// + /// Whether new items should be pre-pended to the list. + /// + [DefaultValue(false)] + public bool PrependItems + { + get { return m_prependItems; } + set { m_prependItems = value; } + } + + /// + /// Raised whenever items are 'picked' in the control. + /// + public event ListItemActionEventHandler ItemsPicked + { + add { m_ItemsPicked += value; } + remove { m_ItemsPicked -= value; } + } + + /// + /// Raised whenever items are selected in the control. + /// + public event ListItemActionEventHandler ItemsSelected + { + add { m_ItemsSelected += value; } + remove { m_ItemsSelected -= value; } + } + + /// + /// Raised whenever items are added to the control. + /// + public event ListItemActionEventHandler ItemsAdded + { + add { m_ItemsAdded += value; } + remove { m_ItemsAdded -= value; } + } + + /// + /// Raised whenever items are modified in the control. + /// + public event ListItemActionEventHandler ItemsModified + { + add { m_ItemsModified += value; } + remove { m_ItemsModified -= value; } + } + + /// + /// Raised whenever items are removed from the control. + /// + public event ListItemActionEventHandler ItemsRemoved + { + add { m_ItemsRemoved += value; } + remove { m_ItemsRemoved -= value; } + } + + /// + /// Returns the number of items in the control. + /// + public int Count + { + get { return ItemsLV.Items.Count; } + } + + /// + /// Returns the objects associated with the items in the control. + /// + public Array GetItems(System.Type type) + { + ArrayList items = new ArrayList(); + + foreach (ListViewItem listItem in ItemsLV.Items) + { + items.Add(listItem.Tag); + } + + return items.ToArray(type); + } + + /// + /// Returns the objects associated with the selected items in the control. + /// + public Array GetSelectedItems(System.Type type) + { + ArrayList items = new ArrayList(); + + foreach (ListViewItem listItem in ItemsLV.SelectedItems) + { + items.Add(listItem.Tag); + } + + return items.ToArray(type); + } + #endregion + + #region Private Members + private bool m_prependItems; + private event ListItemActionEventHandler m_ItemsPicked; + private event ListItemActionEventHandler m_ItemsSelected; + private event ListItemActionEventHandler m_ItemsAdded; + private event ListItemActionEventHandler m_ItemsModified; + private event ListItemActionEventHandler m_ItemsRemoved; + private bool m_updating; + private int m_updateCount; + private string m_instructions; + private bool m_enableDragging; + #endregion + + #region Protected Methods + /// + /// Returns tag of the selected item. Null if no items or more than one item is selected. + /// + protected object SelectedTag + { + get + { + if (ItemsLV.SelectedItems.Count != 1) + { + return null; + } + + return ((ListViewItem) ItemsLV.SelectedItems[0]).Tag; + } + } + + /// + /// Deletes the currently selected items. + /// + protected virtual void DeleteSelection() + { + foreach (ListViewItem item in ItemsLV.Items) + { + if (ItemsLV.SelectedItems.Contains(item)) + { + ItemsLV.Items.Remove(item); + } + } + } + + /// + /// Compares two items in the list. + /// + protected virtual int CompareItems(object item1, object item2) + { + IComparable comparable = item1 as IComparable; + + if (comparable != null) + { + return comparable.CompareTo(item2); + } + + return 0; + } + + /// + /// Returns the data to drag. + /// + protected virtual object GetDataToDrag() + { + if (ItemsLV.SelectedItems.Count > 0) + { + ArrayList data = new ArrayList(); + + foreach (ListViewItem listItem in ItemsLV.SelectedItems) + { + data.Add(listItem.Tag); + } + + return data.ToArray(); + } + + return null; + } + + /// + /// Adds an item to the list. + /// + protected virtual ListViewItem AddItem(object item) + { + return AddItem(item, "SimpleItem", -1); + } + + /// + /// Adds an item to the list. + /// + protected virtual ListViewItem AddItem(object item, string icon, int index) + { + ListViewItem listItem = null; + + if (m_updating) + { + if (m_updateCount < ItemsLV.Items.Count) + { + listItem = (ListViewItem) ItemsLV.Items[m_updateCount]; + } + + m_updateCount++; + } + + if (listItem == null) + { + listItem = new ListViewItem(); + } + + listItem.Name = String.Format("{0}", item); + listItem.Tag = item; + + // calculate new index. + int newIndex = index; + + if (index < 0 || index > ItemsLV.Items.Count) + { + newIndex = ItemsLV.Items.Count; + } + + // update columns. + UpdateItem(listItem, item, newIndex); + + if (listItem.Parent == null) + { + // add to control. + if (index >= 0 && index <= ItemsLV.Items.Count) + { + ItemsLV.Items.Insert(index, listItem); + } + else + { + ItemsLV.Items.Add(listItem); + } + } + + // return new item. + return listItem; + } + + /// + /// Starts overwriting the contents of the control. + /// + protected void BeginUpdate() + { + m_updating = true; + m_updateCount = 0; + } + + /// + /// Finishes overwriting the contents of the control. + /// + protected void EndUpdate() + { + m_updating = false; + + while (ItemsLV.Items.Count > m_updateCount) + { + ItemsLV.Items.Remove(ItemsLV.Items[ItemsLV.Items.Count-1]); + } + + m_updateCount = 0; + } + + /// + /// Updates a list item with the current contents of an object. + /// + protected virtual void UpdateItem(ListViewItem listItem, object item) + { + listItem.Tag = item; + } + + /// + /// Updates a list item with the current contents of an object. + /// + protected virtual void UpdateItem(ListViewItem listItem, object item, int index) + { + UpdateItem(listItem, item); + } + + /// + /// Enables the state of menu items. + /// + protected virtual void EnableMenuItems(ListViewItem clickedItem) + { + // do nothing. + } + + /// + /// Sends notifications whenever items in the control are 'picked'. + /// + protected virtual void PickItems() + { + if (m_ItemsPicked != null) + { + ICollection data = GetDataToDrag() as ICollection; + + if (data != null) + { + m_ItemsPicked(this, new ListItemActionEventArgs(ListItemAction.Picked, data)); + } + } + } + + /// + /// Sends notifications whenever items in the control are 'selected'. + /// + protected virtual void SelectItems() + { + if (m_ItemsSelected != null) + { + object[] selectedObjects = new object[ItemsLV.SelectedItems.Count]; + + for (int ii = 0; ii < selectedObjects.Length; ii++) + { + selectedObjects[ii] = ((ListViewItem) ItemsLV.SelectedItems[ii]).Tag; + } + + m_ItemsSelected(this, new ListItemActionEventArgs(ListItemAction.Selected, selectedObjects)); + } + } + + /// + /// Sends notifications that an item has been added to the control. + /// + protected virtual void NotifyItemAdded(object item) + { + NotifyItemsAdded(new object[] { item }); + } + + /// + /// Sends notifications that items have been added to the control. + /// + protected virtual void NotifyItemsAdded(object[] items) + { + if (m_ItemsAdded != null && items != null && items.Length > 0) + { + m_ItemsAdded(this, new ListItemActionEventArgs(ListItemAction.Added, items)); + } + } + + /// + /// Sends notifications that an item has been modified in the control. + /// + protected virtual void NotifyItemModified(object item) + { + NotifyItemsModified(new object[] { item }); + } + + /// + /// Sends notifications that items have been modified in the control. + /// + protected virtual void NotifyItemsModified(object[] items) + { + if (m_ItemsModified != null && items != null && items.Length > 0) + { + m_ItemsModified(this, new ListItemActionEventArgs(ListItemAction.Modified, items)); + } + } + + /// + /// Sends notifications that and item has been removed from the control. + /// + protected virtual void NotifyItemRemoved(object item) + { + NotifyItemsRemoved(new object[] { item }); + } + + /// + /// Sends notifications that items have been removed from the control. + /// + protected virtual void NotifyItemsRemoved(object[] items) + { + if (m_ItemsRemoved != null && items != null && items.Length > 0) + { + m_ItemsRemoved(this, new ListItemActionEventArgs(ListItemAction.Removed, items)); + } + } + + /// + /// Finds the list item with specified tag in the control, + /// + protected ListViewItem FindItem(object tag) + { + foreach (ListViewItem listItem in ItemsLV.Items) + { + if (Object.ReferenceEquals(tag, listItem.Tag)) + { + return listItem; + } + } + + return null; + } + + /// + /// Returns the tag associated with a selected item. + /// + protected object GetSelectedTag(int index) + { + if (ItemsLV.SelectedItems.Count > index) + { + return ((ListViewItem) ItemsLV.SelectedItems[index]).Tag; + } + + return null; + } + #endregion + + #region BaseListCtrlSorter Class + /// + /// A class that allows the list to be sorted. + /// + private class BaseListCtrlSorter : IComparer + { + /// + /// Initializes the sorter. + /// + public BaseListCtrlSorter(BaseListCtrl control) + { + m_control = control; + } + + /// + /// Compares the two items. + /// + public int Compare(object x, object y) + { + ListViewItem itemX = x as ListViewItem; + ListViewItem itemY = y as ListViewItem; + + return m_control.CompareItems(itemX.Tag, itemY.Tag); + } + + private BaseListCtrl m_control; + } + #endregion + + #region Event Handlers + private void ItemsLV_DoubleClick(object sender, System.EventArgs e) + { + try + { + PickItems(); + } + catch (Exception exception) + { + GuiUtils.HandleException(String.Empty, GuiUtils.CallerName(), exception); + } + } + + private void ItemsLV_SelectedIndexChanged(object sender, System.EventArgs e) + { + try + { + SelectItems(); + } + catch (Exception exception) + { + GuiUtils.HandleException(String.Empty, GuiUtils.CallerName(), exception); + } + } + + /// + /// Handles the DragDrop event of the ItemsLV control. + /// + /// The source of the event. + /// The instance containing the event data. + protected virtual void ItemsLV_DragDrop(object sender, PointerRoutedEventArgs e) + { + // overriden by sub-class. + } + #endregion + } + #region ListItemAction Enumeration + /// + /// The possible actions that could affect an item. + /// + public enum ListItemAction + { + + /// + /// The item was picked (double clicked). + /// + Picked, + + /// + /// The item was selected. + /// + Selected, + + /// + /// The item was added. + /// + Added, + + /// + /// The item was modified. + /// + Modified, + + /// + /// The item was removed. + /// + Removed + } + #endregion + + #region ListItemActionEventArgs Class + /// + /// The event argurments passed when an item event occurs. + /// + public class ListItemActionEventArgs : EventArgs + { + #region Constructors + /// + /// Initializes a new instance of the class. + /// + /// The action. + /// The items. + public ListItemActionEventArgs(ListItemAction action, ICollection items) + { + m_items = items; + m_action = action; + } + #endregion + + #region Public Properties + /// + /// Gets the items. + /// + /// The items. + public ICollection Items + { + get { return m_items; } + } + + /// + /// Gets the action. + /// + /// The action. + public ListItemAction Action + { + get { return m_action; } + } + #endregion + + #region Private Fields + private ICollection m_items; + private ListItemAction m_action; + #endregion + } + + /// + /// The delegate used to receive item action events. + /// + public delegate void ListItemActionEventHandler(object sender, ListItemActionEventArgs e); + #endregion +} diff --git a/SampleApplications/SDK/Controls/Common (OLD)/BaseTreeCtrl.xaml b/SampleApplications/SDK/Controls/Common (OLD)/BaseTreeCtrl.xaml new file mode 100644 index 0000000000..efc0aa6edc --- /dev/null +++ b/SampleApplications/SDK/Controls/Common (OLD)/BaseTreeCtrl.xaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SampleApplications/SDK/Controls/Common (OLD)/BaseTreeCtrl.xaml.cs b/SampleApplications/SDK/Controls/Common (OLD)/BaseTreeCtrl.xaml.cs new file mode 100644 index 0000000000..1ae6113a3c --- /dev/null +++ b/SampleApplications/SDK/Controls/Common (OLD)/BaseTreeCtrl.xaml.cs @@ -0,0 +1,590 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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.Collections.ObjectModel; +using System.Linq; +using Windows.UI; +using Windows.UI.Popups; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Media; +using WinRTXamlToolkit.Controls; +using WinRTXamlToolkit.Imaging; +using WinRTXamlToolkit.Tools; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Windows.UI.Xaml.Input; +using System.Threading.Tasks; + +namespace Opc.Ua.Client.Controls +{ + /// + /// A helper class for tree views. + /// + public abstract class BindableBase : INotifyPropertyChanged + { + /// + /// Multicast event for property change notifications. + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// Checks if a property already matches a desired value. Sets the property and + /// notifies listeners only when necessary. + /// + /// Type of the property. + /// Reference to a property with both getter and setter. + /// Desired value for the property. + /// Name of the property used to notify listeners. This + /// value is optional and can be provided automatically when invoked from compilers that + /// support CallerMemberName. + /// True if the value was changed, false if the existing value matched the + /// desired value. + protected bool SetProperty(ref T storage, T value, [CallerMemberName] String propertyName = null) + { + if (object.Equals(storage, value)) return false; + + storage = value; + this.OnPropertyChanged(propertyName); + return true; + } + + /// + /// Notifies listeners that a property value has changed. + /// + /// Name of the property used to notify listeners. This + /// value is optional and can be provided automatically when invoked from compilers + /// that support . + protected void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + var eventHandler = this.PropertyChanged; + if (eventHandler != null) + { + eventHandler(this, new PropertyChangedEventArgs(propertyName)); + } + } + } + + /// + /// The view model for the tree view. + /// + public class TreeViewPageViewModel : BindableBase + { + public delegate Task TreeItemViewModelHandler(TreeItemViewModel node); + + public TreeItemViewModelHandler OnLoadPropertiesAsync; + public TreeItemViewModelHandler OnLoadChildrenAsync; + public TreeItemViewModelHandler OnRefreshAsync; + + #region TreeItems + private ObservableCollection _treeItems; + public ObservableCollection TreeItems + { + get { return _treeItems; } + set { this.SetProperty(ref _treeItems, value); } + } + #endregion + + #region SelectedItem + private TreeItemViewModel _selectedItem; + public TreeItemViewModel SelectedItem + { + get { return _selectedItem; } + set { SetProperty(ref _selectedItem, value); } + } + #endregion + + public TreeViewPageViewModel() + { + TreeItems = new ObservableCollection(); + } + + #region internal + internal virtual async void LoadPropertiesAsync(TreeItemViewModel node) + { + if (OnLoadPropertiesAsync != null) + { + await OnLoadPropertiesAsync(node); + } + } + + internal virtual async void LoadChildrenAsync(TreeItemViewModel node) + { + if (OnLoadChildrenAsync != null) + { + await OnLoadChildrenAsync(node); + } + } + + internal virtual async void RefreshAsync(TreeItemViewModel node) + { + if (OnRefreshAsync != null) + { + await OnRefreshAsync(node); + } + } + #endregion + } + + /// + /// The view model for tree items. + /// + public class TreeItemViewModel : BindableBase + { + public TreeViewPageViewModel TreeModel { get; protected set; } + private bool _everSelected; + + public TreeItemViewModel( + TreeViewPageViewModel treeModel, + TreeItemViewModel parent) + { + TreeModel = treeModel; + Parent = parent; + _everSelected = false; + } + + #region Parent + private TreeItemViewModel _parent; + public TreeItemViewModel Parent + { + get { return _parent; } + set { this.SetProperty(ref _parent, value); } + } + #endregion + + #region Text + private string _text; + /// + /// Gets or sets the text. + /// + public string Text + { + get { return _text; } + set { this.SetProperty(ref _text, value); } + } + #endregion + + #region Icon + private string _icon; + /// + /// Gets or sets the icon. + /// + public string Icon + { + get { return _icon; } + set { this.SetProperty(ref _icon, value); } + } + #endregion + + #region Children + private ObservableCollection _children = new ObservableCollection(); + /// + /// Gets or sets the child items. + /// + public ObservableCollection Children + { + get { return _children; } + set { this.SetProperty(ref _children, value); } + } + #endregion + + #region Brush + private SolidColorBrush _brush; + /// + /// Gets or sets the brush. + /// + public SolidColorBrush Brush + { + get { return _brush; } + set { this.SetProperty(ref _brush, value); } + } + #endregion + + #region Item + private Object _item; + /// + /// Gets or sets the session. + /// + public Object Item + { + get { return _item; } + set { this.SetProperty(ref _item, value); } + } + #endregion + + #region IsSelected + private bool _isSelected; + public bool IsSelected + { + get { return _isSelected; } + set + { + if (this.SetProperty(ref _isSelected, value) && value) + { + if (!_everSelected) + { + _everSelected = true; + TreeModel.LoadPropertiesAsync(this); + } + this.TreeModel.SelectedItem = this; + } + } + } + #endregion + + #region IsExpanded + private bool _isExpanded; + public bool IsExpanded + { + get { return _isExpanded; } + set + { + if (this.SetProperty(ref _isExpanded, value) && value) + { + TreeModel.LoadChildrenAsync(this); + } + } + } + #endregion + + } + + + /// + /// A base class for tree controls. + /// + public partial class BaseTreeCtrl : UserControl + { +#region Public Interface + /// + /// The TreeView contained in the Control. + /// + private TreeView TV; + protected TreeViewPageViewModel NodesTV; + + /// + /// Initialize tree control. + /// + public BaseTreeCtrl() + { + InitializeComponent(); + DataContext = NodesTV = new TreeViewPageViewModel(); + ContainerGrid.Children.Clear(); + NodesTV.TreeItems?.Clear(); + ContainerGrid.Children.Add(TV = (TreeView)this.TreeViewTemplate.LoadContent()); + NodesTV.OnLoadChildrenAsync += BeforeExpand; + } + + /// + /// Raised whenever a node is 'picked' in the control. + /// + public event TreeNodeActionEventHandler NodePicked + { + add { m_NodePicked += value; } + remove { m_NodePicked -= value; } + } + + /// + /// Raised whenever a node is selected in the control. + /// + public event TreeNodeActionEventHandler NodeSelected + { + add { m_NodeSelected += value; } + remove { m_NodeSelected -= value; } + } + + /// + /// Whether the control should allow items to be dragged. + /// + public bool EnableDragging + { + get { return m_enableDragging; } + set { m_enableDragging = value; } + } + + public PopupMenu ContextMenu { get; set; } + + /// + /// Clears the contents of the TreeView + /// + public void Clear() + { + NodesTV.TreeItems?.Clear(); + } + + #endregion + + #region Private Fields + private event TreeNodeActionEventHandler m_NodePicked; + private event TreeNodeActionEventHandler m_NodeSelected; + private bool m_enableDragging; +#endregion + +#region Protected Methods + /// + /// Adds an item to the tree. + /// + protected virtual TreeItemViewModel AddNode(TreeItemViewModel treeNode, object item) + { + return AddNode(treeNode, item, String.Format("{0}", item), "ClosedFolder"); + } + + /// + /// Adds an item to the tree. + /// + protected virtual TreeItemViewModel AddNode(TreeItemViewModel parent, object item, string text, string icon) + { + // create node. + TreeItemViewModel treeNode = new TreeItemViewModel(NodesTV, parent); + + // update text/icon. + UpdateNode(treeNode, item, text, icon); + + // add to control. + if (parent == null) + { + NodesTV.TreeItems?.Add(treeNode); + } + else + { + parent.Children.Add(treeNode); + } + + // return new tree node. + return treeNode; + } + + /// + /// Updates a tree node with the current contents of an object. + /// + protected virtual void UpdateNode(TreeItemViewModel treeNode, object item, string text, string icon) + { + treeNode.Text = text; + treeNode.Item = item; + treeNode.Icon = icon; + switch (icon) + { + case "Server": + treeNode.Brush = new SolidColorBrush(Colors.Green); + break; + case "ServerStopped": + treeNode.Brush = new SolidColorBrush(Colors.Red); + break; + case "ServerKeepAliveStopped": + treeNode.Brush = new SolidColorBrush(Colors.Yellow); + break; + default: + treeNode.Brush = null; + break; + } + } + + /// + /// Returns the data to drag. + /// + protected virtual object GetDataToDrag(TreeItemViewModel node) + { + return node.Item; + } + + /// + /// Enables the state of menu items. + /// + protected virtual void EnableMenuItems(TreeItemViewModel clickedNode) + { + // do nothing. + } + + /// + /// Initializes a node before expanding it. + /// + protected virtual Task BeforeExpand(TreeItemViewModel clickedNode) + { + return Task.CompletedTask; + } + + /// + /// Sends notifications whenever a node in the control is 'picked'. + /// + protected virtual void PickNode() + { + if (m_NodePicked != null) + { + if (NodesTV.SelectedItem != null) + { + object parent = null; + + if (NodesTV.SelectedItem.Parent != null) + { + parent = NodesTV.SelectedItem.Parent; + } + + m_NodePicked(this, new TreeNodeActionEventArgs(TreeNodeAction.Picked, NodesTV.SelectedItem.Item, parent)); + } + } + } + + /// + /// Sends notifications whenever a node in the control is 'selected'. + /// + protected virtual void SelectNode() + { + if (m_NodeSelected != null) + { + if (NodesTV.SelectedItem != null) + { + object parent = null; + + if (NodesTV.SelectedItem.Parent != null) + { + parent = NodesTV.SelectedItem.Parent; + } + + m_NodeSelected(this, new TreeNodeActionEventArgs(TreeNodeAction.Selected, NodesTV.SelectedItem.Item, parent)); + } + } + } + + /// + /// Returns the Tag for the current selection. + /// + public object SelectedTag + { + get + { + if (NodesTV.SelectedItem != null) + { + return NodesTV.SelectedItem.Item; + } + + return null; + } + } + #endregion + + private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs e) + { + SelectNode(); + } + + private void TreeView_RightTapped(object sender, RightTappedRoutedEventArgs e) + { + TreeView tv = sender as TreeView; + if (ContextMenu != null) + { + FrameworkElement element = (FrameworkElement)e.OriginalSource; + GeneralTransform buttonTransform = element.TransformToVisual(null); + Point point = buttonTransform.TransformPoint(new Point()); + Rect selection = new Rect(point, new Size(element.ActualWidth, element.ActualHeight)); + var chosenCommand = ContextMenu.ShowForSelectionAsync(selection); + } + } + } + + #region TreeNodeAction Eumeration + /// + /// The possible actions that could affect a node. + /// + public enum TreeNodeAction + { + /// + /// A node was picked in the tree. + /// + Picked, + + /// + /// A node was selected in the tree. + /// + Selected + } +#endregion + +#region TreeNodeActionEventArgs class + /// + /// The event argurments passed when an node event occurs. + /// + public class TreeNodeActionEventArgs : EventArgs + { +#region Constructor + /// + /// Initializes the object. + /// + public TreeNodeActionEventArgs(TreeNodeAction action, object node, object parent) + { + m_node = node; + m_parent = parent; + m_action = action; + } +#endregion + +#region Public Fields + /// + /// The tag associated with the node that was acted on. + /// + public object Node + { + get { return m_node; } + } + + /// + /// The tag associated with the parent of the node that was acted on. + /// + public object Parent + { + get { return m_parent; } + } + + /// + /// The action in question. + /// + public TreeNodeAction Action + { + get { return m_action; } + } +#endregion + +#region Private Fields + private object m_node; + private object m_parent; + private TreeNodeAction m_action; +#endregion + } + + /// + /// The delegate used to receive node action events. + /// + public delegate void TreeNodeActionEventHandler(object sender, TreeNodeActionEventArgs e); +#endregion +} diff --git a/SampleApplications/SDK/Controls/Common (OLD)/DataListCtrl.xaml b/SampleApplications/SDK/Controls/Common (OLD)/DataListCtrl.xaml new file mode 100644 index 0000000000..c05b8eb776 --- /dev/null +++ b/SampleApplications/SDK/Controls/Common (OLD)/DataListCtrl.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/SampleApplications/SDK/Controls/Common (OLD)/DataListCtrl.xaml.cs b/SampleApplications/SDK/Controls/Common (OLD)/DataListCtrl.xaml.cs new file mode 100644 index 0000000000..ff71b43bc7 --- /dev/null +++ b/SampleApplications/SDK/Controls/Common (OLD)/DataListCtrl.xaml.cs @@ -0,0 +1,1244 @@ +/* ======================================================================== + * Copyright (c) 2005-2013 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; +using System.Text; +using System.Reflection; +using System.Xml; +using Windows.UI.Xaml.Controls; +using Windows.Devices.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Popups; +using System.Threading.Tasks; +using Opc.Ua; +using Opc.Ua.Client; + +namespace Opc.Ua.Client.Controls +{ + /// + /// Displays a hierarchical view of a complex value. + /// + public sealed partial class DataListCtrl : UserControl + { + /// + /// Initializes a new instance of the class. + /// + public DataListCtrl() + { + InitializeComponent(); + } + + #region Private Fields + private bool m_latestValue = true; + private bool m_expanding; + private int m_depth; + private FontFamily m_defaultFont; + private MonitoredItem m_monitoredItem; + + private const string UnknownType = "(unknown)"; + private const string NullValue = "(null)"; + private const string ExpandIcon = "ExpandPlus"; + private const string CollapseIcon = "ExpandMinus"; + #endregion + + #region Public Interface + /// + /// Whether to update the control when the value changes. + /// + public bool AutoUpdate + { + get { return (bool) UpdatesMI.IsChecked; } + set { UpdatesMI.IsChecked = value; } + } + + /// + /// Whether to only display the latest value for a monitored item. + /// + public bool LatestValue + { + get { return m_latestValue; } + set { m_latestValue = value; } + } + + /// + /// The monitored item associated with the value. + /// + public MonitoredItem MonitoredItem + { + get { return m_monitoredItem; } + set { m_monitoredItem = value; } + } + + /// + /// Clears the contents of the control, + /// + public void Clear() + { + ItemsLV.Items.Clear(); + } + + /// + /// Displays a value in the control. + /// + public void ShowValue(object value) + { + ShowValue(value, false); + } + + /// + /// Displays a value in the control. + /// + public void ShowValue(object value, bool overwrite) + { + if (!overwrite) + { + Clear(); + } + + m_defaultFont = new FontFamily("Courier New"); + + m_expanding = false; + m_depth = 0; + + // show the value. + int index = 0; + ShowValue(ref index, ref overwrite, value.ToString()); + } + #endregion + + #region Overridden Methods + /// + /// Enables the menu items. + /// + public void EnableMenuItems(ListViewItem clickedItem) + { + RefreshMI.IsEnabled = true; + ClearMI.IsEnabled = true; + + if (ItemsLV.SelectedItems.Count == 1) + { + ValueState state = ((ListViewItem) ItemsLV.SelectedItems[0]).Tag as ValueState; + EditMI.IsEnabled = IsEditableType(state.Component); + } + } + #endregion + + #region ValueState Class + /// + /// Stores the state associated with an item. + /// + private class ValueState + { + public bool Expanded = false; + public bool Expandable = false; + public object Value = null; + public object Component = null; + public object ComponentId = null; + } + #endregion + + #region Private Members + /// + /// Returns true is the value is an editable type. + /// + private bool IsEditableType(object value) + { + if (value is bool) return true; + if (value is sbyte) return true; + if (value is byte) return true; + if (value is short) return true; + if (value is ushort) return true; + if (value is int) return true; + if (value is uint) return true; + if (value is long) return true; + if (value is ulong) return true; + if (value is float) return true; + if (value is double) return true; + if (value is string) return true; + if (value is DateTime) return true; + if (value is Guid) return true; + + return false; + } + + /// + /// Returns the list item at the specified index. + /// + private ListViewItem GetListItem(int index, ref bool overwrite, string name, string type) + { + ListViewItem listitem = null; + + // check if there is an item that could be re-used. + if (!m_expanding && index < ItemsLV.Items.Count) + { + return (ListViewItem) ItemsLV.Items[index]; + } + + overwrite = false; + + listitem = new ListViewItem(); + listitem.Name = name; + + listitem.Tag = new ValueState(); + + if (!m_expanding) + { + ItemsLV.Items.Add(listitem); + } + else + { + ItemsLV.Items.Insert(index, listitem); + } + + return listitem; + } + + /// + /// Returns true if the type can be expanded. + /// + private bool IsExpandableType(object value) + { + // check for null. + if (value == null) + { + return false; + } + + // check for Variant. + if (value is Variant) + { + return IsExpandableType(((Variant)value).Value); + } + + // check for bytes. + byte[] bytes = value as byte[]; + + if (bytes != null) + { + return false; + } + + // check for xml element. + XmlElement xml = value as XmlElement; + + if (xml != null) + { + if (xml.ChildNodes.Count == 1 && xml.ChildNodes[0] is XmlText) + { + return false; + } + + return xml.HasChildNodes; + } + + // check for array. + Array array = value as Array; + + if (array != null) + { + return array.Length > 0; + } + + // check for list. + IList list = value as IList; + + if (list != null) + { + return list.Count > 0; + } + + // check for encodeable object. + IEncodeable encodeable = value as IEncodeable; + + if (encodeable != null) + { + return true; + } + + // check for extension object. + ExtensionObject extension = value as ExtensionObject; + + if (extension != null) + { + return IsExpandableType(extension.Body); + } + + // check for data value. + DataValue datavalue = value as DataValue; + + if (datavalue != null) + { + return true; + } + + // check for event value. + EventFieldList eventFields = value as EventFieldList; + + if (eventFields != null) + { + return true; + } + + // must be a simple value. + return false; + } + + /// + /// Formats a value for display in the control. + /// + private string GetValueText(object value) + { + // check for null. + if (value == null) + { + return "(null)"; + } + + // format bytes. + byte[] bytes = value as byte[]; + + if (bytes != null) + { + StringBuilder buffer = new StringBuilder(); + + for (int ii = 0; ii < bytes.Length; ii++) + { + if (ii != 0 && ii%16 == 0) + { + buffer.Append(" "); + } + + buffer.AppendFormat("{0:X2} ", bytes[ii]); + } + + return buffer.ToString(); + } + + // format xml element. + XmlElement xml = value as XmlElement; + + if (xml != null) + { + // return the entire element if not expandable. + if (!IsExpandableType(xml)) + { + return xml.OuterXml; + } + + // show only the start tag. + string text = xml.OuterXml; + + int index = text.IndexOf('>'); + + if (index != -1) + { + text = text.Substring(0, index); + } + + return text; + } + + // format array. + Array array = value as Array; + + if (array != null) + { + return Utils.Format("{1}[{0}]", array.Length, value.GetType().GetElementType().Name); + } + + // format list. + IList list = value as IList; + + if (list != null) + { + string type = value.GetType().Name; + + if (type.EndsWith("Collection")) + { + type = type.Substring(0, type.Length - "Collection".Length); + } + else + { + type = "Object"; + } + + return Utils.Format("{1}[{0}]", list.Count, type); + } + + // format encodeable object. + IEncodeable encodeable = value as IEncodeable; + + if (encodeable != null) + { + return encodeable.GetType().Name; + } + + // format extension object. + ExtensionObject extension = value as ExtensionObject; + + if (extension != null) + { + return GetValueText(extension.Body); + } + + // check for event value. + EventFieldList eventFields = value as EventFieldList; + + if (eventFields != null) + { + if (m_monitoredItem != null) + { + return String.Format("{0}", m_monitoredItem.GetEventType(eventFields)); + } + + return eventFields.GetType().Name; + } + + // check for data value. + DataValue dataValue = value as DataValue; + + if (dataValue != null) + { + if (StatusCode.IsBad(dataValue.StatusCode)) + { + return String.Format("{0}", dataValue.StatusCode); + } + + return String.Format("{0}", dataValue.Value); + } + + // use default formatting. + return Utils.Format("{0}", value); + } + + /// + /// Updates the list with the specified value. + /// + private void UpdateList( + ref int index, + ref bool overwrite, + object value, + object componentValue, + object componentId, + string name, + string type) + { + // get the list item to update. + ListViewItem listitem = GetListItem(index, ref overwrite, name, type); + + // move to next item. + index++; + + ValueState state = listitem.Tag as ValueState; + + // recursively update sub-values if item is expanded. + if (overwrite) + { + if (state.Expanded && state.Expandable) + { + m_depth++; + ShowValue(ref index, ref overwrite, componentValue.ToString()); + m_depth--; + } + } + + // update state. + state.Expandable = IsExpandableType(componentValue); + state.Value = value; + state.Component = componentValue; + state.ComponentId = componentId; + } + + /// + /// Shows property of an encodeable object in the control. + /// + private void ShowValue(ref int index, ref bool overwrite, IEncodeable value, PropertyInfo property) + { + // get the name of the property. + string name = Utils.GetDataMemberName(property); + + if (name == null) + { + return; + } + + // get the property value. + object propertyValue = null; + + MethodInfo[] accessors = property.GetAccessors(); + + for (int ii = 0; ii < accessors.Length; ii++) + { + if (accessors[ii].ReturnType == property.PropertyType) + { + propertyValue = accessors[ii].Invoke(value, null); + break; + } + } + + if (propertyValue is Variant) + { + propertyValue = ((Variant)propertyValue).Value; + } + + // update the list view. + UpdateList( + ref index, + ref overwrite, + value, + propertyValue, + property, + name, + property.PropertyType.Name); + } + + /// + /// Shows the element of an array in the control. + /// + private void ShowValue(ref int index, ref bool overwrite, Array value, int element) + { + // get the name of the element. + string name = Utils.Format("[{0}]", element); + + // get the element value. + object elementValue = value.GetValue(element); + + // get the type name. + string type = null; + + if (elementValue != null) + { + type = elementValue.GetType().Name; + } + + // update the list view. + UpdateList( + ref index, + ref overwrite, + value, + elementValue, + element, + name, + type); + } + + /// + /// Asks for confirmation before expanding a long list. + /// + private async Task PromptOnLongList(int length) + { + if (length < 256) + { + return true; + } + MessageDlg dialog = new MessageDlg("It may take a long time to display the list are you sure you want to continue?", MessageDlgButton.Yes, MessageDlgButton.No); + MessageDlgButton result = await dialog.ShowAsync(); + if (result != MessageDlgButton.Yes) + { + return true; + } + + return false; + } + + /// + /// Shows the element of a list in the control. + /// + private void ShowValue(ref int index, ref bool overwrite, IList value, int element) + { + // get the name of the element. + string name = Utils.Format("[{0}]", element); + + // get the element value. + object elementValue = value[element]; + + // get the type name. + string type = null; + + if (elementValue != null) + { + type = elementValue.GetType().Name; + } + + // update the list view. + UpdateList( + ref index, + ref overwrite, + value, + elementValue, + element, + name, + type); + } + + /// + /// Shows an XML element in the control. + /// + private void ShowValue(ref int index, ref bool overwrite, XmlElement value, int childIndex) + { + // ignore children that are not elements. + XmlElement child = value.ChildNodes[childIndex] as XmlElement; + + if (child == null) + { + return; + } + + // get the name of the element. + string name = Utils.Format("{0}", child.Name); + + // get the type name. + string type = value.GetType().Name; + + // update the list view. + UpdateList( + ref index, + ref overwrite, + value, + child, + childIndex, + name, + type); + } + + /// + /// Shows an event in the control. + /// + private void ShowValue(ref int index, ref bool overwrite, EventFieldList value, int fieldIndex) + { + // ignore children that are not elements. + object field = value.EventFields[fieldIndex].Value; + + if (field == null) + { + return; + } + + // get the name of the element. + string name = null; + + if (m_monitoredItem != null) + { + name = m_monitoredItem.GetFieldName(fieldIndex); + } + + // get the type name. + string type = value.GetType().Name; + + // update the list view. + UpdateList( + ref index, + ref overwrite, + value, + field, + fieldIndex, + name, + type); + } + + /// + /// Shows a byte array in the control. + /// + private void ShowValue(ref int index, ref bool overwrite, byte[] value, int blockStart) + { + // get the name of the element. + string name = Utils.Format("[{0:X4}]", blockStart); + + int bytesLeft = value.Length - blockStart; + + if (bytesLeft > 16) + { + bytesLeft = 16; + } + + // get the element value. + byte[] blockValue = new byte[bytesLeft]; + Array.Copy(value, blockStart, blockValue, 0, bytesLeft); + + // get the type name. + string type = value.GetType().Name; + + // update the list view. + UpdateList( + ref index, + ref overwrite, + value, + blockValue, + blockStart, + name, + type); + } + + /// + /// Shows a data value in the control. + /// + private void ShowValue(ref int index, ref bool overwrite, DataValue value, int component) + { + string name = null; + object componentValue = null; + + switch (component) + { + case 0: + { + name = "Value"; + componentValue = value.Value; + + ExtensionObject extension = componentValue as ExtensionObject; + + if (extension != null) + { + componentValue = extension.Body; + } + + break; + } + + case 1: + { + name = "StatusCode"; + componentValue = value.StatusCode; + break; + } + + case 2: + { + if (value.SourceTimestamp != DateTime.MinValue) + { + name = "SourceTimestamp"; + componentValue = value.SourceTimestamp; + } + + break; + } + + case 3: + { + if (value.ServerTimestamp != DateTime.MinValue) + { + name = "ServerTimestamp"; + componentValue = value.ServerTimestamp; + } + + break; + } + } + + // don't display empty components. + if (name == null) + { + return; + } + + // get the type name. + string type = "(unknown)"; + + if (componentValue != null) + { + type = componentValue.GetType().Name; + } + + // update the list view. + UpdateList( + ref index, + ref overwrite, + value, + componentValue, + component, + name, + type); + } + + /// + /// Shows a node id in the control. + /// + private void ShowValue(ref int index, ref bool overwrite, NodeId value, int component) + { + string name = null; + object componentValue = null; + + switch (component) + { + case 0: + { + name = "IdType"; + componentValue = value.IdType; + break; + } + + case 1: + { + name = "Identifier"; + componentValue = value.Identifier; + break; + } + + case 2: + { + name = "NamespaceIndex"; + componentValue = value.NamespaceIndex; + break; + } + } + + // don't display empty components. + if (name == null) + { + return; + } + + // get the type name. + string type = "(unknown)"; + + if (componentValue != null) + { + type = componentValue.GetType().Name; + } + + // update the list view. + UpdateList( + ref index, + ref overwrite, + value, + componentValue, + component, + name, + type); + } + + /// + /// Shows am expanded node id in the control. + /// + private void ShowValue(ref int index, ref bool overwrite, ExpandedNodeId value, int component) + { + string name = null; + object componentValue = null; + + switch (component) + { + case 0: + { + name = "IdType"; + componentValue = value.IdType; + break; + } + + case 1: + { + name = "Identifier"; + componentValue = value.Identifier; + break; + } + + case 2: + { + name = "NamespaceIndex"; + componentValue = value.NamespaceIndex; + break; + } + + case 3: + { + name = "NamespaceUri"; + componentValue = value.NamespaceUri; + break; + } + } + + // don't display empty components. + if (name == null) + { + return; + } + + // get the type name. + string type = "(unknown)"; + + if (componentValue != null) + { + type = componentValue.GetType().Name; + } + + // update the list view. + UpdateList( + ref index, + ref overwrite, + value, + componentValue, + component, + name, + type); + } + + /// + /// Shows qualified name in the control. + /// + private void ShowValue(ref int index, ref bool overwrite, QualifiedName value, int component) + { + string name = null; + object componentValue = null; + + switch (component) + { + case 0: + { + name = "Name"; + componentValue = value.Name; + break; + } + + case 1: + { + name = "NamespaceIndex"; + componentValue = value.NamespaceIndex; + break; + } + } + + // don't display empty components. + if (name == null) + { + return; + } + + // get the type name. + string type = "(unknown)"; + + if (componentValue != null) + { + type = componentValue.GetType().Name; + } + + // update the list view. + UpdateList( + ref index, + ref overwrite, + value, + componentValue, + component, + name, + type); + } + + /// + /// Shows localized text in the control. + /// + private void ShowValue(ref int index, ref bool overwrite, LocalizedText value, int component) + { + string name = null; + object componentValue = null; + + switch (component) + { + case 0: + { + name = "Text"; + componentValue = value.Text; + break; + } + + case 1: + { + name = "Locale"; + componentValue = value.Locale; + break; + } + } + + // don't display empty components. + if (name == null) + { + return; + } + + // get the type name. + string type = "(unknown)"; + + if (componentValue != null) + { + type = componentValue.GetType().Name; + } + + // update the list view. + UpdateList( + ref index, + ref overwrite, + value, + componentValue, + component, + name, + type); + } + + /// + /// Shows a string in the control. + /// + private void ShowValue(ref int index, ref bool overwrite, string value) + { + string name = "Value"; + object componentValue = value; + + // don't display empty components. + if (name == null) + { + return; + } + + // get the type name. + string type = "(unknown)"; + + if (componentValue != null) + { + type = componentValue.GetType().Name; + } + + // update the list view. + UpdateList( + ref index, + ref overwrite, + value, + componentValue, + 0, + name, + type); + } + + /// + /// Shows a value in control. + /// + private async Task ShowValue(int index, bool overwrite, object value) + { + if (value == null) + { + return; + } + + // show monitored items. + MonitoredItem monitoredItem = value as MonitoredItem; + + if (monitoredItem != null) + { + m_monitoredItem = monitoredItem; + ShowValue(ref index, ref overwrite, monitoredItem.LastValue.ToString()); + return; + } + + // show data changes + MonitoredItemNotification datachange = value as MonitoredItemNotification; + + if (datachange != null) + { + ShowValue(ref index, ref overwrite, datachange.Value.ToString()); + return; + } + + // show events + EventFieldList eventFields = value as EventFieldList; + + if (eventFields != null) + { + for (int ii = 0; ii < eventFields.EventFields.Count; ii++) + { + ShowValue(ref index, ref overwrite, eventFields, ii); + } + + return; + } + + // show extension bodies. + ExtensionObject extension = value as ExtensionObject; + + if (extension != null) + { + ShowValue(ref index, ref overwrite, extension.Body.ToString()); + return; + } + + // show encodeables. + IEncodeable encodeable = value as IEncodeable; + + if (encodeable != null) + { + PropertyInfo[] properties = encodeable.GetType().GetProperties(); + + foreach (PropertyInfo property in properties) + { + ShowValue(ref index, ref overwrite, encodeable, property); + } + + return; + } + + // show bytes. + byte[] bytes = value as byte[]; + + if (bytes != null) + { + bool result = await PromptOnLongList(bytes.Length / 16); + if (!result) + { + return; + } + + for (int ii = 0; ii < bytes.Length; ii += 16) + { + ShowValue(ref index, ref overwrite, bytes, ii); + } + + return; + } + + // show arrays + Array array = value as Array; + + if (array != null) + { + bool result = await PromptOnLongList(array.Length); + if (!result) + { + return; + } + + for (int ii = 0; ii < array.Length; ii++) + { + ShowValue(ref index, ref overwrite, array, ii); + } + + return; + } + + // show lists + IList list = value as IList; + + if (list != null) + { + bool result = await PromptOnLongList(list.Count); + if (!result) + { + return; + } + + for (int ii = 0; ii < list.Count; ii++) + { + ShowValue(ref index, ref overwrite, list, ii); + } + + return; + } + + // show xml elements + XmlElement xml = value as XmlElement; + + if (xml != null) + { + bool result = await PromptOnLongList(xml.ChildNodes.Count); + if (!result) + { + return; + } + + for (int ii = 0; ii < xml.ChildNodes.Count; ii++) + { + ShowValue(ref index, ref overwrite, xml, ii); + } + + return; + } + + // show data value. + DataValue datavalue = value as DataValue; + + if (datavalue != null) + { + ShowValue(ref index, ref overwrite, datavalue, 0); + ShowValue(ref index, ref overwrite, datavalue, 1); + ShowValue(ref index, ref overwrite, datavalue, 2); + ShowValue(ref index, ref overwrite, datavalue, 3); + return; + } + + // show node id value. + NodeId nodeId = value as NodeId; + + if (nodeId != null) + { + ShowValue(ref index, ref overwrite, nodeId, 0); + ShowValue(ref index, ref overwrite, nodeId, 1); + ShowValue(ref index, ref overwrite, nodeId, 2); + return; + } + + // show expanded node id value. + ExpandedNodeId expandedNodeId = value as ExpandedNodeId; + + if (expandedNodeId != null) + { + ShowValue(ref index, ref overwrite, expandedNodeId, 0); + ShowValue(ref index, ref overwrite, expandedNodeId, 1); + ShowValue(ref index, ref overwrite, expandedNodeId, 2); + ShowValue(ref index, ref overwrite, expandedNodeId, 3); + return; + } + + // show qualified name value. + QualifiedName qualifiedName = value as QualifiedName; + + if (qualifiedName != null) + { + ShowValue(ref index, ref overwrite, qualifiedName, 0); + ShowValue(ref index, ref overwrite, qualifiedName, 1); + return; + } + + // show qualified name value. + LocalizedText localizedText = value as LocalizedText; + + if (localizedText != null) + { + ShowValue(ref index, ref overwrite, localizedText, 0); + ShowValue(ref index, ref overwrite, localizedText, 1); + return; + } + + // show variant. + Variant? variant = value as Variant?; + + if (variant != null) + { + ShowValue(ref index, ref overwrite, variant.Value.Value.ToString()); + return; + } + + // show unknown types as strings. + ShowValue(ref index, ref overwrite, String.Format("{0}", value)); + } + #endregion + } +} diff --git a/SampleApplications/SDK/Controls/Common (OLD)/DateTimeValueEditCtrl.xaml b/SampleApplications/SDK/Controls/Common (OLD)/DateTimeValueEditCtrl.xaml new file mode 100644 index 0000000000..577744e447 --- /dev/null +++ b/SampleApplications/SDK/Controls/Common (OLD)/DateTimeValueEditCtrl.xaml @@ -0,0 +1,18 @@ + + + +