#region Copyright (c) 2011-2023 Technosoftware GmbH. All rights reserved //----------------------------------------------------------------------------- // Copyright (c) 2011-2023 Technosoftware GmbH. All rights reserved // Web: https://www.technosoftware.com // // The source code in this file is covered under a dual-license scenario: // - Owner of a purchased license: SCLA 1.0 // - GPL V3: everybody else // // SCLA license terms accompanied with this source code. // See SCLA 1.0: https://technosoftware.com/license/Source_Code_License_Agreement.pdf // // GNU General Public License as published by the Free Software Foundation; // version 3 of the License are accompanied with this source code. // See https://technosoftware.com/license/GPLv3License.txt // // This source code is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. //----------------------------------------------------------------------------- #endregion Copyright (c) 2011-2023 Technosoftware GmbH. All rights reserved #region Using Directives using System; using System.Collections.Generic; using System.Globalization; using System.Reflection; using System.Resources; using System.Runtime.Serialization; #endregion namespace Technosoftware.DaAeHdaClient { /// A base class for an in-process object used to access OPC servers. [Serializable] public class OpcServer : IOpcServer, ISerializable, ICloneable { #region Fields /// /// The remote server object. /// internal IOpcServer Server; /// /// The OpcUrl that describes the network location of the server. /// private OpcUrl opcUrl_; /// /// The factory used to instantiate the remote server. /// protected IOpcFactory Factory; /// /// The last set of credentials used to connect successfully to the server. /// private OpcConnectData connectData_; /// /// A short name for the server. /// private string serverName_; /// /// A short name for the server assigned by the client /// private string clientName_; /// /// The default locale used by the server. /// private string locale_; /// /// The set of locales supported by the remote server. /// private string[] supportedLocales_; /// /// The resource manager used to access localized resources. /// protected ResourceManager ResourceManager; #endregion #region Constructors, Destructor, Initialization /// /// Initializes the object. /// public OpcServer() { Factory = null; Server = null; opcUrl_ = null; serverName_ = null; supportedLocales_ = null; ResourceManager = new ResourceManager("Technosoftware.DaAeHdaClient.Resources.Strings", Assembly.GetExecutingAssembly()); } /// /// Initializes the object with a factory and a default OpcUrl. /// /// The OpcFactory used to connect to remote servers. /// The network address of a remote server. public OpcServer(OpcFactory factory, OpcUrl url) { if (factory == null) throw new ArgumentNullException(nameof(factory)); Factory = (IOpcFactory)factory.Clone(); Server = null; opcUrl_ = null; serverName_ = null; supportedLocales_ = null; ResourceManager = new ResourceManager("Technosoftware.DaAeHdaClient.Resources.Strings", Assembly.GetExecutingAssembly()); if (url != null) SetUrl(url); } /// /// This must be called explicitly by clients to ensure the remote server is released. /// public virtual void Dispose() { if (Factory != null) { Factory.Dispose(); Factory = null; } if (Server != null) { try { Disconnect(); } catch { // ignored } Server = null; } } #endregion #region Properties /// /// Information about an OPC Server /// public OpcServerDescription ServerDescription { get; set; } /// /// List of supported OPC specifications /// public IList SupportedSpecifications { get; set; } /// /// Can be used to force OPC DA 2.0 even if OPC DA 3.0 server features are available /// public bool ForceDa20Usage { get; set; } #endregion #region Public Methods /// /// Finds the best matching locale given a set of supported locales. /// public static string FindBestLocale(string requestedLocale, string[] supportedLocales) { try { // check for direct match with requested locale. foreach (var supportedLocale in supportedLocales) { if (supportedLocale == requestedLocale) { return requestedLocale; } } // try to find match for parent culture. var requestedCulture = new CultureInfo(requestedLocale); foreach (var supportedLocale in supportedLocales) { try { var supportedCulture = new CultureInfo(supportedLocale); if (requestedCulture.Parent.Name == supportedCulture.Name) { return supportedCulture.Name; } } catch { // ignored } } // return default locale. return (supportedLocales.Length > 0) ? supportedLocales[0] : ""; } catch { // return default locale on any error. return (supportedLocales != null && supportedLocales.Length > 0) ? supportedLocales[0] : ""; } } #endregion #region Private Methods /// /// Updates the OpcUrl for the server. /// private void SetUrl(OpcUrl url) { if (url == null) throw new ArgumentNullException(nameof(url)); // cannot change the OpcUrl if the remote server is already instantiated. if (Server != null) throw new OpcResultException(new OpcResult(OpcResult.E_FAIL.Code, OpcResult.FuncCallType.SysFuncCall, null), "The server is already connected."); // copy the url. opcUrl_ = (OpcUrl)url.Clone(); // construct a name for the server. var name = ""; // use the host name as a base. if (opcUrl_.HostName != null) { name = opcUrl_.HostName.ToLower(); // suppress localhost and loopback as explicit hostnames. if (name == "localhost" || name == "127.0.0.1") { name = ""; } } // append the port. if (opcUrl_.Port != 0) { name += $".{opcUrl_.Port}"; } // add a separator. if (name != "") { name += "."; } // use the prog id as the name. if (opcUrl_.Scheme != OpcUrlScheme.HTTP) { var progId = opcUrl_.Path; var index = progId.LastIndexOf('/'); if (index != -1) { progId = progId.Substring(0, index); } name += progId; } // use full path without the extension as the name. else { var path = opcUrl_.Path; // strip the file extension. var index = path.LastIndexOf('.'); if (index != -1) { path = path.Substring(0, index); } // replace slashes with dashes. while (path.IndexOf('/') != -1) { path = path.Replace('/', '-'); } name += path; } // save the generated name in case the server name is not already set if (string.IsNullOrEmpty(serverName_)) { serverName_ = name; } } #endregion #region Protected Methods /// /// Returns a localized string with the specified name. /// protected string GetString(string name) { // create a culture object. CultureInfo culture; try { culture = new CultureInfo(Locale); } catch { culture = new CultureInfo(""); } // lookup resource string. try { return ResourceManager.GetString(name, culture); } catch { return null; } } #endregion #region ISerializable Members /// /// A set of names for fields used in serialization. /// private class Names { internal const string Name = "Name"; internal const string Url = "Url"; internal const string Factory = "Factory"; } /// /// Construct a server by de-serializing its OpcUrl from the stream. /// internal OpcServer(SerializationInfo info, StreamingContext context) { serverName_ = info.GetString(Names.Name); opcUrl_ = (OpcUrl)info.GetValue(Names.Url, typeof(OpcUrl)); Factory = (IOpcFactory)info.GetValue(Names.Factory, typeof(IOpcFactory)); } /// /// Serializes a server into a stream. /// public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue(Names.Name, serverName_); info.AddValue(Names.Url, opcUrl_); info.AddValue(Names.Factory, Factory); } #endregion #region ICloneable Members /// /// Returns an unconnected copy of the server with the same OpcUrl. /// public virtual object Clone() { // do a memberwise clone. var clone = (OpcServer)MemberwiseClone(); // place clone in disconnected state. clone.Server = null; clone.supportedLocales_ = null; clone.locale_ = null; clone.ResourceManager = new ResourceManager("Technosoftware.DaAeHdaClient.Resources.Strings", Assembly.GetExecutingAssembly()); // return clone. return clone; } #endregion #region IOpcServer Members /// /// A short descriptive name for the server. /// public virtual string ServerName { get => serverName_; set => serverName_ = value; } /// /// A short descriptive name for the server assigned by the client. /// public virtual string ClientName { get => clientName_; set { clientName_ = value; Server?.SetClientName(value); } } /// /// The OpcUrl that describes the network location of the server. /// public virtual OpcUrl Url { get => (OpcUrl)opcUrl_?.Clone(); set => SetUrl(value); } /// /// The default of locale used by the remote server. /// public virtual string Locale => locale_; /// /// The set of locales supported by the remote server. /// public virtual string[] SupportedLocales => (string[])supportedLocales_?.Clone(); /// /// Whether the remote server is currently connected. /// public virtual bool IsConnected => (Server != null); /// /// Allows the client to optionally register a client name with the server. This is included primarily for debugging purposes. The recommended behavior is that the client set his Node name and EXE name here. /// public virtual void SetClientName(string clientName) { ClientName = clientName; } /// /// Establishes a physical connection to the remote server. /// public virtual void Connect() { Connect(opcUrl_, null); } /// Establishes a physical connection to the remote server. /// If an OPC specific error occur this exception is raised. The Result field includes then the OPC specific code. /// Name of the server. The usual form is http:://xxx/yyy, e.g. http://localhost//TsOpcXSampleServer/Service.asmx. public virtual void Connect(string url) { Factory = null; var opcUrl = new OpcUrl(url); Connect(opcUrl, null); } /// /// Establishes a physical connection to the remote server. /// /// Any protocol configuration or user authentication information. public virtual void Connect(OpcConnectData connectData) { Connect(opcUrl_, connectData); } /// /// Establishes a physical connection to the remote server identified by a OpcUrl. /// /// The network address of the remote server. /// Any protocol configuration or user authentication information. public virtual void Connect(OpcUrl url, OpcConnectData connectData) { if (url == null) throw new ArgumentNullException(nameof(url)); if (Server != null) throw new OpcResultException(new OpcResult(OpcResult.E_FAIL.Code, OpcResult.FuncCallType.SysFuncCall, null), "The server is already connected."); // save url. SetUrl(url); try { Factory.ForceDa20Usage = ForceDa20Usage; // instantiate the server object. Server = Factory.CreateInstance(url, connectData); if (Server == null) throw new OpcResultException(new OpcResult(OpcResult.E_FAIL.Code, OpcResult.FuncCallType.SysFuncCall, null), "A connection to the server could not be established."); // save the connect data. connectData_ = connectData; try { // cache the supported locales. GetSupportedLocales(); // update the default locale. SetLocale(locale_); } catch { // ignored } } catch (Exception) { if (Server != null) { try { Disconnect(); } catch { // ignored } } throw; } } /// /// Disconnects from the server and releases all network resources. /// public virtual void Disconnect() { if (Server == null) throw new OpcResultException(new OpcResult(OpcResult.E_FAIL.Code, OpcResult.FuncCallType.SysFuncCall, null), "The server is not currently connected."); // dispose of the remote server object. Server.Dispose(); Server = null; } /// /// Creates a new instance of a server object with the same factory and url. /// /// This method does not copy the value of any properties. /// An unconnected duplicate instance of the server object. public virtual OpcServer Duplicate() { var instance = (OpcServer)Activator.CreateInstance(GetType(), Factory, opcUrl_); // preserve the credentials. instance.connectData_ = connectData_; // preserve the locale. instance.locale_ = locale_; return instance; } /// /// An event to receive server shutdown notifications. /// public virtual event OpcServerShutdownEventHandler ServerShutdownEvent { add => Server.ServerShutdownEvent += value; remove => Server.ServerShutdownEvent -= value; } /// /// The locale used in any error messages or results returned to the client. /// /// The locale name in the format "[languagecode]-[country/regioncode]". public virtual string GetLocale() { if (Server == null) throw new NotConnectedException(); // cache the current locale. locale_ = Server.GetLocale(); // return the cached value. return locale_; } /// /// Sets the locale used in any error messages or results returned to the client. /// /// The locale name in the format "[languagecode]-[country/regioncode]". /// A locale that the server supports and is the best match for the requested locale. public virtual string SetLocale(string locale) { if (Server == null) throw new NotConnectedException(); try { // set the requested locale on the server. locale_ = Server.SetLocale(locale); } catch { // find a best match and check if the server supports it. var revisedLocale = FindBestLocale(locale, supportedLocales_); if (revisedLocale != locale) { Server.SetLocale(revisedLocale); } // cache the revised locale. locale_ = revisedLocale; } // return actual local used. return locale_; } /// /// Returns the locales supported by the server /// /// The first element in the array must be the default locale for the server. /// An array of locales with the format "[languagecode]-[country/regioncode]". public virtual string[] GetSupportedLocales() { if (Server == null) throw new OpcResultException(new OpcResult(OpcResult.E_FAIL.Code, OpcResult.FuncCallType.SysFuncCall, null), "The server is not currently connected."); // cache supported locales. supportedLocales_ = Server.GetSupportedLocales(); // return copy of cached locales. return SupportedLocales; } /// /// Returns the localized text for the specified result code. /// /// The locale name in the format "[languagecode]-[country/regioncode]". /// The result code identifier. /// A message localized for the best match for the requested locale. public virtual string GetErrorText(string locale, OpcResult resultId) { if (Server == null) throw new OpcResultException(OpcResult.E_FAIL, "The server is not currently connected."); return Server.GetErrorText(locale ?? locale_, resultId); } #endregion } //============================================================================= // Exceptions /// /// Raised if an operation cannot be executed because the server is not connected. /// [Serializable] public class AlreadyConnectedException : ApplicationException { private const string Default = "The remote server is already connected."; /// public AlreadyConnectedException() : base(Default) { } /// public AlreadyConnectedException(string message) : base(Default + "\r\n" + message) { } /// public AlreadyConnectedException(Exception e) : base(Default, e) { } /// public AlreadyConnectedException(string message, Exception innerException) : base(Default + "\r\n" + message, innerException) { } /// protected AlreadyConnectedException(SerializationInfo info, StreamingContext context) : base(info, context) { } } /// /// Raised if an operation cannot be executed because the server is not connected. /// [Serializable] public class NotConnectedException : ApplicationException { private const string Default = "The remote server is not currently connected."; /// public NotConnectedException() : base(Default) { } /// public NotConnectedException(string message) : base(Default + "\r\n" + message) { } /// public NotConnectedException(Exception e) : base(Default, e) { } /// public NotConnectedException(string message, Exception innerException) : base(Default + "\r\n" + message, innerException) { } /// protected NotConnectedException(SerializationInfo info, StreamingContext context) : base(info, context) { } } /// /// Raised if an operation cannot be executed because the server is not reachable. /// [Serializable] public class ConnectFailedException : OpcResultException { private const string Default = "Could not connect to server."; /// public ConnectFailedException() : base(OpcResult.E_ACCESS_DENIED, Default) { } /// public ConnectFailedException(string message) : base(OpcResult.E_NETWORK_ERROR, Default + "\r\n" + message) { } /// public ConnectFailedException(Exception e) : base(OpcResult.E_NETWORK_ERROR, Default, e) { } /// public ConnectFailedException(string message, Exception innerException) : base(OpcResult.E_NETWORK_ERROR, Default + "\r\n" + message, innerException) { } /// protected ConnectFailedException(SerializationInfo info, StreamingContext context) : base(info, context) { } } /// /// Raised if an operation cannot be executed because the server refuses access. /// [Serializable] public class AccessDeniedException : OpcResultException { private const string Default = "The server refused the connection."; /// public AccessDeniedException() : base(OpcResult.E_ACCESS_DENIED, Default) { } /// public AccessDeniedException(string message) : base(OpcResult.E_ACCESS_DENIED, Default + "\r\n" + message) { } /// public AccessDeniedException(Exception e) : base(OpcResult.E_ACCESS_DENIED, Default, e) { } /// public AccessDeniedException(string message, Exception innerException) : base(OpcResult.E_NETWORK_ERROR, Default + "\r\n" + message, innerException) { } /// protected AccessDeniedException(SerializationInfo info, StreamingContext context) : base(info, context) { } } /// /// Raised an remote operation by the server timed out /// public class ServerTimeoutException : OpcResultException { private const string Default = "The server did not respond within the specified timeout period."; /// public ServerTimeoutException() : base(OpcResult.E_TIMEDOUT, Default) { } /// public ServerTimeoutException(string message) : base(OpcResult.E_TIMEDOUT, Default + "\r\n" + message) { } /// public ServerTimeoutException(Exception e) : base(OpcResult.E_TIMEDOUT, Default, e) { } /// public ServerTimeoutException(string message, Exception innerException) : base(OpcResult.E_TIMEDOUT, Default + "\r\n" + message, innerException) { } /// protected ServerTimeoutException(SerializationInfo info, StreamingContext context) : base(info, context) { } } /// /// Raised an remote operation by the server returned unusable or invalid results. /// [Serializable] public class InvalidResponseException : ApplicationException { private const string Default = "The response from the server was invalid or incomplete."; /// public InvalidResponseException() : base(Default) { } /// public InvalidResponseException(string message) : base(Default + "\r\n" + message) { } /// public InvalidResponseException(Exception e) : base(Default, e) { } /// public InvalidResponseException(string message, Exception innerException) : base(Default + "\r\n" + message, innerException) { } /// protected InvalidResponseException(SerializationInfo info, StreamingContext context) : base(info, context) { } } /// /// Raised if the browse position is not valid. /// [Serializable] public class BrowseCannotContinueException : ApplicationException { private const string Default = "The browse operation cannot continue."; /// public BrowseCannotContinueException() : base(Default) { } /// public BrowseCannotContinueException(string message) : base(Default + "\r\n" + message) { } /// public BrowseCannotContinueException(string message, Exception innerException) : base(Default + "\r\n" + message, innerException) { } /// protected BrowseCannotContinueException(SerializationInfo info, StreamingContext context) : base(info, context) { } } /// /// Raised if the browse position is not valid. /// [Serializable] public class BadInternalErrorException : ApplicationException { private const string Default = "License required! You can't use this feature."; /// public BadInternalErrorException() : base(Default) { } /// public BadInternalErrorException(string message) : base(message) { } /// public BadInternalErrorException(string message, Exception innerException) : base(message, innerException) { } /// protected BadInternalErrorException(SerializationInfo info, StreamingContext context) : base(info, context) { } } /// /// Exception that is raise when a DCOM call is cancelled due to timeout /// /// [Serializable] public class DCOMCallCancelledException : ApplicationException { private const string Default = "The current pending DCOM call was cancelled"; /// public DCOMCallCancelledException() : base(Default) { } /// public DCOMCallCancelledException(string message) : base(message) { } /// public DCOMCallCancelledException(string message, Exception innerException) : base(message, innerException) { } /// protected DCOMCallCancelledException(SerializationInfo info, StreamingContext context) : base(info, context) { } } }