//******************************************************************************************************// MetadataImportAdapter.cs - Gbtc//// Copyright © 2018, Grid Protection Alliance. All Rights Reserved.//// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See// the NOTICE file distributed with this work for additional information regarding copyright ownership.// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this// file except in compliance with the License. You may obtain a copy of the License at://// http://opensource.org/licenses/MIT//// Unless agreed to in writing, the subject software distributed under the License is distributed on an// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the// License for the specific language governing permissions and limitations.//// Code Modification History:// ----------------------------------------------------------------------------------------------------// 02/24/2018 - Stephen C. Wills// Generated original version of source code.////****************************************************************************************************** using GSF;using GSF.Collections;using GSF.Configuration;using GSF.Data;using GSF.Diagnostics;using GSF.Scheduling;using GSF.Threading;using GSF.TimeSeries;using GSF.TimeSeries.Adapters;using System;using System.Collections.Generic;using System.ComponentModel;using System.Data;using System.IO;using System.Linq;using System.Threading; namespace MetadataAdapters{ /// <summary> /// Represents an adapter that reads metadata from a file and updates the database. /// </summary> [Description("Metadata Import: Reads metadata from a file and updates the database.")] public class MetadataImportAdapter : FacileActionAdapterBase { #region [ Members ] // Constants /// <summary> /// Defines the default value for the <see cref="ImportSchedule"/> property. /// </summary> public const string DefaultImportSchedule = "*/5 * * * *"; /// <summary> /// Defines the default value for the <see cref="UseTransactionForMetadata"/> property. /// </summary> public const bool DefaultUseTransactionForMetadata = true; /// <summary> /// Defines the default value for the <see cref="MetadataSynchronizationTimeout"/> property. /// </summary> public const int DefaultMetadataSynchronizationTimeout = 0; /// <summary> /// Defines the default value for the <see cref="UseSourcePrefixNames"/> property. /// </summary> public const bool DefaultUseSourcePrefixNames = false; private const string ScheduleName = nameof(MetadataImportAdapter); // Fields private Action<DataSet> m_synchronizeMetadataAction; private ScheduleManager m_scheduleManager; private DateTime m_lastMetaDataRefreshTime; private bool m_disposed; #endregion #region [ Constructors ] /// <summary> /// Creates a new instance of the <see cref="MetadataImportAdapter"/> class. /// </summary> public MetadataImportAdapter() { m_synchronizeMetadataAction = GetSynchronizeMetadataAction(); m_scheduleManager = new ScheduleManager(); m_scheduleManager.ScheduleDue += ScheduleManager_ScheduleDue; } #endregion #region [ Properties ] /// <summary> /// Gets or sets the path to the file in which the metadata to be imported will be defined. /// </summary> [ConnectionStringParameter] [Description("Defines the path to the file in which the metadata to be imported will be defined.")] public string ImportFilePath { get; set; } /// <summary> /// Gets or sets the schedule, using cron syntax, to search for metadata files to import. /// </summary> [ConnectionStringParameter] [DefaultValue(DefaultImportSchedule)] [Description("Defines the schedule, using cron syntax, to search for metadata files to import.")] public string ImportSchedule { get; set; } /// <summary> /// Gets or sets the acronym of the device used as the parent of the devices defined in the metadata. /// </summary> [ConnectionStringParameter] [DefaultValue("")] [Description("Defines the acronym of the device used as the parent of the devices defined in the metadata.")] public string ParentDeviceAcronym { get; set; } /// <summary> /// Gets or sets flag that determines if metadata synchronization should be performed within a transaction. /// </summary> [ConnectionStringParameter] [DefaultValue(DefaultUseTransactionForMetadata)] [Description("Defines the flag that determines if metadata synchronization should be performed within a transaction.")] public bool UseTransactionForMetadata { get; set; } /// <summary> /// Gets or sets the timeout used when executing database queries during metadata synchronization. /// </summary> [ConnectionStringParameter] [DefaultValue(DefaultMetadataSynchronizationTimeout)] [Description("Defines the timeout used when exeucting database queries during metadata synchronization.")] public int MetadataSynchronizationTimeout { get; set; } /// <summary> /// Gets or sets flag that determines if child devices associated with a subscription /// should be prefixed with the subscription name and an exclamation point to ensure /// device name uniqueness - recommended value is <c>true</c>. /// </summary> [ConnectionStringParameter] [DefaultValue(DefaultUseSourcePrefixNames)] [Description("Defines the flag that determines if child device acronyms should be prefixed with the parent acronym.")] public bool UseSourcePrefixNames { get; set; } /// <summary> /// Gets the flag indicating if this adapter supports temporal processing. /// </summary> public override bool SupportsTemporalProcessing => false; /// <summary> /// Gets or sets primary keys of input measurements the <see cref="MetadataImportAdapter"/> expects, if any. /// </summary> [EditorBrowsable(EditorBrowsableState.Never)] public new MeasurementKey[] InputMeasurementKeys { get { return base.InputMeasurementKeys; } set { base.InputMeasurementKeys = value; } } /// <summary> /// Gets or sets output measurements that the <see cref="MetadataImportAdapter"/> will produce, if any. /// </summary> [EditorBrowsable(EditorBrowsableState.Never)] public new IMeasurement[] OutputMeasurements { get { return base.OutputMeasurements; } set { base.OutputMeasurements = value; } } /// <summary> /// Gets or sets the frames per second to be used by the <see cref="MetadataImportAdapter"/>. /// </summary> [EditorBrowsable(EditorBrowsableState.Never)] public new int FramesPerSecond { get { return base.FramesPerSecond; } set { base.FramesPerSecond = value; } } /// <summary> /// Gets or sets the allowed past time deviation tolerance, in seconds (can be sub-second). /// </summary> [EditorBrowsable(EditorBrowsableState.Never)] public new double LagTime { get { return base.LagTime; } set { base.LagTime = value; } } /// <summary> /// Gets or sets the allowed future time deviation tolerance, in seconds (can be sub-second). /// </summary> [EditorBrowsable(EditorBrowsableState.Never)] public new double LeadTime { get { return base.LeadTime; } set { base.LeadTime = value; } } #endregion #region [ Methods ] /// <summary> /// Initializes <see cref="MetadataImportAdapter"/>. /// </summary> public override void Initialize() { ConnectionStringParser<ConnectionStringParameterAttribute> parser = new ConnectionStringParser<ConnectionStringParameterAttribute>(); parser.ParseConnectionString(ConnectionString, this); base.Initialize(); if (string.IsNullOrWhiteSpace(ParentDeviceAcronym)) ParentDeviceAcronym = Name + "!PARENT"; // Default to not using transactions for metadata on SQL server (helps avoid deadlocks) if (!Settings.ContainsKey(nameof(UseTransactionForMetadata))) { using (AdoDataConnection database = new AdoDataConnection("systemSettings")) { if (database.IsSQLServer) UseTransactionForMetadata = false; } } m_scheduleManager.AddSchedule(ScheduleName, ImportSchedule); } /// <summary> /// Starts the <see cref="MetadataImportAdapter"/> or restarts it if it is already running. /// </summary> public override void Start() { base.Start(); m_scheduleManager.Start(); } /// <summary> /// Stops the <see cref="MetadataImportAdapter"/>. /// </summary> public override void Stop() { base.Stop(); m_scheduleManager.Stop(); } /// <summary> /// Searches for metadata to be imported and imports the metadata if found. /// </summary> [AdapterCommand("Imports metadata if found at the target location.")] public void ImportMetadata() { if (!File.Exists(ImportFilePath)) return; const int MaxFailedAttempts = 5; const int DelayAfterFailure = 2000; int failedAttempts = 0; while (true) { try { DataSet metadata; using (FileStream stream = File.OpenRead(ImportFilePath)) { metadata = stream.DeserializeToDataSet(); } m_synchronizeMetadataAction(metadata); File.Delete(ImportFilePath); break; } catch (IOException) { // Only throw an IOException // after five failed attempts failedAttempts++; if (failedAttempts >= MaxFailedAttempts) throw; Thread.Sleep(DelayAfterFailure); } } } /// <summary> /// Gets a short one-line status of this <see cref="MetadataExportAdapter"/>. /// </summary> /// <param name="maxLength">Maximum number of available characters for display.</param> /// <returns>A short one-line summary of the current status of this <see cref="MetadataExportAdapter"/>.</returns> public override string GetShortStatus(int maxLength) { string timestampFormat = "HH:mm:ss"; if (DateTime.Now.Subtract(m_lastMetaDataRefreshTime).TotalDays > 1.0D) timestampFormat = "yyyy-MM-dd HH:mm"; string lastImport = m_lastMetaDataRefreshTime.ToString(timestampFormat); string shortStatus = $"Last import: {m_lastMetaDataRefreshTime}"; return shortStatus.CenterText(maxLength); } /// <summary> /// Releases the unmanaged resources used by the <see cref="MetadataImportAdapter"/> object and optionally releases the managed resources. /// </summary> /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param> protected override void Dispose(bool disposing) { if (!m_disposed) { try { if (disposing) { m_scheduleManager.Stop(); m_scheduleManager.Dispose(); } } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } } /// <summary> /// Handles metadata synchronization to local system. /// </summary> private Action<DataSet> GetSynchronizeMetadataAction() { DataSet metadataToSynchronize = default(DataSet); DataSet synchronizedMetadata = default(DataSet); Guid nodeID = default(Guid); int virtualProtocolID = default(int); long syncProgressTotalActions = default(long); long syncProgressActionsCount = default(long); long syncProgressUpdateInterval = default(long); long syncProgressLastMessage = default(long); Action<long> initSyncProgress = totalActions => { syncProgressTotalActions = totalActions; syncProgressActionsCount = 0; // We update user on progress with 5 messages or every 15 seconds syncProgressUpdateInterval = (long)(totalActions * 0.2D); syncProgressLastMessage = DateTime.UtcNow.Ticks; }; Action updateSyncProgress = () => { syncProgressActionsCount++; if (syncProgressActionsCount % syncProgressUpdateInterval == 0 || DateTime.UtcNow.Ticks - syncProgressLastMessage > 150000000) { OnStatusMessage(MessageLevel.Info, $"metadata synchronization is {syncProgressActionsCount / (double)syncProgressTotalActions:0.0%} complete..."); syncProgressLastMessage = DateTime.UtcNow.Ticks; } }; Func<DataSet, bool> synchronizedMetadataChanged = newSynchronizedMetadata => { try { return !DataSetEqualityComparer.Default.Equals(synchronizedMetadata, newSynchronizedMetadata); } catch { return true; } }; Action synchronizeMetadata = () => { DataSet metadata = metadataToSynchronize; // Only perform database synchronization if metadata has changed since last update if (!synchronizedMetadataChanged(metadata)) return; if ((object)metadata == null) { OnStatusMessage(MessageLevel.Error, "metadata synchronization was not performed, deserialized dataset was empty."); return; } // Track total metadata synchronization process time Ticks startTime = DateTime.UtcNow.Ticks; DateTime updateTime; DateTime latestUpdateTime = DateTime.MinValue; // Open the configuration database using settings found in the config file using (AdoDataConnection database = new AdoDataConnection("systemSettings")) using (IDbCommand command = database.Connection.CreateCommand()) { IDbTransaction transaction = null; if (UseTransactionForMetadata) transaction = database.Connection.BeginTransaction(database.DefaultIsolationLevel); try { if ((object)transaction != null) command.Transaction = transaction; // Determine the active node ID - we cache this since this value won't change for the lifetime of this class if (nodeID == Guid.Empty) nodeID = Guid.Parse(command.ExecuteScalar($"SELECT NodeID FROM IaonActionAdapter WHERE ID = {(int)ID}", MetadataSynchronizationTimeout).ToString()); // Determine the protocol record auto-inc ID value for the gateway transport protocol (GEP) - this value is also cached since it shouldn't change for the lifetime of this class if (virtualProtocolID == 0) virtualProtocolID = int.Parse(command.ExecuteScalar("SELECT ID FROM Protocol WHERE Acronym = 'VirtualInput'", MetadataSynchronizationTimeout).ToString()); // Define SQL statement to query if parent device is already defined string parentDeviceIDSql = database.ParameterizedQueryString("SELECT ID FROM Device WHERE Acronym = {0}", "acronym"); // Define SQL statement to insert new parent device record string insertParentDeviceSql = database.ParameterizedQueryString("INSERT INTO Device(NodeID, HistorianID, Acronym, Name, ProtocolID, IsConcentrator, Enabled) " + "VALUES ({0}, {1}, {2}, {3}, {4}, 1, 1)", "nodeID", "historianID", "acronym", "name", "protocolID"); // Query the actual record ID based on the known run-time ID for this subscriber device object sourceID = command.ExecuteScalar(parentDeviceIDSql, MetadataSynchronizationTimeout, ParentDeviceAcronym); if (sourceID == null || sourceID == DBNull.Value) { // Get a historian ID, but exclude the STAT historian object randomHistorianID = command.ExecuteScalar($"SELECT ID FROM Historian WHERE Acronym <> 'STAT'", MetadataSynchronizationTimeout); command.ExecuteNonQuery(insertParentDeviceSql, MetadataSynchronizationTimeout, database.Guid(nodeID), randomHistorianID, ParentDeviceAcronym, ParentDeviceAcronym, virtualProtocolID); sourceID = command.ExecuteScalar(parentDeviceIDSql, MetadataSynchronizationTimeout, ParentDeviceAcronym); } int parentID = Convert.ToInt32(sourceID); // Validate that the subscriber device is marked as a concentrator (we are about to associate children devices with it) if (!command.ExecuteScalar($"SELECT IsConcentrator FROM Device WHERE ID = {parentID}", MetadataSynchronizationTimeout).ToString().ParseBoolean()) command.ExecuteNonQuery($"UPDATE Device SET IsConcentrator = 1 WHERE ID = {parentID}", MetadataSynchronizationTimeout); // Get any historian associated with the subscriber device object historianID = command.ExecuteScalar($"SELECT HistorianID FROM Device WHERE ID = {parentID}", MetadataSynchronizationTimeout); // Ascertain total number of actions required for all metadata synchronization so some level feed back can be provided on progress initSyncProgress(metadata.Tables.Cast<DataTable>().Sum(dataTable => (long)dataTable.Rows.Count) + 3); // Prefix all children devices with the name of the parent since the same device names could appear in different connections (helps keep device names unique) string sourcePrefix = UseSourcePrefixNames ? ParentDeviceAcronym + "!" : ""; Dictionary<string, int> deviceIDs = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); string deviceAcronym, signalTypeAcronym; int deviceID; // Check to see if data for the "DeviceDetail" table was included in the metadata if (metadata.Tables.Contains("DeviceDetail")) { DataTable deviceDetail = metadata.Tables["DeviceDetail"]; List<Guid> uniqueIDs = new List<Guid>(); // Define SQL statement to query if this device is already defined (this should always be based on the unique guid-based device ID) string deviceExistsSql = database.ParameterizedQueryString("SELECT COUNT(*) FROM Device WHERE UniqueID = {0}", "uniqueID"); // Define SQL statement to insert new device record string insertDeviceSql = database.ParameterizedQueryString("INSERT INTO Device(NodeID, ParentID, HistorianID, Acronym, Name, ProtocolID, FramesPerSecond, AccessID, Longitude, Latitude, ContactList, IsConcentrator, Enabled) " + "VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, 0, 1)", "nodeID", "parentID", "historianID", "acronym", "name", "protocolID", "framesPerSecond", "accessID", "longitude", "latitude", "contactList"); Lines 1–500 of 981. Load more
//****************************************************************************************************** // MetadataImportAdapter.cs - Gbtc // // Copyright © 2018, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this // file except in compliance with the License. You may obtain a copy of the License at: // // http://opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 02/24/2018 - Stephen C. Wills // Generated original version of source code. // //****************************************************************************************************** using GSF; using GSF.Collections; using GSF.Configuration; using GSF.Data; using GSF.Diagnostics; using GSF.Scheduling; using GSF.Threading; using GSF.TimeSeries; using GSF.TimeSeries.Adapters; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.IO; using System.Linq; using System.Threading; namespace MetadataAdapters { /// <summary> /// Represents an adapter that reads metadata from a file and updates the database. /// </summary> [Description("Metadata Import: Reads metadata from a file and updates the database.")] public class MetadataImportAdapter : FacileActionAdapterBase { #region [ Members ] // Constants /// <summary> /// Defines the default value for the <see cref="ImportSchedule"/> property. /// </summary> public const string DefaultImportSchedule = "*/5 * * * *"; /// <summary> /// Defines the default value for the <see cref="UseTransactionForMetadata"/> property. /// </summary> public const bool DefaultUseTransactionForMetadata = true; /// <summary> /// Defines the default value for the <see cref="MetadataSynchronizationTimeout"/> property. /// </summary> public const int DefaultMetadataSynchronizationTimeout = 0; /// <summary> /// Defines the default value for the <see cref="UseSourcePrefixNames"/> property. /// </summary> public const bool DefaultUseSourcePrefixNames = false; private const string ScheduleName = nameof(MetadataImportAdapter); // Fields private Action<DataSet> m_synchronizeMetadataAction; private ScheduleManager m_scheduleManager; private DateTime m_lastMetaDataRefreshTime; private bool m_disposed; #endregion #region [ Constructors ] /// <summary> /// Creates a new instance of the <see cref="MetadataImportAdapter"/> class. /// </summary> public MetadataImportAdapter() { m_synchronizeMetadataAction = GetSynchronizeMetadataAction(); m_scheduleManager = new ScheduleManager(); m_scheduleManager.ScheduleDue += ScheduleManager_ScheduleDue; } #endregion #region [ Properties ] /// <summary> /// Gets or sets the path to the file in which the metadata to be imported will be defined. /// </summary> [ConnectionStringParameter] [Description("Defines the path to the file in which the metadata to be imported will be defined.")] public string ImportFilePath { get; set; } /// <summary> /// Gets or sets the schedule, using cron syntax, to search for metadata files to import. /// </summary> [ConnectionStringParameter] [DefaultValue(DefaultImportSchedule)] [Description("Defines the schedule, using cron syntax, to search for metadata files to import.")] public string ImportSchedule { get; set; } /// <summary> /// Gets or sets the acronym of the device used as the parent of the devices defined in the metadata. /// </summary> [ConnectionStringParameter] [DefaultValue("")] [Description("Defines the acronym of the device used as the parent of the devices defined in the metadata.")] public string ParentDeviceAcronym { get; set; } /// <summary> /// Gets or sets flag that determines if metadata synchronization should be performed within a transaction. /// </summary> [ConnectionStringParameter] [DefaultValue(DefaultUseTransactionForMetadata)] [Description("Defines the flag that determines if metadata synchronization should be performed within a transaction.")] public bool UseTransactionForMetadata { get; set; } /// <summary> /// Gets or sets the timeout used when executing database queries during metadata synchronization. /// </summary> [ConnectionStringParameter] [DefaultValue(DefaultMetadataSynchronizationTimeout)] [Description("Defines the timeout used when exeucting database queries during metadata synchronization.")] public int MetadataSynchronizationTimeout { get; set; } /// <summary> /// Gets or sets flag that determines if child devices associated with a subscription /// should be prefixed with the subscription name and an exclamation point to ensure /// device name uniqueness - recommended value is <c>true</c>. /// </summary> [ConnectionStringParameter] [DefaultValue(DefaultUseSourcePrefixNames)] [Description("Defines the flag that determines if child device acronyms should be prefixed with the parent acronym.")] public bool UseSourcePrefixNames { get; set; } /// <summary> /// Gets the flag indicating if this adapter supports temporal processing. /// </summary> public override bool SupportsTemporalProcessing => false; /// <summary> /// Gets or sets primary keys of input measurements the <see cref="MetadataImportAdapter"/> expects, if any. /// </summary> [EditorBrowsable(EditorBrowsableState.Never)] public new MeasurementKey[] InputMeasurementKeys { get { return base.InputMeasurementKeys; } set { base.InputMeasurementKeys = value; } } /// <summary> /// Gets or sets output measurements that the <see cref="MetadataImportAdapter"/> will produce, if any. /// </summary> [EditorBrowsable(EditorBrowsableState.Never)] public new IMeasurement[] OutputMeasurements { get { return base.OutputMeasurements; } set { base.OutputMeasurements = value; } } /// <summary> /// Gets or sets the frames per second to be used by the <see cref="MetadataImportAdapter"/>. /// </summary> [EditorBrowsable(EditorBrowsableState.Never)] public new int FramesPerSecond { get { return base.FramesPerSecond; } set { base.FramesPerSecond = value; } } /// <summary> /// Gets or sets the allowed past time deviation tolerance, in seconds (can be sub-second). /// </summary> [EditorBrowsable(EditorBrowsableState.Never)] public new double LagTime { get { return base.LagTime; } set { base.LagTime = value; } } /// <summary> /// Gets or sets the allowed future time deviation tolerance, in seconds (can be sub-second). /// </summary> [EditorBrowsable(EditorBrowsableState.Never)] public new double LeadTime { get { return base.LeadTime; } set { base.LeadTime = value; } } #endregion #region [ Methods ] /// <summary> /// Initializes <see cref="MetadataImportAdapter"/>. /// </summary> public override void Initialize() { ConnectionStringParser<ConnectionStringParameterAttribute> parser = new ConnectionStringParser<ConnectionStringParameterAttribute>(); parser.ParseConnectionString(ConnectionString, this); base.Initialize(); if (string.IsNullOrWhiteSpace(ParentDeviceAcronym)) ParentDeviceAcronym = Name + "!PARENT"; // Default to not using transactions for metadata on SQL server (helps avoid deadlocks) if (!Settings.ContainsKey(nameof(UseTransactionForMetadata))) { using (AdoDataConnection database = new AdoDataConnection("systemSettings")) { if (database.IsSQLServer) UseTransactionForMetadata = false; } } m_scheduleManager.AddSchedule(ScheduleName, ImportSchedule); } /// <summary> /// Starts the <see cref="MetadataImportAdapter"/> or restarts it if it is already running. /// </summary> public override void Start() { base.Start(); m_scheduleManager.Start(); } /// <summary> /// Stops the <see cref="MetadataImportAdapter"/>. /// </summary> public override void Stop() { base.Stop(); m_scheduleManager.Stop(); } /// <summary> /// Searches for metadata to be imported and imports the metadata if found. /// </summary> [AdapterCommand("Imports metadata if found at the target location.")] public void ImportMetadata() { if (!File.Exists(ImportFilePath)) return; const int MaxFailedAttempts = 5; const int DelayAfterFailure = 2000; int failedAttempts = 0; while (true) { try { DataSet metadata; using (FileStream stream = File.OpenRead(ImportFilePath)) { metadata = stream.DeserializeToDataSet(); } m_synchronizeMetadataAction(metadata); File.Delete(ImportFilePath); break; } catch (IOException) { // Only throw an IOException // after five failed attempts failedAttempts++; if (failedAttempts >= MaxFailedAttempts) throw; Thread.Sleep(DelayAfterFailure); } } } /// <summary> /// Gets a short one-line status of this <see cref="MetadataExportAdapter"/>. /// </summary> /// <param name="maxLength">Maximum number of available characters for display.</param> /// <returns>A short one-line summary of the current status of this <see cref="MetadataExportAdapter"/>.</returns> public override string GetShortStatus(int maxLength) { string timestampFormat = "HH:mm:ss"; if (DateTime.Now.Subtract(m_lastMetaDataRefreshTime).TotalDays > 1.0D) timestampFormat = "yyyy-MM-dd HH:mm"; string lastImport = m_lastMetaDataRefreshTime.ToString(timestampFormat); string shortStatus = $"Last import: {m_lastMetaDataRefreshTime}"; return shortStatus.CenterText(maxLength); } /// <summary> /// Releases the unmanaged resources used by the <see cref="MetadataImportAdapter"/> object and optionally releases the managed resources. /// </summary> /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param> protected override void Dispose(bool disposing) { if (!m_disposed) { try { if (disposing) { m_scheduleManager.Stop(); m_scheduleManager.Dispose(); } } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } } /// <summary> /// Handles metadata synchronization to local system. /// </summary> private Action<DataSet> GetSynchronizeMetadataAction() { DataSet metadataToSynchronize = default(DataSet); DataSet synchronizedMetadata = default(DataSet); Guid nodeID = default(Guid); int virtualProtocolID = default(int); long syncProgressTotalActions = default(long); long syncProgressActionsCount = default(long); long syncProgressUpdateInterval = default(long); long syncProgressLastMessage = default(long); Action<long> initSyncProgress = totalActions => { syncProgressTotalActions = totalActions; syncProgressActionsCount = 0; // We update user on progress with 5 messages or every 15 seconds syncProgressUpdateInterval = (long)(totalActions * 0.2D); syncProgressLastMessage = DateTime.UtcNow.Ticks; }; Action updateSyncProgress = () => { syncProgressActionsCount++; if (syncProgressActionsCount % syncProgressUpdateInterval == 0 || DateTime.UtcNow.Ticks - syncProgressLastMessage > 150000000) { OnStatusMessage(MessageLevel.Info, $"metadata synchronization is {syncProgressActionsCount / (double)syncProgressTotalActions:0.0%} complete..."); syncProgressLastMessage = DateTime.UtcNow.Ticks; } }; Func<DataSet, bool> synchronizedMetadataChanged = newSynchronizedMetadata => { try { return !DataSetEqualityComparer.Default.Equals(synchronizedMetadata, newSynchronizedMetadata); } catch { return true; } }; Action synchronizeMetadata = () => { DataSet metadata = metadataToSynchronize; // Only perform database synchronization if metadata has changed since last update if (!synchronizedMetadataChanged(metadata)) return; if ((object)metadata == null) { OnStatusMessage(MessageLevel.Error, "metadata synchronization was not performed, deserialized dataset was empty."); return; } // Track total metadata synchronization process time Ticks startTime = DateTime.UtcNow.Ticks; DateTime updateTime; DateTime latestUpdateTime = DateTime.MinValue; // Open the configuration database using settings found in the config file using (AdoDataConnection database = new AdoDataConnection("systemSettings")) using (IDbCommand command = database.Connection.CreateCommand()) { IDbTransaction transaction = null; if (UseTransactionForMetadata) transaction = database.Connection.BeginTransaction(database.DefaultIsolationLevel); try { if ((object)transaction != null) command.Transaction = transaction; // Determine the active node ID - we cache this since this value won't change for the lifetime of this class if (nodeID == Guid.Empty) nodeID = Guid.Parse(command.ExecuteScalar($"SELECT NodeID FROM IaonActionAdapter WHERE ID = {(int)ID}", MetadataSynchronizationTimeout).ToString()); // Determine the protocol record auto-inc ID value for the gateway transport protocol (GEP) - this value is also cached since it shouldn't change for the lifetime of this class if (virtualProtocolID == 0) virtualProtocolID = int.Parse(command.ExecuteScalar("SELECT ID FROM Protocol WHERE Acronym = 'VirtualInput'", MetadataSynchronizationTimeout).ToString()); // Define SQL statement to query if parent device is already defined string parentDeviceIDSql = database.ParameterizedQueryString("SELECT ID FROM Device WHERE Acronym = {0}", "acronym"); // Define SQL statement to insert new parent device record string insertParentDeviceSql = database.ParameterizedQueryString("INSERT INTO Device(NodeID, HistorianID, Acronym, Name, ProtocolID, IsConcentrator, Enabled) " + "VALUES ({0}, {1}, {2}, {3}, {4}, 1, 1)", "nodeID", "historianID", "acronym", "name", "protocolID"); // Query the actual record ID based on the known run-time ID for this subscriber device object sourceID = command.ExecuteScalar(parentDeviceIDSql, MetadataSynchronizationTimeout, ParentDeviceAcronym); if (sourceID == null || sourceID == DBNull.Value) { // Get a historian ID, but exclude the STAT historian object randomHistorianID = command.ExecuteScalar($"SELECT ID FROM Historian WHERE Acronym <> 'STAT'", MetadataSynchronizationTimeout); command.ExecuteNonQuery(insertParentDeviceSql, MetadataSynchronizationTimeout, database.Guid(nodeID), randomHistorianID, ParentDeviceAcronym, ParentDeviceAcronym, virtualProtocolID); sourceID = command.ExecuteScalar(parentDeviceIDSql, MetadataSynchronizationTimeout, ParentDeviceAcronym); } int parentID = Convert.ToInt32(sourceID); // Validate that the subscriber device is marked as a concentrator (we are about to associate children devices with it) if (!command.ExecuteScalar($"SELECT IsConcentrator FROM Device WHERE ID = {parentID}", MetadataSynchronizationTimeout).ToString().ParseBoolean()) command.ExecuteNonQuery($"UPDATE Device SET IsConcentrator = 1 WHERE ID = {parentID}", MetadataSynchronizationTimeout); // Get any historian associated with the subscriber device object historianID = command.ExecuteScalar($"SELECT HistorianID FROM Device WHERE ID = {parentID}", MetadataSynchronizationTimeout); // Ascertain total number of actions required for all metadata synchronization so some level feed back can be provided on progress initSyncProgress(metadata.Tables.Cast<DataTable>().Sum(dataTable => (long)dataTable.Rows.Count) + 3); // Prefix all children devices with the name of the parent since the same device names could appear in different connections (helps keep device names unique) string sourcePrefix = UseSourcePrefixNames ? ParentDeviceAcronym + "!" : ""; Dictionary<string, int> deviceIDs = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); string deviceAcronym, signalTypeAcronym; int deviceID; // Check to see if data for the "DeviceDetail" table was included in the metadata if (metadata.Tables.Contains("DeviceDetail")) { DataTable deviceDetail = metadata.Tables["DeviceDetail"]; List<Guid> uniqueIDs = new List<Guid>(); // Define SQL statement to query if this device is already defined (this should always be based on the unique guid-based device ID) string deviceExistsSql = database.ParameterizedQueryString("SELECT COUNT(*) FROM Device WHERE UniqueID = {0}", "uniqueID"); // Define SQL statement to insert new device record string insertDeviceSql = database.ParameterizedQueryString("INSERT INTO Device(NodeID, ParentID, HistorianID, Acronym, Name, ProtocolID, FramesPerSecond, AccessID, Longitude, Latitude, ContactList, IsConcentrator, Enabled) " + "VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, 0, 1)", "nodeID", "parentID", "historianID", "acronym", "name", "protocolID", "framesPerSecond", "accessID", "longitude", "latitude", "contactList");