#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; using Technosoftware.DaAeHdaClient.Com.Utilities; using Technosoftware.DaAeHdaClient.Utilities; using Technosoftware.OpcRcw.Comn; #endregion namespace Technosoftware.DaAeHdaClient.Com { /// /// An in-process wrapper for a remote OPC COM server (not thread safe). /// internal class Server : IOpcServer { #region Fields /// /// The COM server wrapped by the object. /// protected object server_; /// /// The URL containing host, prog id and clsid information for The remote server. /// protected OpcUrl url_; /// /// A connect point with the COM server. /// private ConnectionPoint connection_; /// /// The internal object that implements the IOPCShutdown interface. /// private Callback callback_; /// /// The synchronization object for server access /// private static volatile object lock_ = new object(); private int outstandingCalls_; #endregion #region Constructors /// /// Initializes the object. /// internal Server() { url_ = null; server_ = null; callback_ = new Callback(this); } /// /// Initializes the object with the specifed COM server. /// internal Server(OpcUrl url, object server) { if (url == null) throw new ArgumentNullException(nameof(url)); url_ = (OpcUrl)url.Clone(); server_ = server; callback_ = new Callback(this); } #endregion #region IDisposable Members /// /// The finalizer. /// ~Server() { Dispose(false); } /// /// Releases unmanaged resources held by the object. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Dispose(bool disposing) executes in two distinct scenarios. /// If disposing equals true, the method has been called directly /// or indirectly by a user's code. Managed and unmanaged resources /// can be disposed. /// If disposing equals false, the method has been called by the /// runtime from inside the finalizer and you should not reference /// other objects. Only unmanaged resources can be disposed. /// /// If true managed and unmanaged resources can be disposed. If false only unmanaged resources. protected virtual void Dispose(bool disposing) { if (!disposed_) { lock (this) { if (disposing) { // Free other state (managed objects). // close callback connections. if (connection_ != null) { connection_.Dispose(); connection_ = null; } } DisableDCOMCallCancellation(); // Free your own state (unmanaged objects). // Set large fields to null. // release server. Interop.ReleaseServer(server_); server_ = null; } disposed_ = true; } } private bool disposed_; #endregion #region Public Methods /// /// Connects to the server with the specified URL and credentials. /// public virtual void Initialize(OpcUrl url, OpcConnectData connectData) { if (url == null) throw new ArgumentNullException(nameof(url)); lock (lock_) { // re-connect only if the url has changed or has not been initialized. if (url_ == null || !url_.Equals(url)) { // release the current server. if (server_ != null) { Uninitialize(); } // instantiate a new server. server_ = (IOPCCommon)Factory.Connect(url, connectData); } // save url. url_ = (OpcUrl)url.Clone(); } } /// /// Releases The remote server. /// public virtual void Uninitialize() { lock (lock_) { Dispose(); } } /// /// 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) { try { ((IOPCCommon)server_).SetClientName(clientName); } catch (Exception e) { throw Utilities.Interop.CreateException("IOPCCommon.SetClientName", e); } } /// /// Allows cancellation control of DCOM callbacks to the server - by default DCOM calls will wait the default DCOM timeout /// to fail - this method allows for tigher control of the timeout to wait. Note that DOCM calls can only be controlled /// on a COM Single Threaded Apartment thread - use [STAThread] attribute on your application entry point or use Thread SetThreadApartment /// before the thread the server is operating on is created to STA. /// /// The DCOM call timeout - uses the default timeout if not specified public void EnableDCOMCallCancellation(TimeSpan timeout = default) { DCOMCallWatchdog.Enable(timeout); } /// /// Disables cancellation control of DCOM calls to the server /// public void DisableDCOMCallCancellation() { DCOMCallWatchdog.Disable(); } #endregion #region IOpcServer Members /// /// An event to receive server shutdown notifications. /// public virtual event OpcServerShutdownEventHandler ServerShutdownEvent { add { lock (lock_) { try { Advise(); callback_.ServerShutdown += value; } catch { // shutdown not supported. } } } remove { lock (lock_) { callback_.ServerShutdown -= value; Unadvise(); } } } /// /// 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() { lock (this) { if (server_ == null) throw new NotConnectedException(); try { ((IOPCCommon)server_).GetLocaleID(out var localeId); return Interop.GetLocale(localeId); } catch (Exception e) { throw Interop.CreateException("IOPCCommon.GetLocaleID", e); } } } /// /// 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) { lock (this) { if (server_ == null) throw new NotConnectedException(); var lcid = Interop.GetLocale(locale); try { ((IOPCCommon)server_).SetLocaleID(lcid); } catch (Exception e) { if (lcid != 0) { throw Interop.CreateException("IOPCCommon.SetLocaleID", e); } // use LOCALE_SYSTEM_DEFAULT if the server does not support the Neutral LCID. try { ((IOPCCommon)server_).SetLocaleID(0x800); } catch { } } return GetLocale(); } } /// /// 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() { lock (lock_) { if (server_ == null) throw new NotConnectedException(); try { var count = 0; var pLocaleIDs = IntPtr.Zero; ((IOPCCommon)server_).QueryAvailableLocaleIDs(out count, out pLocaleIDs); var localeIDs = Interop.GetInt32s(ref pLocaleIDs, count, true); if (localeIDs != null) { var locales = new ArrayList(); foreach (var localeID in localeIDs) { try { locales.Add(Interop.GetLocale(localeID)); } catch { } } return (string[])locales.ToArray(typeof(string)); } return null; } catch { //throw Interop.CreateException("IOPCCommon.QueryAvailableLocaleIDs", e); return null; } } } /// /// 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) { lock (lock_) { if (server_ == null) throw new NotConnectedException(); try { var currentLocale = GetLocale(); if (currentLocale != locale) { SetLocale(locale); } ((IOPCCommon)server_).GetErrorString(resultId.Code, out var errorText); if (currentLocale != locale) { SetLocale(currentLocale); } return errorText; } catch (Exception e) { throw Utilities.Interop.CreateException("IOPCServer.GetErrorString", e); } } } #endregion #region Protected Members /// /// Releases all references to the server. /// protected virtual void ReleaseServer() { lock (lock_) { SafeNativeMethods.ReleaseServer(server_); server_ = null; } } /// /// Checks if the server supports the specified interface. /// /// The interface to check. /// True if the server supports the interface. protected bool SupportsInterface() where T : class { lock (lock_) { return server_ is T; } } #endregion #region COM Call Tracing /// /// Must be called before any COM call. /// /// The interface to used when making the call. /// Name of the method. /// if set to true interface is an required interface and and exception is thrown on error. /// protected T BeginComCall(string methodName, bool isRequiredInterface) where T : class { return BeginComCall(server_, methodName, isRequiredInterface); } /// /// Must be called before any COM call. /// /// The interface to used when making the call. /// Parent COM object /// Name of the method. /// if set to true interface is an required interface and and exception is thrown on error. /// protected T BeginComCall(object parent, string methodName, bool isRequiredInterface) where T : class { Utils.Trace(Utils.TraceMasks.ExternalSystem, "{0} called.", methodName); lock (lock_) { outstandingCalls_++; if (parent == null) { if (isRequiredInterface) { throw new NotConnectedException(); } } var comObject = parent as T; if (comObject == null) { if (isRequiredInterface) { throw new NotSupportedException(Utils.Format("OPC Interface '{0}' is a required interface but not supported by the server.", typeof(T).Name)); } else { Utils.Trace(Utils.TraceMasks.ExternalSystem, "OPC Interface '{0}' is not supported by server but it is only an optional one.", typeof(T).Name); } } DCOMCallWatchdog.Set(); return comObject; } } /// /// Must called if a COM call returns an unexpected exception. /// /// Name of the method. /// The exception. /// Note that some COM calls are expected to return errors. protected void ComCallError(string methodName, Exception e) { SafeNativeMethods.TraceComError(e, methodName); } /// /// Must be called in the finally block after making a COM call. /// /// Name of the method. protected void EndComCall(string methodName) { Utils.Trace(Utils.TraceMasks.ExternalSystem, "{0} completed.", methodName); lock (lock_) { outstandingCalls_--; DCOMCallWatchdog.Reset(); } } #endregion #region Private Methods /// /// Establishes a connection point callback with the COM server. /// private void Advise() { if (connection_ == null) { connection_ = new ConnectionPoint(server_, typeof(IOPCShutdown).GUID); connection_.Advise(callback_); } } /// /// Closes a connection point callback with the COM server. /// private void Unadvise() { if (connection_ != null) { if (connection_.Unadvise() == 0) { connection_.Dispose(); connection_ = null; } } } /// /// A class that implements the IOPCShutdown interface. /// private class Callback : IOPCShutdown { /// /// Initializes the object with the containing subscription object. /// public Callback(Server server) { m_server = server; } /// /// An event to receive server shutdown notificiations. /// public event OpcServerShutdownEventHandler ServerShutdown { add { lock (lock_) { m_serverShutdown += value; } } remove { lock (lock_) { m_serverShutdown -= value; } } } /// /// A table of item identifiers indexed by internal handle. /// private Server m_server = null; /// /// Raised when data changed callbacks arrive. /// private event OpcServerShutdownEventHandler m_serverShutdown = null; /// /// Called when a shutdown event is received. /// public void ShutdownRequest(string reason) { try { lock (lock_) { if (m_serverShutdown != null) { m_serverShutdown(reason); } } } catch (Exception e) { var stack = e.StackTrace; } } } #endregion } }