diff --git a/src/lib/Common.cs b/src/lib/sttp.core/Common.cs
similarity index 88%
rename from src/lib/Common.cs
rename to src/lib/sttp.core/Common.cs
index 2a0972df..c211ff63 100644
--- a/src/lib/Common.cs
+++ b/src/lib/sttp.core/Common.cs
@@ -23,8 +23,9 @@
//
//******************************************************************************************************
-using System.Security.Cryptography;
-using GSF.Threading;
+#if NET
+#define MONO
+#endif
#if !MONO
using Microsoft.Win32;
@@ -40,13 +41,13 @@ public static class Common
static Common()
{
#if MONO
- UseManagedEncryption = true;
+ UseManagedEncryption = true;
#else
const string FipsKeyOld = @"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa";
const string FipsKeyNew = @"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\FipsAlgorithmPolicy";
// Determine if the operating system configuration to set to use FIPS-compliant algorithms
- UseManagedEncryption = (Registry.GetValue(FipsKeyNew, "Enabled", 0) ?? Registry.GetValue(FipsKeyOld, "FipsAlgorithmPolicy", 0)).ToString() == "0";
+ UseManagedEncryption = ((Registry.GetValue(FipsKeyNew, "Enabled", 0) ?? Registry.GetValue(FipsKeyOld, "FipsAlgorithmPolicy", 0))?.ToString() ?? "0") == "0";
#endif
TimerScheduler = new SharedTimerScheduler();
@@ -64,7 +65,12 @@ public static SymmetricAlgorithm SymmetricAlgorithm
{
get
{
+ #if NET
+ Aes symmetricAlgorithm = Aes.Create();
+ #else
Aes symmetricAlgorithm = UseManagedEncryption ? new AesManaged() : new AesCryptoServiceProvider();
+ #endif
+
symmetricAlgorithm.KeySize = 256;
return symmetricAlgorithm;
}
diff --git a/src/lib/CompactMeasurement.cs b/src/lib/sttp.core/CompactMeasurement.cs
similarity index 98%
rename from src/lib/CompactMeasurement.cs
rename to src/lib/sttp.core/CompactMeasurement.cs
index b1a59b9e..6b7f118e 100644
--- a/src/lib/CompactMeasurement.cs
+++ b/src/lib/sttp.core/CompactMeasurement.cs
@@ -27,12 +27,6 @@
//
//******************************************************************************************************
-using System;
-using GSF;
-using GSF.Parsing;
-using GSF.TimeSeries;
-using GSF.TimeSeries.Transport;
-
namespace sttp;
#region [ Enumerations ]
@@ -195,7 +189,7 @@ public class CompactMeasurement : Measurement, IBinaryMeasurement
/// Base time offset array - set to null to use full fidelity measurement time.
/// Time index to use for base offset.
/// Flag that determines if millisecond resolution is in use for this serialization.
- public CompactMeasurement(SignalIndexCache signalIndexCache, bool includeTime = true, long[] baseTimeOffsets = null, int timeIndex = 0, bool useMillisecondResolution = false)
+ public CompactMeasurement(SignalIndexCache signalIndexCache, bool includeTime = true, long[]? baseTimeOffsets = null, int timeIndex = 0, bool useMillisecondResolution = false)
{
m_signalIndexCache = signalIndexCache;
IncludeTime = includeTime;
@@ -215,7 +209,7 @@ public CompactMeasurement(SignalIndexCache signalIndexCache, bool includeTime =
/// Base time offset array - set to null to use full fidelity measurement time.
/// Time index to use for base offset.
/// Flag that determines if millisecond resolution is in use for this serialization.
- public CompactMeasurement(IMeasurement measurement, SignalIndexCache signalIndexCache, bool includeTime = true, long[] baseTimeOffsets = null, int timeIndex = 0, bool useMillisecondResolution = false)
+ public CompactMeasurement(IMeasurement measurement, SignalIndexCache signalIndexCache, bool includeTime = true, long[]? baseTimeOffsets = null, int timeIndex = 0, bool useMillisecondResolution = false)
{
Metadata = measurement.Metadata;
Value = measurement.Value;
@@ -335,7 +329,7 @@ public int RuntimeID
set
{
// Attempt to restore signal identification
- if (m_signalIndexCache.Reference.TryGetValue(value, out MeasurementKey key))
+ if (m_signalIndexCache.Reference.TryGetValue(value, out MeasurementKey? key) /* && key is not null */)
Metadata = key.Metadata;
else
throw new InvalidOperationException($"Failed to find associated signal identification for runtime ID {value}");
diff --git a/src/lib/DataGapRecoverer.cs b/src/lib/sttp.core/DataGapRecoverer.cs
similarity index 93%
rename from src/lib/DataGapRecoverer.cs
rename to src/lib/sttp.core/DataGapRecoverer.cs
index 30538b2d..d8957f99 100644
--- a/src/lib/DataGapRecoverer.cs
+++ b/src/lib/sttp.core/DataGapRecoverer.cs
@@ -21,21 +21,11 @@
//
//******************************************************************************************************
-using System;
-using System.Collections.Generic;
-using System.Data;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using GSF;
-using GSF.Diagnostics;
-using GSF.IO;
-using GSF.Threading;
-using GSF.TimeSeries;
-using GSF.TimeSeries.Adapters;
-using GSF.Units;
+#if NET
+using CommonFunc = Gemstone.Common;
+#else
+using CommonFunc = GSF.Common;
+#endif
namespace sttp;
@@ -110,7 +100,7 @@ public class DataGapRecoverer : ISupportLifecycle, IProvideStatus
///
/// is a collection of measurements for consumer to process.
///
- public event EventHandler>> RecoveredMeasurements;
+ public event EventHandler>>? RecoveredMeasurements;
///
/// Provides status messages to consumer.
@@ -118,7 +108,7 @@ public class DataGapRecoverer : ISupportLifecycle, IProvideStatus
///
/// is new status message.
///
- public event EventHandler> StatusMessage;
+ public event EventHandler>? StatusMessage;
///
/// Event is raised when there is an exception encountered during processing.
@@ -126,26 +116,26 @@ public class DataGapRecoverer : ISupportLifecycle, IProvideStatus
///
/// is the exception that was thrown.
///
- public event EventHandler> ProcessException;
+ public event EventHandler>? ProcessException;
///
/// Raised after the has been properly disposed.
///
- public event EventHandler Disposed;
+ public event EventHandler? Disposed;
// Fields
private readonly SubscriptionInfo m_subscriptionInfo;
private readonly ManualResetEventSlim m_dataGapRecoveryCompleted;
- private DataSubscriber m_temporalSubscription;
- private SharedTimer m_dataStreamMonitor;
- private DataSet m_dataSource;
- private string m_loggingPath;
- private string m_sourceConnectionName;
- private string m_connectionString;
+ private readonly SharedTimer m_dataStreamMonitor;
+ private DataSubscriber? m_temporalSubscription;
+ private DataSet? m_dataSource;
+ private string m_loggingPath = string.Empty;
+ private string m_sourceConnectionName = default!;
+ private string m_connectionString = default!;
private Time m_recoveryStartDelay;
private Time m_minimumRecoverySpan;
private Time m_maximumRecoverySpan;
- private Outage m_currentDataGap;
+ private Outage? m_currentDataGap;
private Ticks m_mostRecentRecoveredTime;
private long m_measurementsRecoveredForDataGap;
private long m_measurementsRecoveredOverLastInterval;
@@ -230,7 +220,7 @@ public string SourceConnectionName
///
/// Gets or sets based data source available to this .
///
- public DataSet DataSource
+ public DataSet? DataSource
{
get => m_dataSource;
set
@@ -437,7 +427,7 @@ public bool UseMillisecondResolution
/// Gets or sets any additional constraint parameters that will be supplied to adapters in temporal
/// subscription used when recovering data for an .
///
- public string ConstraintParameters
+ public string? ConstraintParameters
{
get => m_subscriptionInfo.ConstraintParameters;
set => m_subscriptionInfo.ConstraintParameters = value;
@@ -469,12 +459,12 @@ public bool Enabled
///
/// Gets reference to the data gap for this .
///
- protected OutageLog DataGapLog { get; private set; }
+ protected OutageLog? DataGapLog { get; private set; }
///
/// Gets reference to the data gap for this .
///
- protected OutageLogProcessor DataGapLogProcessor { get; private set; }
+ protected OutageLogProcessor? DataGapLogProcessor { get; private set; }
// Gets the name of the data gap recoverer.
string IProvideStatus.Name => m_temporalSubscription is null ? GetType().Name : m_temporalSubscription.Name;
@@ -505,7 +495,7 @@ public string Status
status.AppendLine($" Logging path: {FilePath.TrimFileName(m_loggingPath.ToNonNullNorWhiteSpace(FilePath.GetAbsolutePath("")), 51)}");
status.AppendLine($"Last recovered measurement: {((DateTime)m_mostRecentRecoveredTime).ToString(OutageLog.DateTimeFormat)}");
- Outage currentDataGap = m_currentDataGap;
+ Outage? currentDataGap = m_currentDataGap;
if (currentDataGap is not null)
status.AppendLine($" Currently recovering: {currentDataGap.Start.ToString(OutageLog.DateTimeFormat)} to {currentDataGap.End.ToString(OutageLog.DateTimeFormat)}");
@@ -557,20 +547,13 @@ protected virtual void Dispose(bool disposing)
if (!disposing)
return;
- if (m_dataGapRecoveryCompleted is not null)
- {
- // Signal any waiting threads
- m_abnormalTermination = true;
- m_dataGapRecoveryCompleted.Set();
- m_dataGapRecoveryCompleted.Dispose();
- }
+ // Signal any waiting threads
+ m_abnormalTermination = true;
+ m_dataGapRecoveryCompleted.Set();
+ m_dataGapRecoveryCompleted.Dispose();
- if (m_dataStreamMonitor is not null)
- {
- m_dataStreamMonitor.Elapsed -= DataStreamMonitor_Elapsed;
- m_dataStreamMonitor.Dispose();
- m_dataStreamMonitor = null;
- }
+ m_dataStreamMonitor.Elapsed -= DataStreamMonitor_Elapsed;
+ m_dataStreamMonitor.Dispose();
if (DataGapLogProcessor is not null)
{
@@ -613,7 +596,7 @@ public void Initialize()
Dictionary settings = m_connectionString.ToNonNullString().ParseKeyValuePairs();
- if (settings.TryGetValue("sourceConnectionName", out string setting) && !string.IsNullOrWhiteSpace(setting))
+ if (settings.TryGetValue("sourceConnectionName", out string? setting) && !string.IsNullOrWhiteSpace(setting))
m_sourceConnectionName = setting;
if (settings.TryGetValue("recoveryStartDelay", out setting) && double.TryParse(setting, out double timeInterval))
@@ -683,7 +666,7 @@ public void Initialize()
DataGapLog.Initialize();
// Setup data gap processor to process items one at a time, a 5-second minimum period is established between each gap processing
- DataGapLogProcessor = new OutageLogProcessor(DataGapLog, ProcessDataGap, CanProcessDataGap, ex => OnProcessException(MessageLevel.Warning, ex), GSF.Common.Max(5000, (int)(m_recoveryStartDelay * 1000.0D)));
+ DataGapLogProcessor = new OutageLogProcessor(DataGapLog, ProcessDataGap, CanProcessDataGap, ex => OnProcessException(MessageLevel.Warning, ex), CommonFunc.Max(5000, (int)(m_recoveryStartDelay * 1000.0D)));
}
///
@@ -760,6 +743,9 @@ public bool RemoveDataGap(DateTimeOffset startTime, DateTimeOffset endTime)
/// The contents of the outage log.
public string DumpOutageLog()
{
+ if (DataGapLog is null)
+ throw new InvalidOperationException("Data gap log is not defined -- cannot dump outage log.");
+
List outages = DataGapLog.Outages;
StringBuilder dump = new();
@@ -781,6 +767,12 @@ private bool CanProcessDataGap(Outage dataGap)
// to requeue the data gap outage so it will be processed again (could be that remote system is offline).
private void ProcessDataGap(Outage dataGap)
{
+ if (m_temporalSubscription is null)
+ throw new InvalidOperationException("Temporal subscription is not established -- cannot process data gap.");
+
+ if (DataGapLog is null)
+ throw new InvalidOperationException("Data gap log is not defined -- cannot process data gap.");
+
// Establish start and stop time for temporal session
m_subscriptionInfo.StartTime = dataGap.Start.ToString(OutageLog.DateTimeFormat, CultureInfo.InvariantCulture);
m_subscriptionInfo.StopTime = dataGap.End.ToString(OutageLog.DateTimeFormat, CultureInfo.InvariantCulture);
@@ -817,7 +809,7 @@ private void ProcessDataGap(Outage dataGap)
if (m_abnormalTermination)
{
// Make sure any data recovered so far doesn't get unnecessarily re-recovered, this requires that source historian report data in time-sorted order
- dataGap = new Outage(new DateTime(GSF.Common.Max((Ticks)dataGap.Start.Ticks, m_mostRecentRecoveredTime - (m_subscriptionInfo.UseMillisecondResolution ? Ticks.PerMillisecond : 1L)), DateTimeKind.Utc), dataGap.End);
+ dataGap = new Outage(new DateTime(CommonFunc.Max((Ticks)dataGap.Start.Ticks, m_mostRecentRecoveredTime - (m_subscriptionInfo.UseMillisecondResolution ? Ticks.PerMillisecond : 1L)), DateTimeKind.Utc), dataGap.End);
// Re-insert adjusted data gap at the top of the processing queue
DataGapLog.Add(dataGap);
@@ -843,7 +835,7 @@ protected virtual void OnRecoveredMeasurements(ICollection measure
{
try
{
- RecoveredMeasurements?.Invoke(this, new EventArgs>(measurements));
+ RecoveredMeasurements?.SafeInvoke(this, new EventArgs>(measurements));
}
catch (Exception ex)
{
@@ -864,14 +856,14 @@ protected virtual void OnRecoveredMeasurements(ICollection measure
/// generated. In general, there should only be a few dozen distinct event names per class. Exceeding this
/// threshold will cause the EventName to be replaced with a general warning that a usage issue has occurred.
///
- protected virtual void OnStatusMessage(MessageLevel level, string status, string eventName = null, MessageFlags flags = MessageFlags.None)
+ protected virtual void OnStatusMessage(MessageLevel level, string status, string? eventName = null, MessageFlags flags = MessageFlags.None)
{
try
{
Log.Publish(level, flags, eventName ?? "DataGapRecovery", status);
using (Logger.SuppressLogMessages())
- StatusMessage?.Invoke(this, new EventArgs(AdapterBase.GetStatusWithMessageLevelPrefix(status, level)));
+ StatusMessage?.SafeInvoke(this, new EventArgs(AdapterBase.GetStatusWithMessageLevelPrefix(status, level)));
}
catch (Exception ex)
{
@@ -892,14 +884,14 @@ protected virtual void OnStatusMessage(MessageLevel level, string status, string
/// generated. In general, there should only be a few dozen distinct event names per class. Exceeding this
/// threshold will cause the EventName to be replaced with a general warning that a usage issue has occurred.
///
- protected virtual void OnProcessException(MessageLevel level, Exception exception, string eventName = null, MessageFlags flags = MessageFlags.None)
+ protected virtual void OnProcessException(MessageLevel level, Exception exception, string? eventName = null, MessageFlags flags = MessageFlags.None)
{
try
{
- Log.Publish(level, flags, eventName ?? "DataGapRecovery", exception?.Message, null, exception);
+ Log.Publish(level, flags, eventName ?? "DataGapRecovery", exception.Message, null, exception);
using (Logger.SuppressLogMessages())
- ProcessException?.Invoke(this, new EventArgs(exception));
+ ProcessException?.SafeInvoke(this, new EventArgs(exception));
}
catch (Exception ex)
{
@@ -913,12 +905,12 @@ private string GetLoggingPath(string filePath)
return string.IsNullOrWhiteSpace(m_loggingPath) ? FilePath.GetAbsolutePath(filePath) : Path.Combine(m_loggingPath, filePath);
}
- private void TemporalSubscription_ConnectionEstablished(object sender, EventArgs e)
+ private void TemporalSubscription_ConnectionEstablished(object? sender, EventArgs e)
{
m_connected = true;
}
- private void TemporalSubscription_ConnectionTerminated(object sender, EventArgs e)
+ private void TemporalSubscription_ConnectionTerminated(object? sender, EventArgs e)
{
m_connected = false;
@@ -937,7 +929,7 @@ private void TemporalSubscription_ConnectionTerminated(object sender, EventArgs
}
}
- private void TemporalSubscription_ProcessingComplete(object sender, EventArgs e)
+ private void TemporalSubscription_ProcessingComplete(object? sender, EventArgs e)
{
OnStatusMessage(MessageLevel.Info, "Temporal data recovery processing completed.");
@@ -945,7 +937,7 @@ private void TemporalSubscription_ProcessingComplete(object sender, EventArgs> e)
+ private void TemporalSubscription_NewMeasurements(object? sender, EventArgs> e)
{
ICollection measurements = e.Argument;
int total = measurements.Count;
@@ -976,17 +968,17 @@ private void TemporalSubscription_NewMeasurements(object sender, EventArgs e)
+ private void Common_StatusMessage(object? sender, EventArgs e)
{
OnStatusMessage(MessageLevel.Info, e.Argument);
}
- private void Common_ProcessException(object sender, EventArgs e)
+ private void Common_ProcessException(object? sender, EventArgs e)
{
OnProcessException(MessageLevel.Warning, e.Argument);
}
- private void DataStreamMonitor_Elapsed(object sender, EventArgs e)
+ private void DataStreamMonitor_Elapsed(object? sender, EventArgs e)
{
if (m_measurementsRecoveredOverLastInterval == 0)
{
diff --git a/src/lib/DataPublisher.cs b/src/lib/sttp.core/DataPublisher.cs
similarity index 86%
rename from src/lib/DataPublisher.cs
rename to src/lib/sttp.core/DataPublisher.cs
index 96afed43..4c9e9a44 100644
--- a/src/lib/DataPublisher.cs
+++ b/src/lib/sttp.core/DataPublisher.cs
@@ -32,40 +32,8 @@
// Modified Header.
//
//******************************************************************************************************
-
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Configuration;
-using System.Data;
-using System.Globalization;
-using System.IO;
-using System.IO.Compression;
-using System.Linq;
-using System.Net;
-using System.Net.Security;
-using System.Net.Sockets;
-using System.Security.Cryptography.X509Certificates;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Xml;
-using GSF;
-using GSF.Communication;
-using GSF.Configuration;
-using GSF.Data;
-using GSF.Diagnostics;
-using GSF.IO;
-using GSF.Net.Security;
-using GSF.Security.Cryptography;
-using GSF.Threading;
-using GSF.TimeSeries;
-using GSF.TimeSeries.Adapters;
-using GSF.TimeSeries.Statistics;
-using GSF.TimeSeries.Transport;
-using GSF.Units;
-using TcpClient = GSF.Communication.TcpClient;
+// ReSharper disable ArrangeObjectCreationWhenTypeNotEvident
+// ReSharper disable UseUtf8StringLiteral
namespace sttp;
@@ -473,6 +441,7 @@ public enum DataPacketFlags : byte
/// Bit set = compact, bit clear = full fidelity.
///
Compact = (byte)Bits.Bit01,
+
///
/// Determines which cipher index to use when encrypting data packet.
///
@@ -480,6 +449,7 @@ public enum DataPacketFlags : byte
/// Bit set = use odd cipher index (i.e., 1), bit clear = use even cipher index (i.e., 0).
///
CipherIndex = (byte)Bits.Bit02,
+
///
/// Determines if data packet payload is compressed.
///
@@ -487,6 +457,7 @@ public enum DataPacketFlags : byte
/// Bit set = payload compressed, bit clear = payload normal.
///
Compressed = (byte)Bits.Bit03,
+
///
/// Determines which Signal Index Cache index to use when decoding a data packet.
///
@@ -494,6 +465,7 @@ public enum DataPacketFlags : byte
/// Bit set = use odd Signal Index Cache index (i.e., 1), bit clear = use even Signal Index Cache index (i.e., 0).
///
CacheIndex = (byte)Bits.Bit04,
+
///
/// No flags set.
///
@@ -522,6 +494,7 @@ public enum OperationalModes : uint
/// Version number is currently set to 2.
///
VersionMask = (uint)(Bits.Bit04 | Bits.Bit03 | Bits.Bit02 | Bits.Bit01 | Bits.Bit00),
+
///
/// Mask to get mode of compression.
///
@@ -530,6 +503,7 @@ public enum OperationalModes : uint
/// reserved for future compression modes.
///
CompressionModeMask = (uint)(Bits.Bit07 | Bits.Bit06 | Bits.Bit05),
+
///
/// Mask to get character encoding used when exchanging messages between publisher and subscriber.
///
@@ -540,6 +514,7 @@ public enum OperationalModes : uint
/// 11 = ANSI
///
EncodingMask = (uint)(Bits.Bit09 | Bits.Bit08),
+
///
/// Determines whether external measurements are exchanged during metadata synchronization.
///
@@ -547,6 +522,7 @@ public enum OperationalModes : uint
/// Bit set = external measurements are exchanged, bit clear = no external measurements are exchanged
///
ReceiveExternalMetadata = (uint)Bits.Bit25,
+
///
/// Determines whether internal measurements are exchanged during metadata synchronization.
///
@@ -554,6 +530,7 @@ public enum OperationalModes : uint
/// Bit set = internal measurements are exchanged, bit clear = no internal measurements are exchanged
///
ReceiveInternalMetadata = (uint)Bits.Bit26,
+
///
/// Determines whether payload data is compressed when exchanging between publisher and subscriber.
///
@@ -561,6 +538,7 @@ public enum OperationalModes : uint
/// Bit set = compress, bit clear = no compression
///
CompressPayloadData = (uint)Bits.Bit29,
+
///
/// Determines whether the signal index cache is compressed when exchanging between publisher and subscriber.
///
@@ -568,6 +546,7 @@ public enum OperationalModes : uint
/// Bit set = compress, bit clear = no compression
///
CompressSignalIndexCache = (uint)Bits.Bit30,
+
///
/// Determines whether metadata is compressed when exchanging between publisher and subscriber.
///
@@ -575,6 +554,7 @@ public enum OperationalModes : uint
/// Bit set = compress, bit clear = no compression
///
CompressMetadata = (uint)Bits.Bit31,
+
///
/// No flags set.
///
@@ -595,10 +575,12 @@ public enum OperationalEncoding : uint
/// UTF-16, little endian
///
UTF16LE = (uint)Bits.Nil,
+
///
/// UTF-16, big endian
///
UTF16BE = (uint)Bits.Bit08,
+
///
/// UTF-8
///
@@ -615,10 +597,12 @@ public enum CompressionModes : uint
/// GZip compression
///
GZip = (uint)Bits.Bit05,
+
///
/// TSSC compression
///
TSSC = (uint)Bits.Bit06,
+
///
/// No compression
///
@@ -645,7 +629,7 @@ public LatestMeasurementCache(string connectionString)
ConnectionString = connectionString;
}
- public override DataSet DataSource
+ public override DataSet? DataSource
{
get => base.DataSource;
set
@@ -672,7 +656,7 @@ public override string GetShortStatus(int maxLength)
private void UpdateInputMeasurementKeys()
{
- if (Settings.TryGetValue("inputMeasurementKeys", out string inputMeasurementKeys))
+ if (Settings.TryGetValue("inputMeasurementKeys", out string? inputMeasurementKeys))
InputMeasurementKeys = ParseInputMeasurementKeys(DataSource, true, inputMeasurementKeys);
}
}
@@ -687,7 +671,7 @@ private void UpdateInputMeasurementKeys()
/// is the connection identification (e.g., IP and DNS name, if available).
/// is the subscriber information as reported by the client.
///
- public event EventHandler> ClientConnected;
+ public event EventHandler>? ClientConnected;
///
/// Indicates to the host that processing for an input adapter (via temporal session) has completed.
@@ -696,7 +680,7 @@ private void UpdateInputMeasurementKeys()
/// This event is expected to only be raised when an input adapter has been designed to process
/// a finite amount of data, e.g., reading a historical range of data during temporal processing.
///
- public event EventHandler ProcessingComplete;
+ public event EventHandler? ProcessingComplete;
// Constants
@@ -755,19 +739,28 @@ private void UpdateInputMeasurementKeys()
///
public const bool DefaultValidateClientIPAddress = false;
+#if !NET
///
/// Default value for .
///
public const bool DefaultUseSimpleTcpClient = false;
+#endif
///
/// Default value for .
///
public const string DefaultMetadataTables =
+ #if NET
+ "SELECT UniqueID, OriginalSource, IsConcentrator, Acronym, Name, AccessID, ParentAcronym, CompanyAcronym, VendorAcronym, VendorDeviceName, Longitude, Latitude, InterconnectionName, ContactList, Enabled, UpdatedOn FROM DeviceDetail WHERE IsConcentrator = 0;" +
+ "SELECT DeviceAcronym, ID, SignalID, PointTag, AlternateTag, SignalReference, SignalAcronym, PhasorSourceIndex, Description, Internal, Enabled, UpdatedOn FROM MeasurementDetail;" +
+ "SELECT ID, DeviceAcronym, Label, Type, Phase, PrimaryVoltageID, SecondaryVoltageID, SourceIndex, BaseKV, UpdatedOn FROM PhasorDetail;" +
+ "SELECT TOP 1 Version AS VersionNumber FROM VersionInfo AS SchemaVersion";
+ #else
"SELECT NodeID, UniqueID, OriginalSource, IsConcentrator, Acronym, Name, AccessID, ParentAcronym, ProtocolName, FramesPerSecond, CompanyAcronym, VendorAcronym, VendorDeviceName, Longitude, Latitude, InterconnectionName, ContactList, Enabled, UpdatedOn FROM DeviceDetail WHERE IsConcentrator = 0;" +
"SELECT DeviceAcronym, ID, SignalID, PointTag, AlternateTag, SignalReference, SignalAcronym, PhasorSourceIndex, Description, Internal, Enabled, UpdatedOn FROM MeasurementDetail;" +
"SELECT ID, DeviceAcronym, Label, Type, Phase, DestinationPhasorID, SourceIndex, BaseKV, UpdatedOn FROM PhasorDetail;" +
"SELECT VersionNumber FROM SchemaVersion";
+ #endif
///
/// Default value for .
@@ -801,17 +794,21 @@ private void UpdateInputMeasurementKeys()
internal const int CipherSaltLength = 8;
// Fields
- private IServer m_serverCommandChannel;
- private IClient m_clientCommandChannel;
- private CertificatePolicyChecker m_certificateChecker;
- private Dictionary m_subscriberIdentities;
+ private IServer? m_serverCommandChannel;
+ private IClient? m_clientCommandChannel;
+ private CertificatePolicyChecker? m_certificateChecker;
+ private Dictionary? m_subscriberIdentities;
private readonly ConcurrentDictionary m_clientPublicationChannels;
private readonly Dictionary> m_clientNotifications;
+#if NET
+ private readonly Lock m_clientNotificationsLock;
+#else
private readonly object m_clientNotificationsLock;
- private SharedTimer m_cipherKeyRotationTimer;
+#endif
+ private readonly SharedTimer m_cipherKeyRotationTimer;
+ private readonly RoutingTables m_routingTables;
private long m_commandChannelConnectionAttempts;
private Guid? m_proxyClientID;
- private RoutingTables m_routingTables;
private bool m_encryptPayload;
private long m_totalMeasurementsPerSecond;
@@ -839,7 +836,7 @@ public DataPublisher()
ClientConnections = new ConcurrentDictionary();
m_clientPublicationChannels = new ConcurrentDictionary();
m_clientNotifications = new Dictionary>();
- m_clientNotificationsLock = new object();
+ m_clientNotificationsLock = new();
SecurityMode = DefaultSecurityMode;
m_encryptPayload = DefaultEncryptPayload;
SharedDatabase = DefaultSharedDatabase;
@@ -855,7 +852,7 @@ public DataPublisher()
m_routingTables = OptimizationOptions.DefaultRoutingMethod switch
{
OptimizationOptions.RoutingMethod.HighLatencyLowCpu => new RoutingTables(new RouteMappingHighLatencyLowCpu()),
- _ => new RoutingTables()
+ _ => new RoutingTables()
};
}
@@ -880,6 +877,7 @@ public DataPublisher()
[ConnectionStringParameter]
[Description("Define the security mode used for communications over the command channel.")]
[DefaultValue(DefaultSecurityMode)]
+ [Label("Security Mode")]
public SecurityMode SecurityMode { get; set; }
///
@@ -900,6 +898,7 @@ public DataPublisher()
[ConnectionStringParameter]
[Description("Define the flag that determines whether data sent over the data channel should be encrypted.")]
[DefaultValue(DefaultEncryptPayload)]
+ [Label("Encrypt Payload")]
public bool EncryptPayload
{
get => m_encryptPayload;
@@ -908,7 +907,7 @@ public bool EncryptPayload
m_encryptPayload = value;
// Start cipher key rotation timer when encrypting payload
- if (m_cipherKeyRotationTimer is not null)
+ if (!m_disposed)
m_cipherKeyRotationTimer.Enabled = value;
}
}
@@ -920,6 +919,7 @@ public bool EncryptPayload
[ConnectionStringParameter]
[Description("Define the flag that indicates whether this publisher is publishing data that this node subscribed to from another node in a shared database.")]
[DefaultValue(DefaultSharedDatabase)]
+ [Label("Shared Database")]
public bool SharedDatabase { get; set; }
///
@@ -928,6 +928,7 @@ public bool EncryptPayload
[ConnectionStringParameter]
[Description("Define the flag that indicates if this publisher will allow payload compression when requested by subscribers.")]
[DefaultValue(DefaultAllowPayloadCompression)]
+ [Label("Allow Payload Compression")]
public bool AllowPayloadCompression { get; set; }
///
@@ -936,6 +937,7 @@ public bool EncryptPayload
[ConnectionStringParameter]
[Description("Define the flag that indicates if this publisher will allow metadata refresh commands when requested by subscribers.")]
[DefaultValue(DefaultAllowMetadataRefresh)]
+ [Label("Allow Metadata Refresh")]
public bool AllowMetadataRefresh { get; set; }
///
@@ -944,6 +946,7 @@ public bool EncryptPayload
[ConnectionStringParameter]
[Description("Define the flag that indicates if this publisher will allow filtering of data which is not a number.")]
[DefaultValue(DefaultAllowNaNValueFilter)]
+ [Label("Allow NaN Value Filter")]
public bool AllowNaNValueFilter { get; set; }
///
@@ -952,6 +955,7 @@ public bool EncryptPayload
[ConnectionStringParameter]
[Description("Define the flag that indicates if this publisher will force filtering of data which is not a number.")]
[DefaultValue(DefaultForceNaNValueFilter)]
+ [Label("Force NaN Value Filter")]
public bool ForceNaNValueFilter { get; set; }
///
@@ -960,6 +964,7 @@ public bool EncryptPayload
[ConnectionStringParameter]
[Description("Define the flag that determines whether to use base time offsets to decrease the size of compact measurements.")]
[DefaultValue(DefaultUseBaseTimeOffsets)]
+ [Label("Force NaN Value Filter")]
public bool UseBaseTimeOffsets { get; set; }
///
@@ -968,11 +973,16 @@ public bool EncryptPayload
[ConnectionStringParameter]
[Description("Defines the maximum packet size to use for data publications. This number should be set as small as possible to reduce fragmentation, but large enough to keep large data flows from falling behind.")]
[DefaultValue(DefaultMaxPacketSize)]
+ [Label("Max Packet Size")]
public int MaxPacketSize { get; set; } = DefaultMaxPacketSize;
+ ///
+ /// Get or sets the size of the buffer used for sending and receiving data from clients.
+ ///
[ConnectionStringParameter]
- [Description("The size of the buffer used for sending and receiving data from clients.")]
+ [Description("Define the size of the buffer used for sending and receiving data from clients.")]
[DefaultValue(DefaultBufferSize)]
+ [Label("Buffer Size")]
public int BufferSize { get; set; } = DefaultBufferSize;
///
@@ -981,24 +991,16 @@ public bool EncryptPayload
[ConnectionStringParameter]
[Description("Define the period, in milliseconds, over which new cipher keys will be provided to subscribers when EncryptPayload is true.")]
[DefaultValue(DefaultCipherKeyRotationPeriod)]
+ [Label("Cipher Key Rotation Period")]
public double CipherKeyRotationPeriod
{
- get
- {
- if (m_cipherKeyRotationTimer is not null)
- return m_cipherKeyRotationTimer.Interval;
-
- return double.NaN;
- }
+ get => m_cipherKeyRotationTimer.Interval;
set
{
if (value < 1000.0D)
throw new ArgumentOutOfRangeException(nameof(value), "Cipher key rotation period should not be set to less than 1000 milliseconds.");
- if (m_cipherKeyRotationTimer is not null)
- m_cipherKeyRotationTimer.Interval = (int)value;
- else
- throw new ArgumentException("Cannot assign new cipher rotation period, timer is not defined.");
+ m_cipherKeyRotationTimer.Interval = (int)value;
}
}
@@ -1008,9 +1010,12 @@ public double CipherKeyRotationPeriod
///
[ConnectionStringParameter]
[Description("Defines the set of measurements to be cached and sent to subscribers immediately upon subscription.")]
+#if !NET
[CustomConfigurationEditor("GSF.TimeSeries.UI.WPF.dll", "GSF.TimeSeries.UI.Editors.MeasurementEditor")]
+#endif
[DefaultValue("")]
- public string CachedMeasurementExpression { get; set; }
+ [Label("Cached Measurement Expression")]
+ public string CachedMeasurementExpression { get; set; } = "";
///
/// Gets or sets the measurement reporting interval.
@@ -1021,6 +1026,7 @@ public double CipherKeyRotationPeriod
[ConnectionStringParameter]
[Description("Defines the measurement reporting interval used to determined how many measurements should be processed, per subscriber, before reporting status.")]
[DefaultValue(AdapterBase.DefaultMeasurementReportingInterval)]
+ [Label("Measurement Reporting Interval")]
public int MeasurementReportingInterval { get; set; }
///
@@ -1029,6 +1035,7 @@ public double CipherKeyRotationPeriod
[ConnectionStringParameter]
[Description("Defines the maximum publication interval in milliseconds for data publications. Set to zero for no defined maximum.")]
[DefaultValue(DefaultMaxPublishInterval)]
+ [Label("Max Publish Interval")]
public long MaxPublishInterval { get; set; } = DefaultMaxPublishInterval;
///
@@ -1037,6 +1044,7 @@ public double CipherKeyRotationPeriod
[ConnectionStringParameter]
[Description("Define the flag that determines whether measurement rights validation is enforced. Defaults to true for TLS connections.")]
[DefaultValue(DefaultValidateMeasurementRights)]
+ [Label("Validate Measurement Rights")]
public bool ValidateMeasurementRights { get; set; }
///
@@ -1045,8 +1053,10 @@ public double CipherKeyRotationPeriod
[ConnectionStringParameter]
[Description("Define the flag that determines whether client subscriber IP address is validated. Defaults to true when measurement rights are validated.")]
[DefaultValue(DefaultValidateClientIPAddress)]
+ [Label("Validate Client IP Address")]
public bool ValidateClientIPAddress { get; set; }
+#if !NET
///
/// Gets or sets flag that determines if a should be used for reverse connections.
///
@@ -1054,12 +1064,13 @@ public double CipherKeyRotationPeriod
[DefaultValue(DefaultUseSimpleTcpClient)]
[Description("Define the flag that determines whether a simple TCP client should be used for reverse connections.")]
public bool UseSimpleTcpClient { get; set; } = DefaultUseSimpleTcpClient;
+#endif
///
/// Gets or sets based data source used to load each .
/// Updates to this property will cascade to all items in this .
///
- public override DataSet DataSource
+ public override DataSet? DataSource
{
get => base.DataSource;
set
@@ -1092,10 +1103,13 @@ public override string Status
status.Append(m_serverCommandChannel?.Status);
status.Append(m_clientCommandChannel?.Status);
+ #if !NET
if (m_clientCommandChannel is not null)
status.AppendLine($" Using simple TCP client: {UseSimpleTcpClient}");
+ #endif
status.Append(base.Status);
+
status.AppendLine($" Mutual Subscription: {MutualSubscription}");
status.AppendLine($" Reporting interval: {MeasurementReportingInterval:N0} per subscriber");
status.AppendLine($" Buffer block retransmits: {BufferBlockRetransmissions:N0}");
@@ -1119,8 +1133,10 @@ public override string Name
// Only server command channel settings are persisted to config file
base.Name = value.ToUpper();
+ #if !NET
if (m_serverCommandChannel is IPersistSettings commandChannel)
commandChannel.SettingsCategory = value.Replace("!", "").ToLower();
+ #endif
}
}
@@ -1130,6 +1146,7 @@ public override string Name
[ConnectionStringParameter]
[Description("Semi-colon separated list of SQL select statements used to create data for meta-data exchange.")]
[DefaultValue(DefaultMetadataTables)]
+ [Label("Metadata Tables")]
public string MetadataTables { get; set; }
///
@@ -1153,6 +1170,7 @@ public override string Name
[ConnectionStringParameter]
[Description("Gets or sets flag that determines if a subscription is mutual, i.e., bi-directional pub/sub.")]
[DefaultValue(DefaultMutualSubscription)]
+ [Label("Mutual Subscription")]
public bool MutualSubscription { get; set; }
///
@@ -1164,12 +1182,12 @@ public override string Name
///
/// Gets dictionary of connected clients.
///
- protected internal ConcurrentDictionary ClientConnections { get; private set; }
+ protected internal ConcurrentDictionary ClientConnections { get; }
///
/// Gets or sets reference to command channel, attaching and/or detaching to events as needed.
///
- protected IServer ServerCommandChannel
+ protected IServer? ServerCommandChannel
{
get => m_serverCommandChannel;
set
@@ -1214,7 +1232,7 @@ protected IServer ServerCommandChannel
///
/// This handles reverse connectivity operations.
///
- protected IClient ClientCommandChannel
+ protected IClient? ClientCommandChannel
{
get => m_clientCommandChannel;
set
@@ -1321,30 +1339,21 @@ protected override void Dispose(bool disposing)
ServerCommandChannel = null;
- ClientConnections?.Values.AsParallel().ForAll(cc => cc.Dispose());
+ ClientConnections.Values.AsParallel().ForAll(cc => cc.Dispose());
+ ClientConnections.Clear();
- ClientConnections = null;
-
- if (m_routingTables is not null)
- {
- m_routingTables.StatusMessage -= RoutingTables_StatusMessage;
- m_routingTables.ProcessException -= RoutingTables_ProcessException;
- m_routingTables.Dispose();
- }
- m_routingTables = null;
+ m_routingTables.StatusMessage -= RoutingTables_StatusMessage;
+ m_routingTables.ProcessException -= RoutingTables_ProcessException;
+ m_routingTables.Dispose();
// Dispose the cipher key rotation timer
- if (m_cipherKeyRotationTimer is not null)
- {
- m_cipherKeyRotationTimer.Elapsed -= CipherKeyRotationTimer_Elapsed;
- m_cipherKeyRotationTimer.Dispose();
- }
- m_cipherKeyRotationTimer = null;
+ m_cipherKeyRotationTimer.Elapsed -= CipherKeyRotationTimer_Elapsed;
+ m_cipherKeyRotationTimer.Dispose();
}
finally
{
- m_disposed = true; // Prevent duplicate dispose.
- base.Dispose(disposing); // Call base class Dispose().
+ m_disposed = true; // Prevent duplicate dispose.
+ base.Dispose(disposing); // Call base class Dispose().
}
}
@@ -1362,7 +1371,7 @@ public override void Initialize()
Dictionary settings = Settings;
// Check flag that will determine if subscriber payloads should be encrypted by default
- if (settings.TryGetValue(nameof(EncryptPayload), out string setting))
+ if (settings.TryGetValue(nameof(EncryptPayload), out string? setting))
m_encryptPayload = setting.ParseBoolean();
// Check flag that indicates whether publisher is publishing data
@@ -1463,11 +1472,9 @@ public override void Initialize()
bool clientBasedConnection = false;
// Attempt to retrieve any defined command channel settings
- Dictionary commandChannelSettings = settings.TryGetValue("commandChannel", out string commandChannelConnectionString) ?
- commandChannelConnectionString.ParseKeyValuePairs() :
- settings;
+ Dictionary commandChannelSettings = settings.TryGetValue("commandChannel", out string? commandChannelConnectionString) ? commandChannelConnectionString.ParseKeyValuePairs() : settings;
- if (commandChannelSettings.TryGetValue("server", out string server))
+ if (commandChannelSettings.TryGetValue("server", out string? server))
clientBasedConnection = !string.IsNullOrWhiteSpace(server);
if (!commandChannelSettings.TryGetValue("bufferSize", out setting) || !int.TryParse(setting, out bufferSize))
@@ -1476,8 +1483,10 @@ public override void Initialize()
if (bufferSize == 0)
bufferSize = BufferSize;
+ #if !NET
if (settings.TryGetValue(nameof(UseSimpleTcpClient), out setting))
UseSimpleTcpClient = setting.ParseBoolean();
+ #endif
if (SecurityMode == SecurityMode.TLS)
{
@@ -1490,7 +1499,7 @@ public override void Initialize()
{
bool checkCertificateRevocation;
- if (!commandChannelSettings.TryGetValue("localCertificate", out string localCertificate) || !File.Exists(localCertificate))
+ if (!commandChannelSettings.TryGetValue("localCertificate", out string? localCertificate) || !File.Exists(localCertificate))
localCertificate = DataSubscriber.GetLocalCertificate();
if (commandChannelSettings.TryGetValue("remoteCertificate", out setting))
@@ -1513,14 +1522,16 @@ public override void Initialize()
PayloadAware = true,
PayloadMarker = null,
PayloadEndianOrder = EndianOrder.BigEndian,
- PersistSettings = false,
MaxConnectionAttempts = 1,
- CertificateFile = FilePath.GetAbsolutePath(localCertificate),
+ CertificateFile = FilePath.GetAbsolutePath(localCertificate!),
CheckCertificateRevocation = checkCertificateRevocation,
CertificateChecker = m_certificateChecker,
SendBufferSize = BufferSize,
ReceiveBufferSize = bufferSize,
- NoDelay = true
+ NoDelay = true,
+ #if !NET
+ PersistSettings = false,
+ #endif
};
// Assign command channel client reference and attach to needed events
@@ -1531,17 +1542,19 @@ public override void Initialize()
// Create a new TLS server
TlsServer commandChannel = new()
{
- SettingsCategory = Name.Replace("!", "").ToLower(),
ConfigurationString = "port=7167",
PayloadAware = true,
PayloadMarker = null,
PayloadEndianOrder = EndianOrder.BigEndian,
RequireClientCertificate = true,
CertificateChecker = m_certificateChecker,
- PersistSettings = true,
SendBufferSize = BufferSize,
ReceiveBufferSize = BufferSize,
- NoDelay = true
+ NoDelay = true,
+ #if !NET
+ PersistSettings = true,
+ SettingsCategory = Name.Replace("!", "").ToLower(),
+ #endif
};
// Assign command channel server reference and attach to needed events
@@ -1552,6 +1565,7 @@ public override void Initialize()
{
if (clientBasedConnection)
{
+ #if !NET
if (UseSimpleTcpClient)
{
// Create a new simple TCP client
@@ -1572,37 +1586,44 @@ public override void Initialize()
}
else
{
- // Create a new TCP client
- TcpClient commandChannel = new()
- {
- PayloadAware = true,
- PayloadMarker = null,
- PayloadEndianOrder = EndianOrder.BigEndian,
- PersistSettings = false,
- MaxConnectionAttempts = -1,
- SendBufferSize = BufferSize,
- ReceiveBufferSize = bufferSize,
- NoDelay = true
- };
+ #endif
+ // Create a new TCP client
+ TcpClient commandChannel = new()
+ {
+ PayloadAware = true,
+ PayloadMarker = null,
+ PayloadEndianOrder = EndianOrder.BigEndian,
+ MaxConnectionAttempts = -1,
+ SendBufferSize = BufferSize,
+ ReceiveBufferSize = bufferSize,
+ NoDelay = true,
+ #if !NET
+ PersistSettings = false,
+ #endif
+ };
- // Assign command channel client reference and attach to needed events
- ClientCommandChannel = commandChannel;
+ // Assign command channel client reference and attach to needed events
+ ClientCommandChannel = commandChannel;
+ #if !NET
}
+ #endif
}
else
{
// Create a new TCP server
TcpServer commandChannel = new()
{
- SettingsCategory = Name.Replace("!", "").ToLower(),
ConfigurationString = "port=7165",
PayloadAware = true,
PayloadMarker = null,
PayloadEndianOrder = EndianOrder.BigEndian,
- PersistSettings = true,
SendBufferSize = BufferSize,
ReceiveBufferSize = BufferSize,
- NoDelay = true
+ NoDelay = true,
+ #if !NET
+ PersistSettings = true,
+ SettingsCategory = Name.Replace("!", "").ToLower(),
+ #endif
};
// Assign command channel server reference and attach to needed events
@@ -1613,12 +1634,12 @@ public override void Initialize()
if (clientBasedConnection)
{
// Set client-based connection string
- m_clientCommandChannel.ConnectionString = commandChannelConnectionString ?? ConnectionString;
+ m_clientCommandChannel!.ConnectionString = commandChannelConnectionString ?? ConnectionString;
}
else
{
// Initialize TCP server - this will load persisted settings
- m_serverCommandChannel.Initialize();
+ m_serverCommandChannel!.Initialize();
// Allow user to override persisted server settings by specifying a command channel setting
if (!string.IsNullOrWhiteSpace(commandChannelConnectionString))
@@ -1627,7 +1648,7 @@ public override void Initialize()
// Start cipher key rotation timer when encrypting payload
if (m_encryptPayload)
- m_cipherKeyRotationTimer?.Start();
+ m_cipherKeyRotationTimer.Start();
// Register publisher with the statistics engine
StatisticsEngine.Register(this, "Publisher", "PUB");
@@ -1645,7 +1666,7 @@ RoutingPassthroughMethod IOptimizedRoutingConsumer.GetRoutingPassthroughMethods(
/// Queues a collection of measurements for processing to each connected to this .
///
/// Measurements to queue for processing.
- public override void QueueMeasurementsForProcessing(IEnumerable measurements)
+ public override void QueueMeasurementsForProcessing(IEnumerable? measurements)
{
if (measurements is null)
return;
@@ -1666,7 +1687,7 @@ public override void QueueMeasurementsForProcessing(IEnumerable me
/// Queues a collection of measurements for processing to each connected to this .
///
/// Measurements to queue for processing.
- private void QueueMeasurementsForProcessing(List measurements)
+ private void QueueMeasurementsForProcessing(List? measurements)
{
if (measurements is null || measurements.Count == 0)
return;
@@ -1725,33 +1746,37 @@ public override string GetShortStatus(int maxLength)
///
/// Enumerates connected clients.
///
- [AdapterCommand("Enumerates connected clients.", "Administrator", "Editor", "Viewer")]
- public virtual void EnumerateClients()
+ [AdapterCommand("Enumerates connected clients.")]
+ [Label("Enumerate Clients")]
+ public virtual string EnumerateClients()
{
- OnStatusMessage(MessageLevel.Info, EnumerateClients(false));
+ string result = EnumerateClients(false);
+ OnStatusMessage(MessageLevel.Info, result);
+ return result;
}
///
/// Enumerates connected clients with active temporal sessions.
///
- [AdapterCommand("Enumerates connected clients with active temporal sessions.", "Administrator", "Editor", "Viewer")]
- public virtual void EnumerateTemporalClients()
+ [AdapterCommand("Enumerates connected clients with active temporal sessions.")]
+ [Label("Enumerate Temporal Clients")]
+ public virtual string EnumerateTemporalClients()
{
- OnStatusMessage(MessageLevel.Info, EnumerateClients(true));
+ string result = EnumerateClients(true);
+ OnStatusMessage(MessageLevel.Info, result);
+ return result;
}
private string EnumerateClients(bool filterToTemporalSessions)
{
StringBuilder clientEnumeration = new();
- Guid[] clientIDs = (Guid[])m_serverCommandChannel?.ClientIDs.Clone() ?? [m_proxyClientID.GetValueOrDefault()];
+ Guid[] clientIDs = (Guid[]?)m_serverCommandChannel?.ClientIDs.Clone() ?? [m_proxyClientID.GetValueOrDefault()];
- clientEnumeration.AppendLine(filterToTemporalSessions ?
- $"{Environment.NewLine}Indices for connected clients with active temporal sessions:{Environment.NewLine}" :
- $"{Environment.NewLine}Indices for {clientIDs.Length:N0} connected clients:{Environment.NewLine}");
+ clientEnumeration.AppendLine(filterToTemporalSessions ? $"{Environment.NewLine}Indices for connected clients with active temporal sessions:{Environment.NewLine}" : $"{Environment.NewLine}Indices for {clientIDs.Length:N0} connected clients:{Environment.NewLine}");
for (int i = 0; i < clientIDs.Length; i++)
{
- if (!ClientConnections.TryGetValue(clientIDs[i], out SubscriberConnection connection) || connection?.Subscription is null)
+ if (!ClientConnections.TryGetValue(clientIDs[i], out SubscriberConnection? connection) || connection.Subscription is null)
continue;
bool hasActiveTemporalSession = connection.Subscription.TemporalConstraintIsDefined();
@@ -1773,7 +1798,9 @@ private string EnumerateClients(bool filterToTemporalSessions)
/// Rotates cipher keys for specified client connection.
///
/// Enumerated index for client connection.
- [AdapterCommand("Rotates cipher keys for client connection using its enumerated index.", "Administrator")]
+ [AdapterCommand("Rotates cipher keys for client connection using its enumerated index.")]
+ [Label("Rotate Cipher Keys")]
+ [Parameter(nameof(clientIndex), "Client Index", "Enumerated index for client connection.")]
public virtual void RotateCipherKeys(int clientIndex)
{
Guid clientID = Guid.Empty;
@@ -1791,8 +1818,8 @@ public virtual void RotateCipherKeys(int clientIndex)
if (!success)
return;
-
- if (ClientConnections.TryGetValue(clientID, out SubscriberConnection connection))
+
+ if (ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection))
connection.RotateCipherKeys();
else
OnStatusMessage(MessageLevel.Error, $"Failed to find connected client {clientID}");
@@ -1802,7 +1829,8 @@ public virtual void RotateCipherKeys(int clientIndex)
/// Gets subscriber information for specified client connection.
///
/// Enumerated index for client connection.
- [AdapterCommand("Gets subscriber information for client connection using its enumerated index.", "Administrator", "Editor", "Viewer")]
+ [AdapterCommand("Gets subscriber information for client connection using its enumerated index.")]
+ [Label("Get Subscriber Info")]
public virtual string GetSubscriberInfo(int clientIndex)
{
Guid clientID = Guid.Empty;
@@ -1820,7 +1848,7 @@ public virtual string GetSubscriberInfo(int clientIndex)
if (success)
{
- if (ClientConnections.TryGetValue(clientID, out SubscriberConnection connection))
+ if (ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection))
return connection.SubscriberInfo;
OnStatusMessage(MessageLevel.Error, $"Failed to find connected client {clientID}");
@@ -1833,7 +1861,9 @@ public virtual string GetSubscriberInfo(int clientIndex)
/// Gets temporal status for a specified client connection.
///
/// Enumerated index for client connection.
- [AdapterCommand("Gets temporal status for a subscriber, if any, using its enumerated index.", "Administrator", "Editor", "Viewer")]
+ [AdapterCommand("Gets temporal status for a subscriber, if any, using its enumerated index.")]
+ [Label("Get Temporal Status")]
+ [Parameter(nameof(clientIndex), "Client Index", "Enumerated index for client connection.")]
public virtual string GetTemporalStatus(int clientIndex)
{
Guid clientID = Guid.Empty;
@@ -1851,15 +1881,13 @@ public virtual string GetTemporalStatus(int clientIndex)
if (success)
{
- if (ClientConnections.TryGetValue(clientID, out SubscriberConnection connection))
+ if (ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection))
{
- string temporalStatus = null;
+ string? temporalStatus = null;
if (connection.Subscription is not null)
{
- temporalStatus = connection.Subscription.TemporalConstraintIsDefined() ?
- connection.Subscription.TemporalSessionStatus :
- "Subscription does not have an active temporal session.";
+ temporalStatus = connection.Subscription.TemporalConstraintIsDefined() ? connection.Subscription.TemporalSessionStatus : "Subscription does not have an active temporal session.";
}
if (string.IsNullOrWhiteSpace(temporalStatus))
@@ -1878,10 +1906,11 @@ public virtual string GetTemporalStatus(int clientIndex)
/// Gets the local certificate currently in use by the data publisher.
///
/// The local certificate file read directly from the certificate file as an array of bytes.
- [AdapterCommand("Gets the local certificate currently in use by the data publisher.", "Administrator", "Editor")]
+ [AdapterCommand("Gets the local certificate currently in use by the data publisher.")]
+ [Label("Get Local Certificate")]
public virtual byte[] GetLocalCertificate()
{
- if (m_serverCommandChannel is not TlsServer commandChannel)
+ if (m_serverCommandChannel is not TlsServer commandChannel || string.IsNullOrWhiteSpace(commandChannel.CertificateFile))
throw new InvalidOperationException("Certificates can only be exported in TLS security mode with a server-based command channel.");
return File.ReadAllBytes(FilePath.GetAbsolutePath(commandChannel.CertificateFile));
@@ -1893,7 +1922,10 @@ public virtual byte[] GetLocalCertificate()
/// The file name to give to the certificate when imported.
/// The data to be written to the certificate file.
/// The local path on the server where the file was written.
- [AdapterCommand("Imports a certificate to the trusted certificates path.", "Administrator", "Editor")]
+ [AdapterCommand("Imports a certificate to the trusted certificates path.")]
+ [Label("Import Certificate")]
+ [Parameter(nameof(fileName), "File Name", "The file name to give to the certificate when imported.")]
+ [Parameter(nameof(certificateData), "Certificate Data", "The data to be written to the certificate file.")]
public virtual string ImportCertificate(string fileName, byte[] certificateData)
{
if (m_serverCommandChannel is not TlsServer commandChannel)
@@ -1915,18 +1947,21 @@ public virtual string ImportCertificate(string fileName, byte[] certificateData)
/// Gets subscriber status for specified subscriber ID.
///
/// Guid based subscriber ID for client connection.
- [AdapterCommand("Gets subscriber status for client connection using its subscriber ID.", "Administrator", "Editor", "Viewer")]
+ [AdapterCommand("Gets subscriber status for client connection using its subscriber ID.")]
+ [Label("Get Subscriber Status")]
+ [Parameter(nameof(subscriberID), "Subscriber ID", "Guid based subscriber ID for client connection.")]
public virtual Tuple GetSubscriberStatus(Guid subscriberID)
{
- return new Tuple(subscriberID,
- GetConnectionProperty(subscriberID, sc => sc.IsConnected),
- GetConnectionProperty(subscriberID, sc => sc.SubscriberInfo));
+ return new Tuple(subscriberID,
+ GetConnectionProperty(subscriberID, sc => sc.IsConnected),
+ GetConnectionProperty(subscriberID, sc => sc.SubscriberInfo) ?? "undefined");
}
///
/// Resets the counters for the lifetime statistics without interrupting the adapter's operations.
///
- [AdapterCommand("Resets the counters for the lifetime statistics without interrupting the adapter's operations.", "Administrator", "Editor")]
+ [AdapterCommand("Resets the counters for the lifetime statistics without interrupting the adapter's operations.")]
+ [Label("Reset Lifetime Counters")]
public virtual void ResetLifetimeCounters()
{
LifetimeMeasurements = 0L;
@@ -1941,7 +1976,9 @@ public virtual void ResetLifetimeCounters()
/// Sends a notification to all subscribers.
///
/// The message to be sent.
- [AdapterCommand("Sends a notification to all subscribers.", "Administrator", "Editor")]
+ [AdapterCommand("Sends a notification to all subscribers.")]
+ [Label("Send Notification")]
+ [Parameter(nameof(message), "Message", "The message to be sent.")]
public virtual void SendNotification(string message)
{
string notification = $"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss.fff}] {message}";
@@ -1964,7 +2001,7 @@ public virtual void SendNotification(string message)
/// Client ID of connection over which to update signal index cache.
/// New signal index cache.
/// Subscribed measurement keys.
- public Guid[] UpdateSignalIndexCache(Guid clientID, SignalIndexCache signalIndexCache, MeasurementKey[] inputMeasurementKeys)
+ public Guid[] UpdateSignalIndexCache(Guid clientID, SignalIndexCache? signalIndexCache, MeasurementKey[]? inputMeasurementKeys)
{
ConcurrentDictionary reference = new();
List unauthorizedKeys = [];
@@ -1972,9 +2009,7 @@ public Guid[] UpdateSignalIndexCache(Guid clientID, SignalIndexCache signalIndex
if (inputMeasurementKeys is not null)
{
- Func hasRightsFunc = ValidateMeasurementRights ?
- new SubscriberRightsLookup(DataSource, signalIndexCache.SubscriberID).HasRightsFunc :
- _ => true;
+ Func hasRightsFunc = ValidateMeasurementRights ? new SubscriberRightsLookup(DataSource, signalIndexCache?.SubscriberID ?? Guid.Empty).HasRightsFunc : _ => true;
// We will now go through the client's requested keys and see which ones are authorized for subscription,
// this information will be available through the returned signal index cache which will also define
@@ -1992,7 +2027,7 @@ public Guid[] UpdateSignalIndexCache(Guid clientID, SignalIndexCache signalIndex
}
// Send client updated signal index cache
- if (ClientConnections.TryGetValue(clientID, out SubscriberConnection connection))
+ if (ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection))
{
if (connection.Version > 1)
{
@@ -2002,7 +2037,7 @@ public Guid[] UpdateSignalIndexCache(Guid clientID, SignalIndexCache signalIndex
UnauthorizedSignalIDs = unauthorizedKeys.ToArray()
};
- byte[] serializedSignalIndexCache = SerializeSignalIndexCache(clientID, nextSignalIndexCache);
+ byte[]? serializedSignalIndexCache = SerializeSignalIndexCache(clientID, nextSignalIndexCache);
if (serializedSignalIndexCache is null || serializedSignalIndexCache.Length == 0)
{
@@ -2013,7 +2048,7 @@ public Guid[] UpdateSignalIndexCache(Guid clientID, SignalIndexCache signalIndex
lock (connection.CacheUpdateLock)
{
// Update primary signal cache index at startup
- if (connection.SignalIndexCache.RefreshCount == 0)
+ if (connection.SignalIndexCache is not null && connection.SignalIndexCache.RefreshCount == 0)
{
connection.SignalIndexCache.Reference = reference;
connection.SignalIndexCache.UnauthorizedSignalIDs = unauthorizedKeys.ToArray();
@@ -2041,15 +2076,17 @@ public Guid[] UpdateSignalIndexCache(Guid clientID, SignalIndexCache signalIndex
}
}
}
- else
+ else if (signalIndexCache is not null)
{
signalIndexCache.Reference = reference;
signalIndexCache.UnauthorizedSignalIDs = unauthorizedKeys.ToArray();
-
+
if (connection.IsSubscribed)
{
- byte[] serializedSignalIndexCache = SerializeSignalIndexCache(clientID, signalIndexCache);
- SendClientResponse(clientID, ServerResponse.UpdateSignalIndexCache, ServerCommand.Subscribe, serializedSignalIndexCache);
+ byte[]? serializedSignalIndexCache = SerializeSignalIndexCache(clientID, signalIndexCache);
+
+ if (serializedSignalIndexCache is not null)
+ SendClientResponse(clientID, ServerResponse.UpdateSignalIndexCache, ServerCommand.Subscribe, serializedSignalIndexCache);
}
}
}
@@ -2065,13 +2102,13 @@ protected void UpdateLatestMeasurementCache()
{
try
{
- if (!Settings.TryGetValue(nameof(CachedMeasurementExpression), out string cachedMeasurementExpression))
+ if (!Settings.TryGetValue(nameof(CachedMeasurementExpression), out string? cachedMeasurementExpression))
return;
- if (!TryGetAdapterByName(nameof(LatestMeasurementCache), out IActionAdapter cache))
+ if (!TryGetAdapterByName(nameof(LatestMeasurementCache), out IActionAdapter? cache))
return;
- cache.InputMeasurementKeys = AdapterBase.ParseInputMeasurementKeys(DataSource, true, cachedMeasurementExpression);
+ cache!.InputMeasurementKeys = AdapterBase.ParseInputMeasurementKeys(DataSource, true, cachedMeasurementExpression);
m_routingTables.CalculateRoutingTables(null);
}
catch (Exception ex)
@@ -2100,9 +2137,9 @@ private void UpdateClientNotifications()
{
m_clientNotifications.Clear();
- if (DataSource.Tables.Contains("Subscribers"))
+ if (DataSource?.Tables.Contains("Subscribers") ?? false)
{
- foreach (DataRow row in DataSource.Tables["Subscribers"].Rows)
+ foreach (DataRow row in DataSource.Tables["Subscribers"]!.Rows)
{
if (Guid.TryParse(row["ID"].ToNonNullString(), out Guid subscriberID))
m_clientNotifications.Add(subscriberID, new Dictionary());
@@ -2160,17 +2197,17 @@ private void DeserializeClientNotifications()
using FileStream fileStream = File.OpenRead(notificationsFileName);
using TextReader reader = new StreamReader(fileStream);
- string line = reader.ReadLine();
+ string? line = reader.ReadLine();
while (line is not null)
{
int separatorIndex = line.IndexOf(',');
- if (Guid.TryParse(line.Substring(0, separatorIndex), out Guid subscriberID))
+ if (Guid.TryParse(line[..separatorIndex], out Guid subscriberID))
{
- if (m_clientNotifications.TryGetValue(subscriberID, out Dictionary notifications))
+ if (m_clientNotifications.TryGetValue(subscriberID, out Dictionary? notifications) /* && notifications is not null */)
{
- string notification = line.Substring(separatorIndex + 1);
+ string notification = line[(separatorIndex + 1)..];
notifications.Add(notification.GetHashCode(), notification);
}
}
@@ -2189,7 +2226,7 @@ private void SendNotifications(SubscriberConnection connection)
{
using BlockAllocatedMemoryStream buffer = new();
- if (!m_clientNotifications.TryGetValue(connection.SubscriberID, out Dictionary notifications))
+ if (!m_clientNotifications.TryGetValue(connection.SubscriberID, out Dictionary? notifications) /* || notifications is null */)
return;
foreach (KeyValuePair pair in notifications)
@@ -2211,12 +2248,7 @@ private void SendNotifications(SubscriberConnection connection)
/// Text encoding associated with a particular client.
protected internal Encoding GetClientEncoding(Guid clientID)
{
- if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection connection))
- return Encoding.UTF8;
-
- Encoding clientEncoding = connection.Encoding;
-
- return clientEncoding ?? Encoding.UTF8;
+ return !ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection) ? Encoding.UTF8 : connection.Encoding;
}
///
@@ -2228,7 +2260,7 @@ protected internal virtual bool SendDataStartTime(Guid clientID, Ticks startTime
{
bool result = SendClientResponse(clientID, ServerResponse.DataStartTime, ServerCommand.Subscribe, BigEndian.GetBytes((long)startTime));
- if (ClientConnections.TryGetValue(clientID, out SubscriberConnection connection))
+ if (ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection) /* && connection is not null */)
OnStatusMessage(MessageLevel.Info, $"Start time sent to {connection.ConnectionID}.");
return result;
@@ -2245,7 +2277,7 @@ protected internal virtual bool SendDataStartTime(Guid clientID, Ticks startTime
/// true if send was successful; otherwise false.
protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse response, ServerCommand command)
{
- return SendClientResponse(clientID, response, command, (byte[])null);
+ return SendClientResponse(clientID, response, command, (byte[]?)null);
}
///
@@ -2256,11 +2288,9 @@ protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse
/// In response to command.
/// Status message to return.
/// true if send was successful; otherwise false.
- protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse response, ServerCommand command, string status)
+ protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse response, ServerCommand command, string? status)
{
- return status is null ?
- SendClientResponse(clientID, response, command) :
- SendClientResponse(clientID, response, command, GetClientEncoding(clientID).GetBytes(status));
+ return status is null ? SendClientResponse(clientID, response, command) : SendClientResponse(clientID, response, command, GetClientEncoding(clientID).GetBytes(status));
}
///
@@ -2274,9 +2304,7 @@ protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse
/// true if send was successful; otherwise false.
protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse response, ServerCommand command, string formattedStatus, params object[] args)
{
- return string.IsNullOrWhiteSpace(formattedStatus) ?
- SendClientResponse(clientID, response, command) :
- SendClientResponse(clientID, response, command, GetClientEncoding(clientID).GetBytes(string.Format(formattedStatus, args)));
+ return string.IsNullOrWhiteSpace(formattedStatus) ? SendClientResponse(clientID, response, command) : SendClientResponse(clientID, response, command, GetClientEncoding(clientID).GetBytes(string.Format(formattedStatus, args)));
}
///
@@ -2287,7 +2315,7 @@ protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse
/// In response to command.
/// Data to return to client; null if none.
/// true if send was successful; otherwise false.
- protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse response, ServerCommand command, byte[] data)
+ protected internal virtual bool SendClientResponse(Guid clientID, ServerResponse response, ServerCommand command, byte[]? data)
{
return SendClientResponse(clientID, (byte)response, (byte)command, data);
}
@@ -2318,7 +2346,7 @@ protected internal virtual void UpdateLatencyStatistics(IEnumerable latenc
// Attempts to get the subscriber for the given client based on that client's X.509 certificate.
private void TryFindClientDetails(SubscriberConnection connection)
{
- X509Certificate remoteCertificate;
+ X509Certificate? remoteCertificate;
// If connection is not TLS, there is no X.509 certificate
if (m_serverCommandChannel is not TlsServer serverCommandChannel)
@@ -2327,22 +2355,22 @@ private void TryFindClientDetails(SubscriberConnection connection)
return;
// Get remote certificate and corresponding trusted certificate
- remoteCertificate = clientCommandChannel.SslStream.RemoteCertificate;
+ remoteCertificate = clientCommandChannel.SslStream?.RemoteCertificate;
}
else
{
// If connection is not found, cannot get X.509 certificate
- if (!serverCommandChannel.TryGetClient(connection.ClientID, out TransportProvider client))
+ if (!serverCommandChannel.TryGetClient(connection.ClientID, out TransportProvider? client))
return;
// Get remote certificate and corresponding trusted certificate
- remoteCertificate = client.Provider.SslStream.RemoteCertificate;
+ remoteCertificate = client!.Provider?.SslStream?.RemoteCertificate;
}
if (remoteCertificate is null)
return;
- if (!m_subscriberIdentities.TryGetValue(m_certificateChecker.GetTrustedCertificate(remoteCertificate), out DataRow subscriber))
+ if (m_certificateChecker is null || m_subscriberIdentities is null || !m_subscriberIdentities.TryGetValue(m_certificateChecker.GetTrustedCertificate(remoteCertificate)!, out DataRow? subscriber) /* || subscriber is null */)
return;
// Load client details from subscriber identity
@@ -2357,13 +2385,13 @@ private void UpdateCertificateChecker()
{
try
{
- if (m_certificateChecker is null || m_subscriberIdentities is null || SecurityMode != SecurityMode.TLS)
+ if (m_certificateChecker is null || m_subscriberIdentities is null || SecurityMode != SecurityMode.TLS || DataSource is null || !DataSource.Tables.Contains("Subscribers"))
return;
m_certificateChecker.DistrustAll();
m_subscriberIdentities.Clear();
- foreach (DataRow subscriber in DataSource.Tables["Subscribers"].Select("Enabled <> 0"))
+ foreach (DataRow subscriber in DataSource.Tables["Subscribers"]!.Select("Enabled <> 0"))
{
try
{
@@ -2379,7 +2407,9 @@ private void UpdateCertificateChecker()
if (!File.Exists(remoteCertificateFile))
continue;
+ #pragma warning disable SYSLIB0057
X509Certificate certificate = new X509Certificate2(remoteCertificateFile);
+ #pragma warning restore SYSLIB0057
m_certificateChecker.Trust(certificate, policy);
m_subscriberIdentities.Add(certificate, subscriber);
}
@@ -2396,18 +2426,17 @@ private void UpdateCertificateChecker()
}
// Update rights for the given subscription.
- private void UpdateRights(SubscriberConnection connection)
+ private void UpdateRights(SubscriberConnection? connection)
{
- if (connection is null)
+ if (connection is null || DataSource is null || !DataSource.Tables.Contains("Subscribers"))
return;
try
{
- SubscriberAdapter subscription = connection.Subscription;
+ SubscriberAdapter? subscription = connection.Subscription;
// Determine if the connection has been disabled or removed - make sure to set authenticated to false if necessary
- if (DataSource is not null && DataSource.Tables.Contains("Subscribers") &&
- !DataSource.Tables["Subscribers"].Select($"ID = '{connection.SubscriberID}' AND Enabled <> 0").Any())
+ if (DataSource is not null && DataSource.Tables.Contains("Subscribers") && DataSource.Tables["Subscribers"]!.Select($"ID = '{connection.SubscriberID}' AND Enabled <> 0").Length == 0)
connection.Authenticated = false;
if (subscription is null)
@@ -2415,12 +2444,10 @@ private void UpdateRights(SubscriberConnection connection)
// It is important here that "SELECT" not be allowed in parsing the input measurement keys expression since this key comes
// from the remote subscription - this will prevent possible SQL injection attacks.
- MeasurementKey[] requestedInputs = AdapterBase.ParseInputMeasurementKeys(DataSource, false, subscription.RequestedInputFilter);
+ MeasurementKey[] requestedInputs = AdapterBase.ParseInputMeasurementKeys(DataSource, false, subscription.RequestedInputFilter ?? "FILTER ActiveMeasurements WHERE True");
HashSet authorizedSignals = [];
- Func hasRightsFunc = ValidateMeasurementRights ?
- new SubscriberRightsLookup(DataSource, subscription.SubscriberID).HasRightsFunc :
- _ => true;
+ Func hasRightsFunc = ValidateMeasurementRights ? new SubscriberRightsLookup(DataSource, subscription.SubscriberID).HasRightsFunc : _ => true;
foreach (MeasurementKey input in requestedInputs)
{
@@ -2428,7 +2455,7 @@ private void UpdateRights(SubscriberConnection connection)
authorizedSignals.Add(input);
}
- if (authorizedSignals.SetEquals(subscription.InputMeasurementKeys))
+ if (authorizedSignals.SetEquals(subscription.InputMeasurementKeys ?? []))
return;
// Update the subscription associated with this connection based on newly acquired or revoked rights
@@ -2446,13 +2473,13 @@ private void UpdateRights(SubscriberConnection connection)
}
// Send binary response packet to client
- private bool SendClientResponse(Guid clientID, byte responseCode, byte commandCode, byte[] data)
+ private bool SendClientResponse(Guid clientID, byte responseCode, byte commandCode, byte[]? data)
{
// Attempt to lookup associated client connection
- if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection connection) || connection is null || connection.ClientNotFoundExceptionOccurred)
+ if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection) /* || connection is null */ || connection.ClientNotFoundExceptionOccurred)
return false;
- BlockAllocatedMemoryStream workingBuffer = null;
+ BlockAllocatedMemoryStream? workingBuffer = null;
bool success = false;
try
@@ -2529,12 +2556,10 @@ private bool SendClientResponse(Guid clientID, byte responseCode, byte commandCo
if (m_clientCommandChannel is null)
{
// Data packets and buffer blocks can be published on a UDP data channel, so check for this...
- IServer publishChannel = useDataChannel ?
- m_clientPublicationChannels.GetOrAdd(clientID, _ => connection.ServerPublishChannel) :
- m_serverCommandChannel;
+ IServer publishChannel = useDataChannel ? m_clientPublicationChannels.GetOrAdd(clientID, _ => connection.ServerPublishChannel!) : m_serverCommandChannel!;
// Send response packet
- if (publishChannel?.CurrentState == ServerState.Running)
+ if (publishChannel.CurrentState == ServerState.Running)
{
byte[] responseData = workingBuffer.ToArray();
@@ -2603,26 +2628,23 @@ private bool SendClientResponse(Guid clientID, byte responseCode, byte commandCo
}
// Socket exception handler
- private bool HandleSocketException(Guid clientID, Exception ex)
+ private bool HandleSocketException(Guid clientID, Exception? ex)
{
- if (ex is SocketException socketException)
+ // WSAECONNABORTED and WSAECONNRESET are common errors after a client disconnect,
+ // if they happen for other reasons, make sure disconnect procedure is handled
+ if (ex is SocketException { ErrorCode: 10053 or 10054 })
{
- // WSAECONNABORTED and WSAECONNRESET are common errors after a client disconnect,
- // if they happen for other reasons, make sure disconnect procedure is handled
- if (socketException.ErrorCode == 10053 || socketException.ErrorCode == 10054)
+ try
{
- try
- {
- ThreadPool.QueueUserWorkItem(DisconnectClient, clientID);
- }
- catch (Exception queueException)
- {
- // Process exception for logging
- OnProcessException(MessageLevel.Info, new InvalidOperationException($"Failed to queue client disconnect due to exception: {queueException.Message}", queueException));
- }
-
- return true;
+ ThreadPool.QueueUserWorkItem(DisconnectClient, clientID);
+ }
+ catch (Exception queueException)
+ {
+ // Process exception for logging
+ OnProcessException(MessageLevel.Info, new InvalidOperationException($"Failed to queue client disconnect due to exception: {queueException.Message}", queueException));
}
+
+ return true;
}
if (ex is not null)
@@ -2632,8 +2654,11 @@ private bool HandleSocketException(Guid clientID, Exception ex)
}
// Disconnect client - this should be called from non-blocking thread (e.g., thread pool)
- private void DisconnectClient(object state)
+ private void DisconnectClient(object? state)
{
+ if (state is null)
+ return;
+
m_commandChannelConnectionAttempts = 0;
try
@@ -2642,13 +2667,11 @@ private void DisconnectClient(object state)
RemoveClientSubscription(clientID);
- if (ClientConnections.TryRemove(clientID, out SubscriberConnection connection))
+ if (ClientConnections.TryRemove(clientID, out SubscriberConnection? connection))
{
connection.Dispose();
- OnStatusMessage(MessageLevel.Info, clientID == m_proxyClientID ?
- $"Data publisher client-based connection disconnected from subscriber via {m_clientCommandChannel.ServerUri}." :
- "Client disconnected from command channel.");
+ OnStatusMessage(MessageLevel.Info, clientID == m_proxyClientID ? $"Data publisher client-based connection disconnected from subscriber via {m_clientCommandChannel?.ServerUri ?? "undefined server URI"}." : "Client disconnected from command channel.");
}
m_clientPublicationChannels.TryRemove(clientID, out _);
@@ -2665,7 +2688,7 @@ private void RemoveClientSubscription(Guid clientID)
{
lock (this)
{
- if (!TryGetClientSubscription(clientID, out SubscriberAdapter clientSubscription))
+ if (!TryGetClientSubscription(clientID, out SubscriberAdapter? clientSubscription) || clientSubscription is null)
return;
clientSubscription.Stop();
@@ -2685,18 +2708,18 @@ private void RemoveClientSubscription(Guid clientID)
}
// Handle notification on input measurement key change
- private void NotifyHostOfSubscriptionRemoval(object state)
+ private void NotifyHostOfSubscriptionRemoval(object? _)
{
OnInputMeasurementKeysUpdated();
}
// Attempt to find client subscription
- private bool TryGetClientSubscription(Guid clientID, out SubscriberAdapter subscription)
+ private bool TryGetClientSubscription(Guid clientID, out SubscriberAdapter? subscription)
{
// Lookup adapter by its client ID
- if (TryGetAdapter(clientID, GetClientSubscription, out IActionAdapter adapter))
+ if (TryGetAdapter(clientID, GetClientSubscription, out IActionAdapter? adapter))
{
- subscription = (SubscriberAdapter)adapter;
+ subscription = (SubscriberAdapter)adapter!;
return true;
}
@@ -2710,12 +2733,12 @@ private bool GetClientSubscription(IActionAdapter item, Guid value)
}
// Gets specified property from client connection based on subscriber ID
- private TResult GetConnectionProperty(Guid subscriberID, Func predicate)
+ private TResult? GetConnectionProperty(Guid subscriberID, Func predicate)
{
- TResult result = default;
+ TResult? result = default;
// Lookup client connection by subscriber ID
- SubscriberConnection connection = ClientConnections.Values.FirstOrDefault(cc => cc.SubscriberID == subscriberID);
+ SubscriberConnection? connection = ClientConnections.Values.FirstOrDefault(cc => cc.SubscriberID == subscriberID);
// Extract desired property from client connection using given predicate function
if (connection is not null)
@@ -2759,43 +2782,66 @@ protected virtual void OnClientConnected(Guid subscriberID, string connectionID,
}
}
- protected internal new void OnStatusMessage(MessageLevel level, string status, string eventName = null, MessageFlags flags = MessageFlags.None)
+ ///
+ /// Raises the event and sends this data to the .
+ ///
+ /// The to assign to this message
+ /// New status message.
+ /// A fixed string to classify this event; defaults to null.
+ /// to use, if any; defaults to .
+ ///
+ /// should be a constant string value associated with what type of message is being
+ /// generated. In general, there should only be a few dozen distinct event names per class. Exceeding this
+ /// threshold will cause the EventName to be replaced with a general warning that a usage issue has occurred.
+ ///
+ // Re-exposed for internal use, see SubscriberConnection
+ protected internal new void OnStatusMessage(MessageLevel level, string status, string? eventName = null, MessageFlags flags = MessageFlags.None)
{
base.OnStatusMessage(level, status, eventName, flags);
}
- protected internal new void OnProcessException(MessageLevel level, Exception exception, string eventName = null, MessageFlags flags = MessageFlags.None)
+ ///
+ /// Raises the event.
+ ///
+ /// The to assign to this message
+ /// Processing .
+ /// A fixed string to classify this event; defaults to null.
+ /// to use, if any; defaults to .
+ ///
+ /// should be a constant string value associated with what type of message is being
+ /// generated. In general, there should only be a few dozen distinct event names per class. Exceeding this
+ /// threshold will cause the EventName to be replaced with a general warning that a usage issue has occurred.
+ ///
+ // Re-exposed for internal use, see SubscriberConnection
+ protected internal new void OnProcessException(MessageLevel level, Exception exception, string? eventName = null, MessageFlags flags = MessageFlags.None)
{
base.OnProcessException(level, exception, eventName, flags);
}
// Make sure to expose any routing table messages
- private void RoutingTables_StatusMessage(object sender, EventArgs e)
+ private void RoutingTables_StatusMessage(object? sender, EventArgs e)
{
OnStatusMessage(MessageLevel.Info, e.Argument);
}
// Make sure to expose any routing table exceptions
- private void RoutingTables_ProcessException(object sender, EventArgs e)
+ private void RoutingTables_ProcessException(object? sender, EventArgs e)
{
OnProcessException(MessageLevel.Warning, e.Argument);
}
// Cipher key rotation timer handler
- private void CipherKeyRotationTimer_Elapsed(object sender, EventArgs e)
+ private void CipherKeyRotationTimer_Elapsed(object? sender, EventArgs e)
{
- if (ClientConnections is null)
- return;
-
foreach (SubscriberConnection connection in ClientConnections.Values)
{
- if (connection is not null && connection.Authenticated)
+ if (connection.Authenticated)
connection.RotateCipherKeys();
}
}
// Determines whether the data in the data source has actually changed when receiving a new data source.
- private bool DataSourceChanged(DataSet newDataSource)
+ private bool DataSourceChanged(DataSet? newDataSource)
{
try
{
@@ -2813,7 +2859,7 @@ private bool DataSourceChanged(DataSet newDataSource)
private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buffer, int startIndex, int length)
{
Guid clientID = connection.ClientID;
- SubscriberAdapter subscription;
+ SubscriberAdapter? subscription;
string message;
// Handle subscribe
@@ -2856,7 +2902,7 @@ private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buff
// Update connection string settings for GSF adapter syntax:
Dictionary settings = connectionString.ParseKeyValuePairs();
- if (settings.TryGetValue(nameof(SubscriptionInfo.Throttled), out string setting))
+ if (settings.TryGetValue(nameof(SubscriptionInfo.Throttled), out string? setting))
settings[nameof(FacileActionAdapterBase.TrackLatestMeasurements)] = setting;
if (settings.TryGetValue(nameof(SubscriptionInfo.FilterExpression), out setting))
@@ -2877,9 +2923,9 @@ private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buff
if (subscription.Settings.TryGetValue("dataChannel", out setting))
{
- Socket clientSocket = connection.GetCommandChannelSocket();
+ Socket? clientSocket = connection.GetCommandChannelSocket();
settings = setting.ParseKeyValuePairs();
- IPEndPoint localEndPoint = null;
+ IPEndPoint? localEndPoint = null;
string networkInterface = "::0";
// Make sure return interface matches incoming client connection
@@ -2892,7 +2938,7 @@ private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buff
// Remove dual-stack prefix
if (networkInterface.StartsWith("::ffff:", true, CultureInfo.InvariantCulture))
- networkInterface = networkInterface.Substring(7);
+ networkInterface = networkInterface[7..];
}
if (settings.TryGetValue("port", out setting) || settings.TryGetValue("localport", out setting))
@@ -2914,7 +2960,7 @@ private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buff
}
// Remove any existing cached publication channel since connection is changing
- m_clientPublicationChannels.TryRemove(clientID, out IServer _);
+ m_clientPublicationChannels.TryRemove(clientID, out _);
// Update payload compression state and strength
subscription.UsePayloadCompression = usePayloadCompression;
@@ -2956,7 +3002,7 @@ private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buff
if (connection.Version == 1)
{
// Send updated signal index cache to client with validated rights of the selected input measurement keys
- byte[] serializedSignalIndexCache = SerializeSignalIndexCache(clientID, connection.SignalIndexCache);
+ byte[]? serializedSignalIndexCache = SerializeSignalIndexCache(clientID, connection.SignalIndexCache);
SendClientResponse(clientID, ServerResponse.UpdateSignalIndexCache, ServerCommand.Subscribe, serializedSignalIndexCache);
}
@@ -2969,9 +3015,9 @@ private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buff
subscription.Start();
// If client has subscribed to any cached measurements, queue them up for the client
- if (TryGetAdapterByName(nameof(LatestMeasurementCache), out IActionAdapter cacheAdapter))
+ if (TryGetAdapterByName(nameof(LatestMeasurementCache), out IActionAdapter? cacheAdapter))
{
- if (cacheAdapter is LatestMeasurementCache cache)
+ if (cacheAdapter is LatestMeasurementCache cache && subscription.InputMeasurementKeys is not null)
{
IEnumerable cachedMeasurements = cache.LatestMeasurements.Where(measurement => subscription.InputMeasurementKeys.Any(key => key.SignalID == measurement.ID));
subscription.QueueMeasurementsForProcessing(cachedMeasurements);
@@ -3002,9 +3048,7 @@ private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buff
}
else
{
- message = subscription.InputMeasurementKeys is null ?
- $"Client subscribed as {(useCompactMeasurementFormat ? "" : "non-")}compact, but no signals were specified. Make sure \"inputMeasurementKeys\" setting is properly defined." :
- $"Client subscribed as {(useCompactMeasurementFormat ? "" : "non-")}compact with {subscription.InputMeasurementKeys.Length} signals.";
+ message = subscription.InputMeasurementKeys is null ? $"Client subscribed as {(useCompactMeasurementFormat ? "" : "non-")}compact, but no signals were specified. Make sure \"inputMeasurementKeys\" setting is properly defined." : $"Client subscribed as {(useCompactMeasurementFormat ? "" : "non-")}compact with {subscription.InputMeasurementKeys.Length} signals.";
}
connection.IsSubscribed = true;
@@ -3013,9 +3057,7 @@ private void HandleSubscribeRequest(SubscriberConnection connection, byte[] buff
}
else
{
- message = byteLength > 0 ?
- "Not enough buffer was provided to parse client data subscription." :
- "Cannot initialize client data subscription without a connection string.";
+ message = byteLength > 0 ? "Not enough buffer was provided to parse client data subscription." : "Cannot initialize client data subscription without a connection string.";
SendClientResponse(clientID, ServerResponse.Failed, ServerCommand.Subscribe, message);
OnProcessException(MessageLevel.Warning, new InvalidOperationException(message));
@@ -3065,12 +3107,17 @@ private void HandleUnsubscribeRequest(SubscriberConnection connection)
/// Meta-data to be returned to client.
protected virtual DataSet AcquireMetadata(SubscriberConnection connection, Dictionary> filterExpressions)
{
+#if NET
+ using AdoDataConnection adoDatabase = new(ConfigSettings.Default);
+ DbConnection dbConnection = adoDatabase.Connection;
+#else
using AdoDataConnection adoDatabase = new("systemSettings");
IDbConnection dbConnection = adoDatabase.Connection;
- DataSet metadata = new();
-
+
// Initialize active node ID
- Guid nodeID = Guid.Parse(dbConnection.ExecuteScalar($"SELECT NodeID FROM IaonActionAdapter WHERE ID = {ID}").ToString());
+ Guid nodeID = Guid.Parse(dbConnection.ExecuteScalar($"SELECT NodeID FROM IaonActionAdapter WHERE ID = {ID}")?.ToString() ?? Guid.Empty.ToString());
+#endif
+ DataSet metadata = new();
// Determine whether we're sending internal and external meta-data
bool sendExternalMetadata = connection.OperationalModes.HasFlag(OperationalModes.ReceiveExternalMetadata);
@@ -3082,11 +3129,37 @@ protected virtual DataSet AcquireMetadata(SubscriberConnection connection, Dicti
if (string.IsNullOrWhiteSpace(tableExpression))
continue;
+ string metadataQuery = tableExpression.Trim();
+
// Query the table or view information from the database
- DataTable table = dbConnection.RetrieveData(adoDatabase.AdapterType, tableExpression);
+ // ReSharper disable once JoinDeclarationAndInitializer
+ DataTable table;
+ Match regexMatch;
+
+ if (adoDatabase.IsSqlite)
+ {
+ // SQLite does not support TOP clause, so we need to replace it with LIMIT
+ regexMatch = Regex.Match(tableExpression, @"\s+TOP\s+(\d+)\s+", RegexOptions.IgnoreCase);
+
+ if (regexMatch.Success)
+ {
+ // Remove TOP clause with count
+ string topCount = regexMatch.Groups[1].Value;
+ metadataQuery = Regex.Replace(tableExpression, @"\s+TOP\s+\d+\s+", " ");
+
+ // Append LIMIT clause with count
+ metadataQuery = $"{metadataQuery} LIMIT {topCount}";
+ }
+ }
+
+ #if NET
+ table = dbConnection.RetrieveData(metadataQuery);
+ #else
+ table = dbConnection.RetrieveData(adoDatabase.AdapterType, metadataQuery);
+ #endif
// Remove any expression from table name
- Match regexMatch = Regex.Match(tableExpression, @"FROM \w+");
+ regexMatch = Regex.Match(metadataQuery, @"FROM \w+");
table.TableName = regexMatch.Value.Split(' ')[1];
string sortField = "";
@@ -3095,8 +3168,10 @@ protected virtual DataSet AcquireMetadata(SubscriberConnection connection, Dicti
// Build filter list
List filters = [];
+ #if !NET
if (table.Columns.Contains("NodeID"))
filters.Add($"NodeID = '{nodeID}'");
+ #endif
if (table.Columns.Contains("Internal") && !(sendInternalMetadata && sendExternalMetadata))
filters.Add($"Internal {(sendExternalMetadata ? "=" : "<>")} 0");
@@ -3104,7 +3179,7 @@ protected virtual DataSet AcquireMetadata(SubscriberConnection connection, Dicti
if (table.Columns.Contains("OriginalSource") && !(sendInternalMetadata && sendExternalMetadata) && !MutualSubscription)
filters.Add($"OriginalSource IS {(sendExternalMetadata ? "NOT" : "")} NULL");
- if (filterExpressions.TryGetValue(table.TableName, out Tuple filterParameters))
+ if (filterExpressions.TryGetValue(table.TableName, out Tuple? filterParameters))
{
filters.Add($"({filterParameters.Item1})");
sortField = filterParameters.Item2;
@@ -3140,7 +3215,7 @@ protected virtual DataSet AcquireMetadata(SubscriberConnection connection, Dicti
if (filteredRowList.Count == 0)
continue;
- DataTable metadataTable = metadata.Tables[table.TableName];
+ DataTable metadataTable = metadata.Tables[table.TableName]!;
// Manually copy-in each row into table
foreach (DataRow row in filteredRowList)
@@ -3157,41 +3232,41 @@ protected virtual DataSet AcquireMetadata(SubscriberConnection connection, Dicti
}
// Do some post analysis on the meta-data to be delivered to the client, e.g., if a device exists with no associated measurements - don't send the device.
- if (!metadata.Tables.Contains("MeasurementDetail") || !metadata.Tables["MeasurementDetail"].Columns.Contains("DeviceAcronym") || !metadata.Tables.Contains("DeviceDetail") || !metadata.Tables["DeviceDetail"].Columns.Contains("Acronym"))
+ if (!metadata.Tables.Contains("MeasurementDetail") || !metadata.Tables["MeasurementDetail"]!.Columns.Contains("DeviceAcronym") || !metadata.Tables.Contains("DeviceDetail") || !metadata.Tables["DeviceDetail"]!.Columns.Contains("Acronym"))
return metadata;
List rowsToRemove = [];
string deviceAcronym;
// Remove device records where no associated measurement records exist
- foreach (DataRow row in metadata.Tables["DeviceDetail"].Rows)
+ foreach (DataRow row in metadata.Tables["DeviceDetail"]!.Rows)
{
deviceAcronym = row["Acronym"].ToNonNullString();
- if (!string.IsNullOrEmpty(deviceAcronym) && (int)metadata.Tables["MeasurementDetail"].Compute("Count(DeviceAcronym)", $"DeviceAcronym = '{deviceAcronym}'") == 0)
+ if (!string.IsNullOrEmpty(deviceAcronym) && (int)metadata.Tables["MeasurementDetail"]!.Compute("Count(DeviceAcronym)", $"DeviceAcronym = '{deviceAcronym}'") == 0)
rowsToRemove.Add(row);
}
- if (metadata.Tables.Contains("PhasorDetail") && metadata.Tables["PhasorDetail"].Columns.Contains("DeviceAcronym"))
+ if (metadata.Tables.Contains("PhasorDetail") && metadata.Tables["PhasorDetail"]!.Columns.Contains("DeviceAcronym"))
{
// Remove phasor records where no associated device records exist
- foreach (DataRow row in metadata.Tables["PhasorDetail"].Rows)
+ foreach (DataRow row in metadata.Tables["PhasorDetail"]!.Rows)
{
deviceAcronym = row["DeviceAcronym"].ToNonNullString();
- if (!string.IsNullOrEmpty(deviceAcronym) && (int)metadata.Tables["DeviceDetail"].Compute("Count(Acronym)", $"Acronym = '{deviceAcronym}'") == 0)
+ if (!string.IsNullOrEmpty(deviceAcronym) && (int)metadata.Tables["DeviceDetail"]!.Compute("Count(Acronym)", $"Acronym = '{deviceAcronym}'") == 0)
rowsToRemove.Add(row);
}
- if (metadata.Tables["PhasorDetail"].Columns.Contains("SourceIndex") && metadata.Tables["MeasurementDetail"].Columns.Contains("PhasorSourceIndex"))
+ if (metadata.Tables["PhasorDetail"]!.Columns.Contains("SourceIndex") && metadata.Tables["MeasurementDetail"]!.Columns.Contains("PhasorSourceIndex"))
{
// Remove measurement records where no associated phasor records exist
- foreach (DataRow row in metadata.Tables["MeasurementDetail"].Rows)
+ foreach (DataRow row in metadata.Tables["MeasurementDetail"]!.Rows)
{
deviceAcronym = row["DeviceAcronym"].ToNonNullString();
int? phasorSourceIndex = row.ConvertField("PhasorSourceIndex");
- if (!string.IsNullOrEmpty(deviceAcronym) && phasorSourceIndex is not null && (int)metadata.Tables["PhasorDetail"].Compute("Count(DeviceAcronym)", $"DeviceAcronym = '{deviceAcronym}' AND SourceIndex = {phasorSourceIndex}") == 0)
+ if (!string.IsNullOrEmpty(deviceAcronym) && phasorSourceIndex is not null && (int)metadata.Tables["PhasorDetail"]!.Compute("Count(DeviceAcronym)", $"DeviceAcronym = '{deviceAcronym}' AND SourceIndex = {phasorSourceIndex}") == 0)
rowsToRemove.Add(row);
}
}
@@ -3250,7 +3325,7 @@ private void HandleMetadataRefresh(SubscriberConnection connection, byte[] buffe
try
{
DataSet metadata = AcquireMetadata(connection, filterExpressions);
- byte[] serializedMetadata = SerializeMetadata(clientID, metadata);
+ byte[]? serializedMetadata = SerializeMetadata(clientID, metadata);
long rowCount = metadata.Tables.Cast().Select(dataTable => (long)dataTable.Rows.Count).Sum();
if (rowCount > 0)
@@ -3285,7 +3360,7 @@ private void HandleUpdateProcessingInterval(SubscriberConnection connection, byt
// Next 4 bytes are an integer representing the new processing interval
int processingInterval = BigEndian.ToInt32(buffer, startIndex);
- SubscriberAdapter subscription = connection.Subscription;
+ SubscriberAdapter? subscription = connection.Subscription;
if (subscription is null)
{
@@ -3337,9 +3412,10 @@ private void HandleConfirmNotification(SubscriberConnection connection, byte[] b
{
lock (m_clientNotificationsLock)
{
- if (m_clientNotifications.TryGetValue(connection.SubscriberID, out Dictionary notifications))
+ if (m_clientNotifications.TryGetValue(connection.SubscriberID, out Dictionary? notifications) /* && notifications is not null */)
{
- if (notifications.TryGetValue(hash, out string notification))
+ // ReSharper disable once CanSimplifyDictionaryRemovingWithSingleCall
+ if (notifications.TryGetValue(hash, out string? notification))
{
notifications.Remove(hash);
OnStatusMessage(MessageLevel.Info, $"Subscriber {connection.ConnectionID} confirmed receipt of notification: {notification}.");
@@ -3362,18 +3438,18 @@ private void HandleConfirmNotification(SubscriberConnection connection, byte[] b
}
}
- private void HandleConfirmBufferBlock(SubscriberConnection connection, byte[] buffer, int startIndex, int length)
+ private static void HandleConfirmBufferBlock(SubscriberConnection connection, byte[] buffer, int startIndex, int length)
{
if (length < 4)
return;
uint sequenceNumber = BigEndian.ToUInt32(buffer, startIndex);
- connection.Subscription.ConfirmBufferBlock(sequenceNumber);
+ connection.Subscription?.ConfirmBufferBlock(sequenceNumber);
}
- private void HandleConfirmSignalIndexCache(SubscriberConnection connection)
+ private static void HandleConfirmSignalIndexCache(SubscriberConnection connection)
{
- connection.Subscription.ConfirmSignalIndexCache(connection.ClientID);
+ connection.Subscription?.ConfirmSignalIndexCache(connection.ClientID);
}
private void HandlePublishCommandMeasurements(SubscriberConnection connection, byte[] buffer, int startIndex)
@@ -3394,7 +3470,7 @@ private void HandlePublishCommandMeasurements(SubscriberConnection connection, b
CommandMeasurement measurement = new();
connectionStringParser.ParseConnectionString(measurementString, measurement);
- measurements.Add(new Measurement()
+ measurements.Add(new Measurement
{
Metadata = MeasurementKey.LookUpBySignalID(measurement.SignalID).Metadata,
Timestamp = measurement.Timestamp,
@@ -3429,12 +3505,12 @@ protected virtual void HandleUserCommand(SubscriberConnection connection, Server
OnStatusMessage(MessageLevel.Info, $"Received command code for user-defined command \"{command}\".");
}
- private byte[] SerializeSignalIndexCache(Guid clientID, SignalIndexCache signalIndexCache)
+ private byte[]? SerializeSignalIndexCache(Guid clientID, SignalIndexCache? signalIndexCache)
{
- if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection connection) || connection is null)
+ if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection) /* || connection is null */)
return null;
- if (signalIndexCache.Reference.Count == 0)
+ if (signalIndexCache is null || signalIndexCache.Reference.Count == 0)
return null;
OperationalModes operationalModes = connection.OperationalModes;
@@ -3443,7 +3519,7 @@ private byte[] SerializeSignalIndexCache(Guid clientID, SignalIndexCache signalI
using BlockAllocatedMemoryStream compressedData = new();
if (connection.Version > 1)
- compressedData.Write(byte.MaxValue); // Place-holder - actual value updated inside lock
+ compressedData.WriteByte(byte.MaxValue); // Place-holder - actual value updated inside lock
signalIndexCache.Encoding = GetClientEncoding(clientID);
byte[] serializedSignalIndexCache = new byte[signalIndexCache.BinaryLength];
@@ -3454,12 +3530,12 @@ private byte[] SerializeSignalIndexCache(Guid clientID, SignalIndexCache signalI
{
if (connection.Version <= 1)
return serializedSignalIndexCache;
-
+
compressedData.Write(serializedSignalIndexCache);
return compressedData.ToArray();
}
- GZipStream deflater = null;
+ GZipStream? deflater = null;
try
{
@@ -3479,9 +3555,9 @@ private byte[] SerializeSignalIndexCache(Guid clientID, SignalIndexCache signalI
return serializedSignalIndexCache;
}
- private byte[] SerializeMetadata(Guid clientID, DataSet metadata)
+ private byte[]? SerializeMetadata(Guid clientID, DataSet metadata)
{
- if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection connection))
+ if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection))
return null;
byte[] serializedMetadata;
@@ -3489,7 +3565,7 @@ private byte[] SerializeMetadata(Guid clientID, DataSet metadata)
OperationalModes operationalModes = connection.OperationalModes;
CompressionModes compressionModes = (CompressionModes)(operationalModes & OperationalModes.CompressionModeMask);
bool compressMetadata = (operationalModes & OperationalModes.CompressMetadata) > 0;
- GZipStream deflater = null;
+ GZipStream? deflater = null;
// Encode XML into encoded data buffer
using (BlockAllocatedMemoryStream encodedData = new())
@@ -3557,13 +3633,13 @@ private void ResetMeasurementsPerSecondCounters()
m_measurementsPerSecondCount = 0L;
}
- private void Subscription_BufferBlockRetransmission(object sender, EventArgs eventArgs)
+ private void Subscription_BufferBlockRetransmission(object? sender, EventArgs eventArgs)
{
BufferBlockRetransmissions++;
}
// Bubble up processing complete notifications from subscriptions
- private void Subscription_ProcessingComplete(object sender, EventArgs e)
+ private void Subscription_ProcessingComplete(object? sender, EventArgs e)
{
// Expose notification via data publisher event subscribers
ProcessingComplete?.Invoke(sender, e.Argument2);
@@ -3572,15 +3648,14 @@ private void Subscription_ProcessingComplete(object sender, EventArgs e)
+ private void ServerCommandChannelReceiveClientDataComplete(object? sender, EventArgs e)
{
try
{
@@ -3589,7 +3664,7 @@ private void ServerCommandChannelReceiveClientDataComplete(object sender, EventA
int length = e.Argument3;
int index = 0;
- if (length <= 0 || buffer is null)
+ if (length <= 0)
return;
byte commandByte = buffer[index];
@@ -3599,7 +3674,7 @@ private void ServerCommandChannelReceiveClientDataComplete(object sender, EventA
bool validServerCommand = Enum.TryParse(commandByte.ToString(), out ServerCommand command);
// Look up this client connection
- if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection connection))
+ if (!ClientConnections.TryGetValue(clientID, out SubscriberConnection? connection))
{
// Received a request from an unknown client, this request is denied
OnStatusMessage(MessageLevel.Warning, $"Ignored {length} byte {(validServerCommand ? command.ToString() : "unidentified")} command request received from an unrecognized client: {clientID}", flags: MessageFlags.UsageIssue);
@@ -3675,7 +3750,7 @@ private void ServerCommandChannelReceiveClientDataComplete(object sender, EventA
case ServerCommand.UserCommand14:
case ServerCommand.UserCommand15:
// Handle confirmation of receipt of a user-defined command
- HandleUserCommand(connection, command, buffer, index, length);
+ HandleUserCommand(connection, command, buffer, index, --length);
break;
}
}
@@ -3693,7 +3768,7 @@ private void ServerCommandChannelReceiveClientDataComplete(object sender, EventA
}
}
- private void ServerCommandChannelClientConnected(object sender, EventArgs e)
+ private void ServerCommandChannelClientConnected(object? sender, EventArgs e)
{
Guid clientID = e.Argument;
@@ -3711,7 +3786,7 @@ private void ServerCommandChannelClientConnected(object sender, EventArgs
ClientConnections[clientID] = connection;
- OnStatusMessage(MessageLevel.Info, $"Client connected to command channel {(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}.");
+ OnStatusMessage(MessageLevel.Info, $"Client connected to command channel{(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}.");
if (connection.Authenticated)
{
@@ -3731,7 +3806,7 @@ private void ServerCommandChannelClientConnected(object sender, EventArgs
}
}
- private void ServerCommandChannelClientDisconnected(object sender, EventArgs e)
+ private void ServerCommandChannelClientDisconnected(object? sender, EventArgs e)
{
try
{
@@ -3744,18 +3819,18 @@ private void ServerCommandChannelClientDisconnected(object sender, EventArgs e)
+ private void ServerCommandChannelClientConnectingException(object? sender, EventArgs e)
{
Exception ex = e.Argument;
OnProcessException(MessageLevel.Info, new ConnectionException($"Data publisher encountered an exception while connecting client to the command channel: {ex.Message}", ex));
}
- private void ServerCommandChannelServerStarted(object sender, EventArgs e)
+ private void ServerCommandChannelServerStarted(object? sender, EventArgs e)
{
OnStatusMessage(MessageLevel.Info, "Data publisher command channel started.");
}
- private void ServerCommandChannelServerStopped(object sender, EventArgs e)
+ private void ServerCommandChannelServerStopped(object? sender, EventArgs e)
{
if (Enabled)
{
@@ -3765,7 +3840,7 @@ private void ServerCommandChannelServerStopped(object sender, EventArgs e)
{
try
{
- m_serverCommandChannel.Start();
+ m_serverCommandChannel?.Start();
}
catch (Exception ex)
{
@@ -3782,7 +3857,7 @@ private void ServerCommandChannelServerStopped(object sender, EventArgs e)
}
}
- private void ServerCommandChannelSendClientDataException(object sender, EventArgs e)
+ private void ServerCommandChannelSendClientDataException(object? sender, EventArgs e)
{
Exception ex = e.Argument2;
@@ -3790,7 +3865,7 @@ private void ServerCommandChannelSendClientDataException(object sender, EventArg
OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data publisher encountered an exception while sending command channel data to client connection: {ex.Message}", ex));
}
- private void ServerCommandChannelReceiveClientDataException(object sender, EventArgs e)
+ private void ServerCommandChannelReceiveClientDataException(object? sender, EventArgs e)
{
Exception ex = e.Argument2;
@@ -3802,7 +3877,7 @@ private void ServerCommandChannelReceiveClientDataException(object sender, Event
#region [ Client Command Channel Event Handlers ]
- private void ClientCommandChannelConnectionEstablished(object sender, EventArgs e)
+ private void ClientCommandChannelConnectionEstablished(object? sender, EventArgs e)
{
try
{
@@ -3810,18 +3885,18 @@ private void ClientCommandChannelConnectionEstablished(object sender, EventArgs
if (m_proxyClientID is not null && ClientConnections.ContainsKey(m_proxyClientID.GetValueOrDefault()))
return;
- OnStatusMessage(MessageLevel.Info, $"Client-based command channel subscriber connection established via {m_clientCommandChannel.ServerUri}.");
+ OnStatusMessage(MessageLevel.Info, $"Client-based command channel subscriber connection established{(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}.");
m_proxyClientID ??= Guid.NewGuid();
ServerCommandChannelClientConnected(sender, new EventArgs(m_proxyClientID.GetValueOrDefault()));
}
catch (Exception ex)
{
- OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to establish client connection session via {m_clientCommandChannel.ServerUri}: {ex.Message}", ex));
+ OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to establish client connection session{(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}: {ex.Message}", ex));
}
}
- private void ClientCommandChannelConnectionTerminated(object sender, EventArgs e)
+ private void ClientCommandChannelConnectionTerminated(object? sender, EventArgs e)
{
ServerCommandChannelClientDisconnected(sender, new EventArgs(m_proxyClientID.GetValueOrDefault()));
@@ -3830,57 +3905,57 @@ private void ClientCommandChannelConnectionTerminated(object sender, EventArgs e
// If user didn't initiate disconnect, restart the connection
new Action(() =>
- {
- if (!Enabled || m_clientCommandChannel is null || m_clientCommandChannel.CurrentState != ClientState.Disconnected)
- return;
+ {
+ if (!Enabled || m_clientCommandChannel is null || m_clientCommandChannel.CurrentState != ClientState.Disconnected)
+ return;
- OnStatusMessage(MessageLevel.Info, $"Attempting to re-establish client-based command channel subscriber connection via {m_clientCommandChannel.ServerUri}...");
- m_commandChannelConnectionAttempts = 0;
- m_clientCommandChannel.ConnectAsync();
- })
- .DelayAndExecute(1000);
+ OnStatusMessage(MessageLevel.Info, $"Attempting to re-establish client-based command channel subscriber connection{(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}...");
+ m_commandChannelConnectionAttempts = 0;
+ m_clientCommandChannel?.ConnectAsync();
+ })
+ .DelayAndExecute(1000);
}
- private void ClientCommandChannelConnectionException(object sender, EventArgs e)
+ private void ClientCommandChannelConnectionException(object? sender, EventArgs e)
{
Exception ex = e.Argument;
- OnProcessException(MessageLevel.Info, new ConnectionException($"Data publisher encountered an exception while attempting client-based command channel subscriber connection via {m_clientCommandChannel.ServerUri}: {ex.Message}", ex));
+ OnProcessException(MessageLevel.Info, new ConnectionException($"Data publisher encountered an exception while attempting client-based command channel subscriber connection{(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}: {ex.Message}", ex));
}
- private void ClientCommandChannelConnectionAttempt(object sender, EventArgs e)
+ private void ClientCommandChannelConnectionAttempt(object? sender, EventArgs e)
{
// Inject a short delay between multiple connection attempts
if (m_commandChannelConnectionAttempts > 0)
Thread.Sleep(2000);
- OnStatusMessage(MessageLevel.Info, $"Attempting client-based command channel connection to subscriber via {m_clientCommandChannel.ServerUri}...");
+ OnStatusMessage(MessageLevel.Info, $"Attempting client-based command channel connection to subscriber{(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}...");
m_commandChannelConnectionAttempts++;
}
- private void ClientCommandChannelSendDataException(object sender, EventArgs e)
+ private void ClientCommandChannelSendDataException(object? sender, EventArgs e)
{
Exception ex = e.Argument;
if (!HandleSocketException(m_proxyClientID.GetValueOrDefault(), ex) && ex is not ObjectDisposedException)
- OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data publisher encountered an exception while sending client-based command channel data to subscriber connection via {m_clientCommandChannel.ServerUri}: {ex.Message}", ex));
+ OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data publisher encountered an exception while sending client-based command channel data to subscriber connection{(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}: {ex.Message}", ex));
}
- private void ClientCommandChannelReceiveDataComplete(object sender, EventArgs e)
+ private void ClientCommandChannelReceiveDataComplete(object? sender, EventArgs e)
{
ServerCommandChannelReceiveClientDataComplete(sender, new EventArgs(m_proxyClientID.GetValueOrDefault(), e.Argument1, e.Argument2));
}
- private void ClientCommandChannelReceiveDataException(object sender, EventArgs e)
+ private void ClientCommandChannelReceiveDataException(object? sender, EventArgs e)
{
Exception ex = e.Argument;
if (!HandleSocketException(m_proxyClientID.GetValueOrDefault(), ex) && ex is not ObjectDisposedException)
- OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data publisher encountered an exception while receiving client-based command channel data from subscriber connection via {m_clientCommandChannel.ServerUri}: {ex.Message}", ex));
+ OnProcessException(MessageLevel.Info, new InvalidOperationException($"Data publisher encountered an exception while receiving client-based command channel data from subscriber connection{(m_clientCommandChannel is null ? "" : $" via {m_clientCommandChannel.ServerUri}")}: {ex.Message}", ex));
}
#endregion
- #endregion
+#endregion
#region [ Static ]
@@ -3900,7 +3975,7 @@ private static List ParseAddressList(string addressList)
foreach (string address in splitList)
{
// Attempt to parse the IP address
- if (!IPAddress.TryParse(address.Trim(), out IPAddress ipAddress))
+ if (!IPAddress.TryParse(address.Trim(), out IPAddress? ipAddress))
continue;
// Add the parsed address to the list
@@ -3934,8 +4009,8 @@ private static string GetOperationalModes(SubscriberConnection connection)
}
else
{
- description.Append($" {(connection.Subscription.UseCompactMeasurementFormat ? "Compact" : "FullSize")}PayloadData[");
- description.AppendLine($"{connection.Subscription.TimestampSize}-byte Timestamps]");
+ description.Append($" {(connection.Subscription?.UseCompactMeasurementFormat ?? true ? "Compact" : "FullSize")}PayloadData[");
+ description.AppendLine($"{connection.Subscription?.TimestampSize ?? 8}-byte Timestamps]");
}
if ((operationalModes & OperationalModes.CompressSignalIndexCache) > 0 && gzipEnabled)
diff --git a/src/lib/DataSubscriber.cs b/src/lib/sttp.core/DataSubscriber.cs
similarity index 84%
rename from src/lib/DataSubscriber.cs
rename to src/lib/sttp.core/DataSubscriber.cs
index 3de75e25..abadf032 100644
--- a/src/lib/DataSubscriber.cs
+++ b/src/lib/sttp.core/DataSubscriber.cs
@@ -1,4878 +1,5101 @@
-//******************************************************************************************************
-// DataSubscriber.cs - Gbtc
-//
-// Copyright © 2012, 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://www.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:
-// ----------------------------------------------------------------------------------------------------
-// 08/20/2010 - J. Ritchie Carroll
-// Generated original version of source code.
-// 02/07/2012 - Mehulbhai Thakkar
-// Modified SynchronizeMetadata to filter devices by original source and modified insert query
-// to populate OriginalSource value. Added to flag to optionally avoid meta-data synchronization.
-// 12/20/2012 - Starlynn Danyelle Gilliam
-// Modified Header.
-//
-//******************************************************************************************************
-
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Data;
-using System.Globalization;
-using System.IO;
-using System.IO.Compression;
-using System.Linq;
-using System.Net;
-using System.Net.Security;
-using System.Net.Sockets;
-using System.Security.Cryptography.X509Certificates;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Xml;
-using GSF;
-using GSF.Collections;
-using GSF.Communication;
-using GSF.Configuration;
-using GSF.Data;
-using GSF.Diagnostics;
-using GSF.IO;
-using GSF.Net.Security;
-using GSF.Reflection;
-using GSF.Security.Cryptography;
-using GSF.Threading;
-using GSF.TimeSeries;
-using GSF.TimeSeries.Adapters;
-using GSF.TimeSeries.Data;
-using GSF.TimeSeries.Statistics;
-using GSF.TimeSeries.Transport;
-using GSF.Units;
-using sttp.tssc;
-using TcpClient = GSF.Communication.TcpClient;
-using UdpClient = GSF.Communication.UdpClient;
-
-// ReSharper disable BadControlBracesIndent
-namespace sttp;
-
-///
-/// Represents a data subscribing client that will connect to a data publisher for a data subscription.
-///
-[Description("STTP Subscriber: client that subscribes to an STTP-style publishing server for a streaming data.")]
-[EditorBrowsable(EditorBrowsableState.Advanced)] // Normally defined as an input device protocol
-public class DataSubscriber : InputAdapterBase
-{
- #region [ Members ]
-
- // Nested Types
-
- private class SubscribedDevice : IDevice, IDisposable
- {
- #region [ Members ]
-
- // Fields
- private long m_dataQualityErrors;
- private long m_timeQualityErrors;
- private long m_deviceErrors;
- private long m_measurementsReceived;
- private double m_measurementsExpected;
- private long m_measurementsWithError;
- private long m_measurementsDefined;
- private bool m_disposed;
-
- #endregion
-
- #region [ Constructors ]
-
- public SubscribedDevice(string name)
- {
- Name = name ?? throw new ArgumentNullException(nameof(name));
- StatisticsEngine.Register(this, name, "Device", "PMU");
- }
-
- ///
- /// Releases the unmanaged resources before the object is reclaimed by .
- ///
- ~SubscribedDevice()
- {
- Unregister();
- }
-
- #endregion
-
- #region [ Properties ]
-
- public string Name { get; }
-
- public Guid StatusFlagsID { get; set; }
-
- public Guid FrequencyID { get; set; }
-
- public Guid DeltaFrequencyID { get; set; }
-
- public long DataQualityErrors
- {
- get => Interlocked.Read(ref m_dataQualityErrors);
- set => Interlocked.Exchange(ref m_dataQualityErrors, value);
- }
-
- public long TimeQualityErrors
- {
- get => Interlocked.Read(ref m_timeQualityErrors);
- set => Interlocked.Exchange(ref m_timeQualityErrors, value);
- }
-
- public long DeviceErrors
- {
- get => Interlocked.Read(ref m_deviceErrors);
- set => Interlocked.Exchange(ref m_deviceErrors, value);
- }
-
- public long MeasurementsReceived
- {
- get => Interlocked.Read(ref m_measurementsReceived);
- set => Interlocked.Exchange(ref m_measurementsReceived, value);
- }
-
- public long MeasurementsExpected
- {
- get => (long)Interlocked.CompareExchange(ref m_measurementsExpected, 0.0D, 0.0D);
- set => Interlocked.Exchange(ref m_measurementsExpected, value);
- }
-
- public long MeasurementsWithError
- {
- get => Interlocked.Read(ref m_measurementsWithError);
- set => Interlocked.Exchange(ref m_measurementsWithError, value);
- }
-
- public long MeasurementsDefined
- {
- get => Interlocked.Read(ref m_measurementsDefined);
- set => Interlocked.Exchange(ref m_measurementsDefined, value);
- }
-
- #endregion
-
- #region [ Methods ]
-
- public override bool Equals(object obj)
- {
- return obj is SubscribedDevice subscribedDevice && Name.Equals(subscribedDevice.Name);
- }
-
- public override int GetHashCode()
- {
- return Name.GetHashCode();
- }
-
- ///
- /// Releases all the resources used by the object.
- ///
- public void Dispose()
- {
- Unregister();
- GC.SuppressFinalize(this);
- }
-
- private void Unregister()
- {
- if (m_disposed)
- return;
-
- try
- {
- StatisticsEngine.Unregister(this);
- }
- finally
- {
- m_disposed = true; // Prevent duplicate dispose.
- }
- }
-
- #endregion
- }
-
- ///
- /// EventArgs implementation for handling user commands.
- ///
- public class UserCommandArgs : EventArgs
- {
- ///
- /// Creates a new instance of the class.
- ///
- /// The code for the user command.
- /// The code for the server's response.
- /// Buffer containing the message from the server.
- /// Index into the buffer used to skip the header.
- /// The length of the message in the buffer, including the header.
- public UserCommandArgs(ServerCommand command, ServerResponse response, byte[] buffer, int startIndex, int length)
- {
- Command = command;
- Response = response;
- Buffer = buffer;
- StartIndex = startIndex;
- Length = length;
- }
-
- ///
- /// Gets the code for the user command.
- ///
- public ServerCommand Command { get; }
-
- ///
- /// Gets the code for the server's response.
- ///
- public ServerResponse Response { get; }
-
- ///
- /// Gets the buffer containing the message from the server.
- ///
- public byte[] Buffer { get; }
-
- ///
- /// Gets the index into the buffer used to skip the header.
- ///
- public int StartIndex { get; }
-
- ///
- /// Gets the length of the message in the buffer, including the header.
- ///
- public int Length { get; }
- }
-
- // Constants
-
- ///
- /// Defines default value for property.
- ///
- public const OperationalModes DefaultOperationalModes = (OperationalModes)((uint)OperationalModes.VersionMask & 2U) | OperationalModes.CompressMetadata | OperationalModes.CompressSignalIndexCache | OperationalModes.ReceiveInternalMetadata;
-
- ///
- /// Defines the default value for the property.
- ///
- public const int DefaultMetadataSynchronizationTimeout = 0;
-
- ///
- /// Defines the default value for the property.
- ///
- public const bool DefaultUseTransactionForMetadata = true;
-
- ///
- /// Default value for property.
- ///
- public const string DefaultLoggingPath = "ConfigurationCache";
-
- ///
- /// Specifies the default value for the property.
- ///
- public const int DefaultAllowedParsingExceptions = 10;
-
- ///
- /// Specifies the default value for the property.
- ///
- public const long DefaultParsingExceptionWindow = 50000000L; // 5 seconds
-
- private const int EvenKey = 0; // Even key/IV index
- private const int OddKey = 1; // Odd key/IV index
- private const int KeyIndex = 0; // Index of cipher key component in keyIV array
- private const int IVIndex = 1; // Index of initialization vector component in keyIV array
-
- private const long MissingCacheWarningInterval = 20000000;
-
- // Events
-
- ///
- /// Occurs when client connection to the data publication server is established.
- ///
- public event EventHandler ConnectionEstablished;
-
- ///
- /// Occurs when client connection to the data publication server is terminated.
- ///
- public event EventHandler ConnectionTerminated;
-
- ///
- /// Occurs when client connection to the data publication server has successfully authenticated.
- ///
- public event EventHandler ConnectionAuthenticated;
-
- ///
- /// Occurs when client receives response from the server.
- ///
- public event EventHandler> ReceivedServerResponse;
-
- ///
- /// Occurs when client receives message from the server in response to a user command.
- ///
- public event EventHandler ReceivedUserCommandResponse;
-
- ///
- /// Occurs when client receives requested meta-data transmitted by data publication server.
- ///
- public event EventHandler> MetaDataReceived;
-
- ///
- /// Occurs when first measurement is transmitted by data publication server.
- ///
- public event EventHandler> DataStartTime;
-
- ///
- /// Indicates that processing for an input adapter (via temporal session) has completed.
- ///
- ///
- /// This event is expected to only be raised when an input adapter has been designed to process
- /// a finite amount of data, e.g., reading a historical range of data during temporal processing.
- ///
- public new event EventHandler> ProcessingComplete;
-
- ///
- /// Occurs when a notification has been received from the .
- ///
- public event EventHandler> NotificationReceived;
-
- ///
- /// Occurs when the server has sent a notification that its configuration has changed, this
- /// can allow subscriber to request updated meta-data if desired.
- ///
- public event EventHandler ServerConfigurationChanged;
-
- ///
- /// Occurs when number of parsing exceptions exceed during .
- ///
- public event EventHandler ExceededParsingExceptionThreshold;
-
- // Fields
- private IClient m_clientCommandChannel;
- private IServer m_serverCommandChannel;
- private UdpClient m_dataChannel;
- private Guid m_activeClientID;
- private string m_connectionID;
- private SharedTimer m_dataStreamMonitor;
- private long m_commandChannelConnectionAttempts;
- private long m_dataChannelConnectionAttempts;
- private volatile SignalIndexCache m_remoteSignalIndexCache;
- private volatile SignalIndexCache[] m_signalIndexCache;
- private readonly object m_signalIndexCacheLock;
- private volatile int m_cacheIndex;
- private volatile long[] m_baseTimeOffsets;
- private volatile int m_timeIndex;
- private volatile byte[][][] m_keyIVs;
- private volatile bool m_subscribed;
- private volatile int m_lastBytesReceived;
- private DateTime m_lastReceivedAt;
- private long m_monitoredBytesReceived;
- private long m_lastMissingCacheWarning;
- private Guid m_nodeID;
- private int m_sttpProtocolID;
- private bool m_includeTime;
- private bool m_metadataRefreshPending;
- private readonly LongSynchronizedOperation m_synchronizeMetadataOperation;
- private volatile DataSet m_receivedMetadata;
- private DataSet m_synchronizedMetadata;
- private DateTime m_lastMetaDataRefreshTime;
- private OperationalModes m_operationalModes;
- private string m_loggingPath;
- private RunTimeLog m_runTimeLog;
- private bool m_dataGapRecoveryEnabled;
- private DataGapRecoverer m_dataGapRecoverer;
- private int m_parsingExceptionCount;
- private long m_lastParsingExceptionTime;
-
- private bool m_supportsTemporalProcessing;
- private volatile Dictionary> m_subscribedDevicesLookup;
- private volatile List> m_statisticsHelpers;
- private readonly LongSynchronizedOperation m_registerStatisticsOperation;
-
- private readonly List m_bufferBlockCache;
- private uint m_expectedBufferBlockSequenceNumber;
-
- private Ticks m_realTime;
- private Ticks m_lastStatisticsHelperUpdate;
- private SharedTimer m_subscribedDevicesTimer;
-
- private long m_totalMeasurementsPerSecond;
- private long m_measurementsPerSecondCount;
- private long m_measurementsInSecond;
- private long m_lastSecondsSinceEpoch;
- private long m_lifetimeTotalLatency;
- private long m_lifetimeMinimumLatency;
- private long m_lifetimeMaximumLatency;
- private long m_lifetimeLatencyMeasurements;
-
- private long m_syncProgressTotalActions;
- private long m_syncProgressActionsCount;
- private long m_syncProgressLastMessage;
-
- private bool m_disposed;
-
- #endregion
-
- #region [ Constructors ]
-
- ///
- /// Creates a new .
- ///
- public DataSubscriber()
- {
- m_registerStatisticsOperation = new LongSynchronizedOperation(HandleDeviceStatisticsRegistration)
- {
- IsBackground = true
- };
-
- m_synchronizeMetadataOperation = new LongSynchronizedOperation(SynchronizeMetadata)
- {
- IsBackground = true
- };
-
- Encoding = Encoding.Unicode;
- m_operationalModes = DefaultOperationalModes;
- MetadataSynchronizationTimeout = DefaultMetadataSynchronizationTimeout;
- AllowedParsingExceptions = DefaultAllowedParsingExceptions;
- ParsingExceptionWindow = DefaultParsingExceptionWindow;
-
- string loggingPath = FilePath.GetDirectoryName(FilePath.GetAbsolutePath(DefaultLoggingPath));
-
- if (Directory.Exists(loggingPath))
- m_loggingPath = loggingPath;
-
- // Default to not using transactions for meta-data on SQL server (helps avoid deadlocks)
- try
- {
- using AdoDataConnection database = new("systemSettings");
- UseTransactionForMetadata = database.DatabaseType != DatabaseType.SQLServer;
- }
- catch
- {
- UseTransactionForMetadata = DefaultUseTransactionForMetadata;
- }
-
- DataLossInterval = 10.0D;
- m_bufferBlockCache = [];
- UseLocalClockAsRealTime = true;
- UseSourcePrefixNames = true;
- m_signalIndexCacheLock = new object();
- }
-
- #endregion
-
- #region [ Properties ]
-
- ///
- /// Gets or sets the security mode used for communications over the command channel.
- ///
- public SecurityMode SecurityMode { get; set; }
-
- ///
- /// Gets or sets logging path to be used to be runtime and outage logs of the subscriber which are required for
- /// automated data recovery.
- ///
- ///
- /// Leave value blank for default path, i.e., installation folder. Can be a fully qualified path or a path that
- /// is relative to the installation folder, e.g., a value of "ConfigurationCache" might resolve to
- /// "C:\Program Files\MyTimeSeriesApp\ConfigurationCache\".
- ///
- public string LoggingPath
- {
- get => m_loggingPath;
- set
- {
- if (!string.IsNullOrWhiteSpace(value))
- {
- string loggingPath = FilePath.GetDirectoryName(FilePath.GetAbsolutePath(value));
-
- if (Directory.Exists(loggingPath))
- value = loggingPath;
- }
-
- m_loggingPath = value;
- }
- }
-
- ///
- /// Gets or sets flag that determines if should attempt to auto-connection to using defined connection settings.
- ///
- public bool AutoConnect { get; set; }
-
- ///
- /// Gets or sets flag that determines if should
- /// automatically request meta-data synchronization and synchronize publisher
- /// meta-data with its own database configuration.
- ///
- public bool AutoSynchronizeMetadata { get; set; }
-
- ///
- /// Gets flag that indicates whether the connection will be persisted
- /// even while the adapter is offline in order to synchronize metadata.
- ///
- public bool PersistConnectionForMetadata =>
- !AutoStart && AutoSynchronizeMetadata && !this.TemporalConstraintIsDefined();
-
- ///
- /// 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 true.
- ///
- public bool UseSourcePrefixNames { get; set; }
-
- ///
- /// Gets or sets requested meta-data filter expressions to be applied by before meta-data is sent.
- ///
- ///
- /// Multiple meta-data filters, such filters for different data tables, should be separated by a semicolon. Specifying fields in the filter
- /// expression that do not exist in the data publisher's current meta-data set could cause filter expressions to not be applied and possibly
- /// result in no meta-data being received for the specified data table.
- ///
- ///
- /// FILTER MeasurementDetail WHERE SignalType <> 'STAT'; FILTER PhasorDetail WHERE Phase = '+'
- ///
- public string MetadataFilters { get; set; }
-
- ///
- /// Gets or sets flag that determines if a subscription is mutual, i.e., bidirectional pub/sub. In this mode one node will
- /// be the owner and set Internal = True and the other node will be the renter and set Internal = False.
- ///
- ///
- ///
- /// This flag is intended to be used in scenarios where a remote subscriber can add new measurements associated with a
- /// source device, e.g., creating new calculated result measurements on a remote machine for load distribution that should
- /// get associated with a device on the local machine, thus becoming part of the local measurement set.
- ///
- ///
- /// For best results, both the owner and renter subscriptions should be reduced to needed measurements, i.e., renter should
- /// only receive measurements needed for remote calculations and owner should only receive new calculated results. Note that
- /// when used with a TLS-style subscription this can be accomplished by using the subscription UI screens that control the
- /// measurement subscribed flag. For internal subscriptions, reduction of metadata and subscribed measurements will
- /// need to be controlled via connection string with metadataFilters and outputMeasurements, respectively.
- ///
- ///
- /// Setting to true will force to true,
- /// to false, and to false.
- ///
- ///
- public bool MutualSubscription { get; set; }
-
- ///
- /// Gets or sets flag that informs publisher if base time-offsets can use millisecond resolution to conserve bandwidth.
- ///
- [Obsolete("SubscriptionInfo object defines this parameter.", false)]
- public bool UseMillisecondResolution { get; set; }
-
- ///
- /// Gets or sets flag that determines if a should be used for command channel connections.
- ///
- public bool UseSimpleTcpClient { get; set; }
-
- ///
- /// Gets flag that determines whether the command channel is connected.
- ///
- public bool CommandChannelConnected => m_clientCommandChannel?.Enabled ?? m_serverCommandChannel?.Enabled ?? false;
-
- ///
- /// Gets total data packet bytes received during this session.
- ///
- public long TotalBytesReceived { get; private set; }
-
- ///
- /// Gets or sets data loss monitoring interval, in seconds. Set to zero to disable monitoring.
- ///
- public double DataLossInterval
- {
- get => m_dataStreamMonitor?.Interval / 1000.0D ?? 0.0D;
- set
- {
- if (value > 0.0D)
- {
- if (m_dataStreamMonitor is null)
- {
- // Create data stream monitoring timer
- m_dataStreamMonitor = Common.TimerScheduler.CreateTimer();
- m_dataStreamMonitor.Elapsed += DataStreamMonitor_Elapsed;
- m_dataStreamMonitor.AutoReset = true;
- m_dataStreamMonitor.Enabled = false;
- }
-
- // Set user specified interval
- m_dataStreamMonitor.Interval = (int)(value * 1000.0D);
- }
- else
- {
- // Disable data monitor
- if (m_dataStreamMonitor is not null)
- {
- m_dataStreamMonitor.Elapsed -= DataStreamMonitor_Elapsed;
- m_dataStreamMonitor.Dispose();
- }
-
- m_dataStreamMonitor = null;
- }
- }
- }
-
- ///
- /// Gets or sets a set of flags that define ways in which the subscriber and publisher communicate.
- ///
- public OperationalModes OperationalModes
- {
- get => m_operationalModes;
- set
- {
- m_operationalModes = value;
- OperationalEncoding operationalEncoding = (OperationalEncoding)(value & OperationalModes.EncodingMask);
- Encoding = GetCharacterEncoding(operationalEncoding);
- }
- }
-
- ///
- /// Gets or sets the operational mode flag to compress meta-data.
- ///
- public bool CompressMetadata
- {
- get => m_operationalModes.HasFlag(OperationalModes.CompressMetadata);
- set
- {
- if (value)
- m_operationalModes |= OperationalModes.CompressMetadata;
- else
- m_operationalModes &= ~OperationalModes.CompressMetadata;
- }
- }
-
- ///
- /// Gets or sets the operational mode flag to compress the signal index cache.
- ///
- public bool CompressSignalIndexCache
- {
- get => m_operationalModes.HasFlag(OperationalModes.CompressSignalIndexCache);
- set
- {
- if (value)
- m_operationalModes |= OperationalModes.CompressSignalIndexCache;
- else
- m_operationalModes &= ~OperationalModes.CompressSignalIndexCache;
- }
- }
-
- ///
- /// Gets or sets the operational mode flag to compress data payloads.
- ///
- public bool CompressPayload
- {
- get => m_operationalModes.HasFlag(OperationalModes.CompressPayloadData);
- set
- {
- if (value)
- m_operationalModes |= OperationalModes.CompressPayloadData;
- else
- m_operationalModes &= ~OperationalModes.CompressPayloadData;
- }
- }
-
- ///
- /// Gets or sets the operational mode flag to receive internal meta-data.
- ///
- public bool ReceiveInternalMetadata
- {
- get => m_operationalModes.HasFlag(OperationalModes.ReceiveInternalMetadata);
- set
- {
- if (value)
- m_operationalModes |= OperationalModes.ReceiveInternalMetadata;
- else
- m_operationalModes &= ~OperationalModes.ReceiveInternalMetadata;
- }
- }
-
- ///
- /// Gets or sets flag that determines if measurements for this data subscription should be marked as "internal", i.e., owned and allowed for proxy.
- ///
- public bool Internal { get; set; }
-
- ///
- /// Gets or sets flag that determines if output measurements should be automatically filtered to only those belonging to the subscriber.
- ///
- public bool FilterOutputMeasurements { get; set; }
-
- ///
- /// Gets or sets flag that determines if identity inserts should be used for SQL Server connections during meta-data synchronization.
- ///
- public bool UseIdentityInsertsForMetadata { get; set; }
-
- ///
- /// Gets or sets flag that determines if CALC signals not defined in metadata should be deleted during synchronizations. Do not set this
- /// value to true if local calculations are being created, and associated with, data arriving from STTP stream.
- ///
- public bool AutoDeleteCalculatedMeasurements { get; set; }
-
- ///
- /// Gets or sets flag that determines if ALARM signals not defined in metadata should be deleted during synchronizations. Do not set this
- /// value to true if local alarms are being created, and associated with, data arriving from STTP stream.
- ///
- public bool AutoDeleteAlarmMeasurements { get; set; }
-
- ///
- /// Gets or sets flag that determines if the data subscriber should attempt to synchronize device metadata as independent devices, i.e.,
- /// not as children of the parent STTP device connection.
- /// Defaults to false.
- ///
- ///
- /// This is useful when using an STTP connection to only synchronize metadata from a publisher, but not to receive data. When enabled,
- /// the device enabled state will not be synchronized upon creation unless is set to
- /// true. In this mode it may be useful to add the original "ConnectionString" field to the publisher's device metadata so it can
- /// be synchronized to the subscriber. To ensure no data is received, the subscriber should be configured with an "OutputMeasurements"
- /// filter in the adapter's connection string that does not include any measurements, e.g.:
- /// outputMeasurements={FILTER ActiveMeasurements WHERE False}
- ///
- public bool SyncIndependentDevices { get; set; }
-
- ///
- /// Gets or sets flag that determines if the data subscriber should automatically enable independently synced devices.
- /// Defaults to false.
- ///
- public bool AutoEnableIndependentlySyncedDevices { get; set; }
-
- ///
- /// Gets or sets flag that determines if statistics engine should be enabled for the data subscriber.
- ///
- public bool BypassStatistics { get; set; }
-
- ///
- /// Gets or sets the operational mode flag to receive external meta-data.
- ///
- public bool ReceiveExternalMetadata
- {
- get => m_operationalModes.HasFlag(OperationalModes.ReceiveExternalMetadata);
- set
- {
- if (value)
- m_operationalModes |= OperationalModes.ReceiveExternalMetadata;
- else
- m_operationalModes &= ~OperationalModes.ReceiveExternalMetadata;
- }
- }
-
- ///
- /// Gets or sets the used by the subscriber and publisher.
- ///
- public OperationalEncoding OperationalEncoding
- {
- get => (OperationalEncoding)(m_operationalModes & OperationalModes.EncodingMask);
- set
- {
- m_operationalModes &= ~OperationalModes.EncodingMask;
- m_operationalModes |= (OperationalModes)value;
- Encoding = GetCharacterEncoding(value);
- }
- }
-
- ///
- /// Gets or sets the used by the subscriber and publisher.
- ///
- public CompressionModes CompressionModes
- {
- get => (CompressionModes)(m_operationalModes & OperationalModes.CompressionModeMask);
- set
- {
- m_operationalModes &= ~OperationalModes.CompressionModeMask;
- m_operationalModes |= (OperationalModes)value;
-
- if (value.HasFlag(CompressionModes.TSSC))
- CompressPayload = true;
- }
- }
-
- ///
- /// Gets or sets the version number of the protocol in use by this subscriber.
- ///
- public int Version
- {
- get => (int)(m_operationalModes & OperationalModes.VersionMask);
- set
- {
- m_operationalModes &= ~OperationalModes.VersionMask;
- m_operationalModes |= (OperationalModes)value;
- }
- }
-
- ///
- /// Gets the character encoding defined by the
- /// of the communications stream.
- ///
- public Encoding Encoding { get; private set; }
-
- ///
- /// Gets flag indicating if this adapter supports real-time processing.
- ///
- ///
- /// Setting this value to false indicates that the adapter should not be enabled unless it exists within a temporal session.
- /// As an example, this flag can be used in a gateway system to set up two separate subscribers: one to the PDC for real-time
- /// data streams and one to the historian for historical data streams. In this scenario, the assumption is that the PDC is
- /// the data source for the historian, implying that only local data is destined for archival.
- ///
- public bool SupportsRealTimeProcessing { get; private set; }
-
- ///
- /// Gets the flag indicating if this adapter supports temporal processing.
- ///
- ///
- ///
- /// Although the data subscriber provisions support for temporal processing by receiving historical data from a remote source,
- /// the adapter opens sockets and does not need to be engaged within an actual temporal , therefore
- /// this method normally returns false to make sure the adapter doesn't get instantiated within a temporal session.
- ///
- ///
- /// Setting this to true means that a subscriber will be initialized within a temporal session to provide historical
- /// data from a remote source - this should only be enabled in cases where (1) there is no locally defined, e.g., in-process,
- /// historian that can already provide historical data for temporal sessions, and (2) a remote subscriber should be allowed
- /// to proxy temporal requests, e.g., those requested for data gap recovery, to an up-stream subscription. This is useful in
- /// cases where a primary data subscriber that has data gap recovery enabled can also allow a remote subscription to proxy in
- /// data gap recovery requests. It is recommended that remote data gap recovery request parameters be (1) either slightly
- /// looser than those of local system to reduce the possibility of duplicated recovery sessions for the same data loss, or
- /// (2) only enabled in the end-most system that most needs the recovered data, like a historian.
- ///
- ///
- public override bool SupportsTemporalProcessing => m_supportsTemporalProcessing;
-
- ///
- /// Gets or sets the desired processing interval, in milliseconds, for the adapter.
- ///
- ///
- /// Except for the values of -1 and 0, this value specifies the desired processing interval for data, i.e., basically a delay,
- /// or timer interval, over which to process data. A value of -1 means to use the default processing interval while a value of
- /// 0 means to process data as fast as possible.
- ///
- public override int ProcessingInterval
- {
- get => base.ProcessingInterval;
- set
- {
- base.ProcessingInterval = value;
-
- // Request server update the processing interval
- SendServerCommand(ServerCommand.UpdateProcessingInterval, BigEndian.GetBytes(value));
- }
- }
-
- ///
- /// Gets or sets the flag that determines whether to request that the subscription be throttled to certain publication interval, see .
- ///
- public bool Throttled { get; set; }
-
- ///
- /// Gets or sets the interval, in seconds, at which data should be published when using a throttled subscription.
- ///
- public double PublishInterval { get; set; } = -1.0D;
-
- ///
- /// Gets or sets the timeout used when executing database queries during meta-data synchronization.
- ///
- public int MetadataSynchronizationTimeout { get; set; }
-
- ///
- /// Gets or sets flag that determines if meta-data synchronization should be performed within a transaction.
- ///
- public bool UseTransactionForMetadata { get; set; }
-
- ///
- /// Gets or sets flag that determines whether to use the local clock when calculating statistics.
- ///
- public bool UseLocalClockAsRealTime { get; set; }
-
- ///
- /// Gets or sets number of parsing exceptions allowed during before connection is reset.
- ///
- public int AllowedParsingExceptions { get; set; }
-
- ///
- /// Gets or sets time duration, in , to monitor parsing exceptions.
- ///
- public Ticks ParsingExceptionWindow { get; set; }
-
- ///
- /// Gets or sets based data source available to this .
- ///
- public override DataSet DataSource
- {
- get => base.DataSource;
- set
- {
- base.DataSource = value;
- m_registerStatisticsOperation.RunOnce();
-
- bool outputMeasurementsUpdated = AutoConnect && UpdateOutputMeasurements();
-
- // For automatic connections, when meta-data refresh is complete, update output measurements to see if any
- // points for subscription have changed after re-application of filter expressions and if so, resubscribe
- if (outputMeasurementsUpdated && Enabled && CommandChannelConnected)
- {
- OnStatusMessage(MessageLevel.Info, "Meta-data received from publisher modified measurement availability, adjusting active subscription...");
-
- // Updating subscription will restart data stream monitor upon successful resubscribe
- if (AutoStart)
- SubscribeToOutputMeasurements(true);
- }
-
- if (m_dataGapRecoverer is not null)
- m_dataGapRecoverer.DataSource = value;
- }
- }
-
- ///
- /// Gets or sets output measurement keys that are requested by other adapters based on what adapter says it can provide.
- ///
- public override MeasurementKey[] RequestedOutputMeasurementKeys
- {
- get => base.RequestedOutputMeasurementKeys;
- set
- {
- MeasurementKey[] oldKeys = base.RequestedOutputMeasurementKeys ?? [];
- MeasurementKey[] newKeys = value ?? [];
- HashSet oldKeySet = [..oldKeys];
-
- base.RequestedOutputMeasurementKeys = value;
-
- if (AutoStart || !Enabled || !CommandChannelConnected || oldKeySet.SetEquals(newKeys))
- return;
-
- OnStatusMessage(MessageLevel.Info, "Requested measurements have changed, adjusting active subscription...");
- SubscribeToOutputMeasurements(true);
- }
- }
-
- ///
- /// Gets or sets output measurements that the will produce, if any.
- ///
- public override IMeasurement[] OutputMeasurements
- {
- get => base.OutputMeasurements;
-
- set
- {
- base.OutputMeasurements = value;
-
- if (m_dataGapRecoverer is not null)
- m_dataGapRecoverer.FilterExpression = this.OutputMeasurementKeys().Select(key => key.SignalID.ToString()).ToDelimitedString(';');
- }
- }
-
- ///
- /// Gets connection info for adapter, if any.
- ///
- public override string ConnectionInfo
- {
- get
- {
- if (m_serverCommandChannel is not null && string.IsNullOrWhiteSpace(m_connectionID))
- {
- Guid clientID = m_serverCommandChannel.ClientIDs.FirstOrDefault();
- IPEndPoint endPoint = GetCommandChannelSocket()?.RemoteEndPoint as IPEndPoint;
- m_connectionID = SubscriberConnection.GetEndPointConnectionID(clientID, endPoint);
- }
-
- string commandChannelServerUri = m_clientCommandChannel?.ServerUri ?? m_connectionID;
- string dataChannelServerUri = m_dataChannel?.ServerUri;
-
- if (string.IsNullOrWhiteSpace(commandChannelServerUri) && string.IsNullOrWhiteSpace(dataChannelServerUri))
- return null;
-
- if (string.IsNullOrWhiteSpace(dataChannelServerUri))
- return commandChannelServerUri;
-
- if (string.IsNullOrWhiteSpace(commandChannelServerUri))
- return dataChannelServerUri;
-
- return $"{commandChannelServerUri} / {dataChannelServerUri}";
- }
- }
-
- ///
- /// Gets the status of this .
- ///
- ///
- /// Derived classes should provide current status information about the adapter for display purposes.
- ///
- public override string Status
- {
- get
- {
- StringBuilder status = new();
-
- status.AppendLine($" Protocol version: {Version}");
- status.AppendLine($" Connected: {CommandChannelConnected}");
- status.AppendLine($" Subscribed: {m_subscribed}");
- status.AppendLine($" Security mode: {SecurityMode}");
- status.AppendLine($" Authenticated: {SecurityMode == SecurityMode.TLS && CommandChannelConnected}");
- status.AppendLine($" Compression modes: {CompressionModes}");
- status.AppendLine($" Mutual subscription: {MutualSubscription}{(MutualSubscription ? $" - System has {(Internal ? "Owner" : "Renter")} Role" : "")}");
- status.AppendLine($" Mark received as internal: {Internal}");
- status.AppendLine($" Receive internal metadata: {ReceiveInternalMetadata}");
- status.AppendLine($" Receive external metadata: {ReceiveExternalMetadata}");
- status.AppendLine($"Filter output measurements: {FilterOutputMeasurements}");
- status.AppendLine($" Synchronize metadata IDs: {UseIdentityInsertsForMetadata}");
- status.AppendLine($" Auto delete CALC signals: {AutoDeleteCalculatedMeasurements}");
- status.AppendLine($" Auto delete ALRM signals: {AutoDeleteAlarmMeasurements}");
- status.AppendLine($" Sync independent devices: {SyncIndependentDevices}");
-
- if (SyncIndependentDevices)
- status.AppendLine($"Independent synced devices: {(AutoEnableIndependentlySyncedDevices? "enabled" : "disabled")} on creation");
-
- status.AppendLine($" Bypass statistics engine: {BypassStatistics}");
- status.AppendLine($" Total bytes received: {TotalBytesReceived:N0}");
- status.AppendLine($" Data packet security: {(SecurityMode == SecurityMode.TLS && m_dataChannel is null ? "Secured via TLS" : m_keyIVs is null ? "Unencrypted" : "AES Encrypted")}");
- status.AppendLine($" Data monitor enabled: {m_dataStreamMonitor is not null && m_dataStreamMonitor.Enabled}");
- status.AppendLine($" Logging path: {FilePath.TrimFileName(m_loggingPath.ToNonNullNorWhiteSpace(FilePath.GetAbsolutePath("")), 51)}");
- status.AppendLine($"No data reconnect interval: {(DataLossInterval > 0.0D ? $"{DataLossInterval:0.000} seconds" : "Disabled")}");
- status.AppendLine($" Data gap recovery mode: {(m_dataGapRecoveryEnabled ? "Enabled" : "Disabled")}");
-
- if (m_dataGapRecoveryEnabled && m_dataGapRecoverer is not null)
- status.Append(m_dataGapRecoverer.Status);
-
- if (m_runTimeLog is not null)
- {
- status.AppendLine();
- status.AppendLine("Run-Time Log Status".CenterText(50));
- status.AppendLine("-------------------".CenterText(50));
- status.Append(m_runTimeLog.Status);
- }
-
- if (m_dataChannel is not null)
- {
- status.AppendLine();
- status.AppendLine("Data Channel Status".CenterText(50));
- status.AppendLine("-------------------".CenterText(50));
- status.Append(m_dataChannel.Status);
- }
-
- if (m_clientCommandChannel is not null)
- {
- status.AppendLine();
- status.AppendLine("Command Channel Status".CenterText(50));
- status.AppendLine("----------------------".CenterText(50));
- status.Append(m_clientCommandChannel.Status);
-
- status.AppendLine($" Using simple TCP client: {UseSimpleTcpClient}");
- }
-
- if (m_serverCommandChannel is not null)
- {
- status.AppendLine();
- status.AppendLine("Command Channel Status".CenterText(50));
- status.AppendLine("----------------------".CenterText(50));
- status.Append(m_serverCommandChannel.Status);
- }
-
- status.Append(base.Status);
-
- return status.ToString();
- }
- }
-
- ///
- /// Gets a flag that determines if this uses an asynchronous connection.
- ///
- protected override bool UseAsyncConnect => true;
-
- ///
- /// Gets or sets reference to data channel, attaching and/or detaching to events as needed.
- ///
- protected UdpClient DataChannel
- {
- get => m_dataChannel;
- set
- {
- if (m_dataChannel is not null)
- {
- // Detach from events on existing data channel reference
- m_dataChannel.ConnectionException -= DataChannelConnectionException;
- m_dataChannel.ConnectionAttempt -= DataChannelConnectionAttempt;
- m_dataChannel.ReceiveData -= DataChannelReceiveData;
- m_dataChannel.ReceiveDataException -= DataChannelReceiveDataException;
-
- if (m_dataChannel != value)
- m_dataChannel.Dispose();
- }
-
- // Assign new data channel reference
- m_dataChannel = value;
-
- if (m_dataChannel is not null)
- {
- // Attach to desired events on new data channel reference
- m_dataChannel.ConnectionException += DataChannelConnectionException;
- m_dataChannel.ConnectionAttempt += DataChannelConnectionAttempt;
- m_dataChannel.ReceiveData += DataChannelReceiveData;
- m_dataChannel.ReceiveDataException += DataChannelReceiveDataException;
- }
- }
- }
-
- ///
- /// Gets or sets reference to command channel, attaching and/or detaching to events as needed.
- ///
- protected IClient ClientCommandChannel
- {
- get => m_clientCommandChannel;
- set
- {
- if (m_clientCommandChannel is not null)
- {
- // Detach from events on existing command channel reference
- m_clientCommandChannel.ConnectionAttempt -= ClientCommandChannelConnectionAttempt;
- m_clientCommandChannel.ConnectionEstablished -= ClientCommandChannelConnectionEstablished;
- m_clientCommandChannel.ConnectionException -= ClientCommandChannelConnectionException;
- m_clientCommandChannel.ConnectionTerminated -= ClientCommandChannelConnectionTerminated;
- m_clientCommandChannel.ReceiveData -= ClientCommandChannelReceiveData;
- m_clientCommandChannel.ReceiveDataException -= ClientCommandChannelReceiveDataException;
- m_clientCommandChannel.SendDataException -= ClientCommandChannelSendDataException;
-
- if (m_clientCommandChannel != value)
- m_clientCommandChannel.Dispose();
- }
-
- // Assign new command channel reference
- m_clientCommandChannel = value;
-
- if (m_clientCommandChannel is not null)
- {
- // Attach to desired events on new command channel reference
- m_clientCommandChannel.ConnectionAttempt += ClientCommandChannelConnectionAttempt;
- m_clientCommandChannel.ConnectionEstablished += ClientCommandChannelConnectionEstablished;
- m_clientCommandChannel.ConnectionException += ClientCommandChannelConnectionException;
- m_clientCommandChannel.ConnectionTerminated += ClientCommandChannelConnectionTerminated;
- m_clientCommandChannel.ReceiveData += ClientCommandChannelReceiveData;
- m_clientCommandChannel.ReceiveDataException += ClientCommandChannelReceiveDataException;
- m_clientCommandChannel.SendDataException += ClientCommandChannelSendDataException;
- }
- }
- }
-
- ///
- /// Gets or sets reference to command channel, attaching and/or detaching to events as needed.
- ///
- ///
- /// This handles reverse connectivity operations.
- ///
- protected IServer ServerCommandChannel
- {
- get => m_serverCommandChannel;
- set
- {
- if (m_serverCommandChannel is not null)
- {
- // Detach from events on existing command channel reference
- m_serverCommandChannel.ClientConnected -= ServerCommandChannelClientConnected;
- m_serverCommandChannel.ClientDisconnected -= ServerCommandChannelClientDisconnected;
- m_serverCommandChannel.ClientConnectingException -= ServerCommandChannelClientConnectingException;
- m_serverCommandChannel.ReceiveClientData -= ServerCommandChannelReceiveClientData;
- m_serverCommandChannel.ReceiveClientDataException -= ServerCommandChannelReceiveClientDataException;
- m_serverCommandChannel.SendClientDataException -= ServerCommandChannelSendClientDataException;
- m_serverCommandChannel.ServerStarted -= ServerCommandChannelServerStarted;
- m_serverCommandChannel.ServerStopped -= ServerCommandChannelServerStopped;
-
- if (m_serverCommandChannel != value)
- m_serverCommandChannel.Dispose();
- }
-
- // Assign new command channel reference
- m_serverCommandChannel = value;
-
- if (m_serverCommandChannel is not null)
- {
- // Attach to desired events on new command channel reference
- m_serverCommandChannel.ClientConnected += ServerCommandChannelClientConnected;
- m_serverCommandChannel.ClientDisconnected += ServerCommandChannelClientDisconnected;
- m_serverCommandChannel.ClientConnectingException += ServerCommandChannelClientConnectingException;
- m_serverCommandChannel.ReceiveClientData += ServerCommandChannelReceiveClientData;
- m_serverCommandChannel.ReceiveClientDataException += ServerCommandChannelReceiveClientDataException;
- m_serverCommandChannel.SendClientDataException += ServerCommandChannelSendClientDataException;
- m_serverCommandChannel.ServerStarted += ServerCommandChannelServerStarted;
- m_serverCommandChannel.ServerStopped += ServerCommandChannelServerStopped;
- }
- }
- }
-
- ///
- /// Gets the total number of measurements processed through this data publisher over the lifetime of the subscriber.
- ///
- public long LifetimeMeasurements { get; private set; }
-
- ///
- /// Gets the minimum value of the measurements per second calculation.
- ///
- public long MinimumMeasurementsPerSecond { get; private set; }
-
- ///
- /// Gets the maximum value of the measurements per second calculation.
- ///
- public long MaximumMeasurementsPerSecond { get; private set; }
-
- ///
- /// Gets the average value of the measurements per second calculation.
- ///
- public long AverageMeasurementsPerSecond => m_measurementsPerSecondCount == 0L ? 0L : m_totalMeasurementsPerSecond / m_measurementsPerSecondCount;
-
- ///
- /// Gets the minimum latency calculated over the full lifetime of the subscriber.
- ///
- public int LifetimeMinimumLatency => (int)Ticks.ToMilliseconds(m_lifetimeMinimumLatency);
-
- ///
- /// Gets the maximum latency calculated over the full lifetime of the subscriber.
- ///
- public int LifetimeMaximumLatency => (int)Ticks.ToMilliseconds(m_lifetimeMaximumLatency);
-
- ///
- /// Gets the average latency calculated over the full lifetime of the subscriber.
- ///
- public int LifetimeAverageLatency => m_lifetimeLatencyMeasurements == 0 ? -1 : (int)Ticks.ToMilliseconds(m_lifetimeTotalLatency / m_lifetimeLatencyMeasurements);
-
- ///
- /// Gets real-time as determined by either the local clock or the latest measurement received.
- ///
- protected Ticks RealTime => UseLocalClockAsRealTime ? DateTime.UtcNow.Ticks : m_realTime;
-
- #endregion
-
- #region [ Methods ]
-
- ///
- /// Releases the unmanaged resources used by the object and optionally releases the managed resources.
- ///
- /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
- protected override void Dispose(bool disposing)
- {
- if (m_disposed)
- return;
-
- try
- {
- if (!disposing)
- return;
-
- DataLossInterval = 0.0D;
- ClientCommandChannel = null;
- ServerCommandChannel = null;
- DataChannel = null;
-
- if (m_dataGapRecoverer is not null)
- {
- m_dataGapRecoverer.RecoveredMeasurements -= DataGapRecoverer_RecoveredMeasurements;
- m_dataGapRecoverer.StatusMessage -= DataGapRecoverer_StatusMessage;
- m_dataGapRecoverer.ProcessException -= DataGapRecoverer_ProcessException;
- m_dataGapRecoverer.Dispose();
- m_dataGapRecoverer = null;
- }
-
- if (m_runTimeLog is not null)
- {
- m_runTimeLog.ProcessException -= RunTimeLog_ProcessException;
- m_runTimeLog.Dispose();
- m_runTimeLog = null;
- }
-
- if (m_subscribedDevicesTimer is not null)
- {
- m_subscribedDevicesTimer.Elapsed -= SubscribedDevicesTimer_Elapsed;
- m_subscribedDevicesTimer.Dispose();
- m_subscribedDevicesTimer = null;
- }
- }
- finally
- {
- m_disposed = true; // Prevent duplicate dispose.
- base.Dispose(disposing); // Call base class Dispose().
- }
- }
-
- ///
- /// Initializes .
- ///
- public override void Initialize()
- {
- base.Initialize();
-
- Dictionary settings = Settings;
-
- // See if user has opted for different operational modes
- if (settings.TryGetValue(nameof(OperationalModes), out string setting) && Enum.TryParse(setting, true, out OperationalModes operationalModes))
- OperationalModes = operationalModes;
-
- // Set the security mode if explicitly defined
- if (!settings.TryGetValue(nameof(SecurityMode), out setting) || !Enum.TryParse(setting, true, out SecurityMode securityMode))
- securityMode = SecurityMode.None;
-
- SecurityMode = securityMode;
-
- // Apply any version override (e.g., to downgrade to older version)
- if (settings.TryGetValue(nameof(Version), out setting) && int.TryParse(setting, out int value) && value < 32)
- Version = value;
-
- // Apply gateway compression mode to operational mode flags
- if (settings.TryGetValue(nameof(CompressionModes), out setting) && Enum.TryParse(setting, true, out CompressionModes compressionModes))
- CompressionModes = compressionModes;
-
- // Check if output measurements should be filtered to only those belonging to the subscriber
- FilterOutputMeasurements = !settings.TryGetValue(nameof(FilterOutputMeasurements), out setting) || setting.ParseBoolean();
-
- // Check if the subscriber supports real-time and historical processing
- SupportsRealTimeProcessing = !settings.TryGetValue(nameof(SupportsRealTimeProcessing), out setting) || setting.ParseBoolean();
- m_supportsTemporalProcessing = settings.TryGetValue(nameof(SupportsTemporalProcessing), out setting) && setting.ParseBoolean();
-
- // Check if measurements for this connection should be marked as "internal" - i.e., owned and allowed for proxy
- if (settings.TryGetValue(nameof(Internal), out setting))
- Internal = setting.ParseBoolean();
-
- // Check if user has explicitly defined the ReceiveInternalMetadata flag
- if (settings.TryGetValue(nameof(ReceiveInternalMetadata), out setting))
- ReceiveInternalMetadata = setting.ParseBoolean();
-
- // Check if user has explicitly defined the ReceiveExternalMetadata flag
- if (settings.TryGetValue(nameof(ReceiveExternalMetadata), out setting))
- ReceiveExternalMetadata = setting.ParseBoolean();
-
- // Check if user has explicitly defined the MutualSubscription flag
- if (settings.TryGetValue(nameof(MutualSubscription), out setting) && setting.ParseBoolean())
- {
- MutualSubscription = true;
- ReceiveInternalMetadata = true;
- ReceiveExternalMetadata = false;
- FilterOutputMeasurements = false;
- }
-
- // Check if user has defined a meta-data synchronization timeout
- if (settings.TryGetValue(nameof(MetadataSynchronizationTimeout), out setting) && int.TryParse(setting, out int metadataSynchronizationTimeout))
- MetadataSynchronizationTimeout = metadataSynchronizationTimeout;
-
- // Check if user has defined a flag for using a transaction during meta-data synchronization
- if (settings.TryGetValue(nameof(UseTransactionForMetadata), out setting))
- UseTransactionForMetadata = setting.ParseBoolean();
-
- // Check if user has defined a flag for using identity inserts during meta-data synchronization
- if (settings.TryGetValue(nameof(UseIdentityInsertsForMetadata), out setting))
- UseIdentityInsertsForMetadata = setting.ParseBoolean();
-
- // Check if user has defined a flag for auto-deletion of CALC signals during meta-data synchronization
- if (settings.TryGetValue(nameof(AutoDeleteCalculatedMeasurements), out setting))
- AutoDeleteCalculatedMeasurements = setting.ParseBoolean();
-
- // Check if user has defined a flag for auto-deletion of ALRM signals during meta-data synchronization
- if (settings.TryGetValue(nameof(AutoDeleteAlarmMeasurements), out setting))
- AutoDeleteAlarmMeasurements = setting.ParseBoolean();
-
- // Check if user has defined a flag for synchronizing independent devices during meta-data synchronization
- if (settings.TryGetValue(nameof(SyncIndependentDevices), out setting))
- SyncIndependentDevices = setting.ParseBoolean();
-
- // Check if user has defined a flag for auto-enabling independently synced devices
- if (settings.TryGetValue(nameof(AutoEnableIndependentlySyncedDevices), out setting))
- AutoEnableIndependentlySyncedDevices = setting.ParseBoolean();
-
- // Check if user wants to request that publisher use millisecond resolution to conserve bandwidth
- #pragma warning disable CS0618 // Type or member is obsolete
- UseMillisecondResolution = !settings.TryGetValue(nameof(UseMillisecondResolution), out setting) || setting.ParseBoolean();
- #pragma warning restore CS0618
-
- // Check if user has defined any meta-data filter expressions
- if (settings.TryGetValue(nameof(MetadataFilters), out setting))
- MetadataFilters = setting;
-
- // Define auto connect setting
- if (settings.TryGetValue(nameof(AutoConnect), out setting))
- {
- AutoConnect = setting.ParseBoolean();
-
- if (AutoConnect)
- AutoSynchronizeMetadata = true;
- }
-
- // Define the maximum allowed exceptions before resetting the connection
- if (settings.TryGetValue(nameof(AllowedParsingExceptions), out setting))
- AllowedParsingExceptions = int.Parse(setting);
-
- // Define the window of time over which parsing exceptions are tolerated
- if (settings.TryGetValue(nameof(ParsingExceptionWindow), out setting))
- ParsingExceptionWindow = Ticks.FromSeconds(double.Parse(setting));
-
- // Check if synchronize meta-data is explicitly enabled or disabled
- if (settings.TryGetValue(nameof(AutoSynchronizeMetadata), out setting))
- AutoSynchronizeMetadata = setting.ParseBoolean();
- else if (settings.TryGetValue("synchronizeMetadata", out setting))
- AutoSynchronizeMetadata = setting.ParseBoolean();
-
- // Determine if source name prefixes should be applied during metadata synchronization
- if (settings.TryGetValue(nameof(UseSourcePrefixNames), out setting))
- UseSourcePrefixNames = setting.ParseBoolean();
-
- // Define data loss interval
- if (settings.TryGetValue(nameof(DataLossInterval), out setting) && double.TryParse(setting, out double interval))
- DataLossInterval = interval;
-
- // Define buffer size
- if (!settings.TryGetValue("bufferSize", out setting) || !int.TryParse(setting, out int bufferSize))
- bufferSize = DataPublisher.DefaultBufferSize;
-
- if (settings.TryGetValue(nameof(UseLocalClockAsRealTime), out setting))
- UseLocalClockAsRealTime = setting.ParseBoolean();
-
- // Handle throttled subscription options
- if (settings.TryGetValue(nameof(Throttled), out setting))
- Throttled = setting.ParseBoolean();
-
- if (settings.TryGetValue(nameof(PublishInterval), out setting) && double.TryParse(setting, out interval))
- PublishInterval = interval;
-
- if (AutoConnect)
- {
- // Connect to local events when automatically engaging connection cycle
- ConnectionAuthenticated += DataSubscriber_ConnectionAuthenticated;
- MetaDataReceived += DataSubscriber_MetaDataReceived;
-
- // Update output measurements to include "subscribed" points
- UpdateOutputMeasurements(true);
- }
- else if (AutoSynchronizeMetadata)
- {
- // Output measurements do not include "subscribed" points,
- // but should still be filtered if applicable
- TryFilterOutputMeasurements();
- }
-
- // Attempt to retrieve any defined command channel settings
- Dictionary commandChannelSettings = settings.TryGetValue("commandChannel", out string commandChannelConnectionString) ? commandChannelConnectionString.ParseKeyValuePairs() : settings;
-
- if (string.IsNullOrWhiteSpace(commandChannelConnectionString))
- commandChannelConnectionString = ConnectionString;
-
- bool serverBasedConnection = !commandChannelSettings.TryGetValue("server", out string server) || string.IsNullOrWhiteSpace(server);
-
- if (settings.TryGetValue(nameof(UseSimpleTcpClient), out setting))
- UseSimpleTcpClient = setting.ParseBoolean();
-
- if (securityMode == SecurityMode.TLS)
- {
- bool checkCertificateRevocation;
-
- if (!commandChannelSettings.TryGetValue("localCertificate", out string localCertificate) || !File.Exists(localCertificate))
- localCertificate = GetLocalCertificate();
-
- if (!commandChannelSettings.TryGetValue("remoteCertificate", out string remoteCertificate) || !RemoteCertificateExists(ref remoteCertificate))
- throw new ArgumentException("The \"remoteCertificate\" setting must be defined and certificate file must exist when using TLS security mode.");
-
- if (!commandChannelSettings.TryGetValue("validPolicyErrors", out setting) || !Enum.TryParse(setting, out SslPolicyErrors validPolicyErrors))
- validPolicyErrors = SslPolicyErrors.None;
-
- if (!commandChannelSettings.TryGetValue("validChainFlags", out setting) || !Enum.TryParse(setting, out X509ChainStatusFlags validChainFlags))
- validChainFlags = X509ChainStatusFlags.NoError;
-
- if (commandChannelSettings.TryGetValue("checkCertificateRevocation", out setting) && !string.IsNullOrWhiteSpace(setting))
- checkCertificateRevocation = setting.ParseBoolean();
- else
- checkCertificateRevocation = true;
-
- SimpleCertificateChecker certificateChecker = new();
-
- // Set up certificate checker
- certificateChecker.TrustedCertificates.Add(new X509Certificate2(FilePath.GetAbsolutePath(remoteCertificate)));
- certificateChecker.ValidPolicyErrors = validPolicyErrors;
- certificateChecker.ValidChainFlags = validChainFlags;
-
- if (serverBasedConnection)
- {
- // Create a new TLS server
- TlsServer commandChannel = new()
- {
- ConfigurationString = commandChannelConnectionString,
- PayloadAware = true,
- PayloadMarker = null,
- PayloadEndianOrder = EndianOrder.BigEndian,
- PersistSettings = false,
- MaxClientConnections = 1, // Subscriber can only serve a single publisher
- CertificateFile = localCertificate,
- CheckCertificateRevocation = checkCertificateRevocation,
- CertificateChecker = certificateChecker,
- RequireClientCertificate = true,
- ReceiveBufferSize = bufferSize,
- SendBufferSize = bufferSize,
- NoDelay = true
- };
-
- // Assign command channel server reference and attach to needed events
- ServerCommandChannel = commandChannel;
- }
- else
- {
- // Create a new TLS client
- TlsClient commandChannel = new()
- {
- ConnectionString = commandChannelConnectionString,
- PayloadAware = true,
- PayloadMarker = null,
- PayloadEndianOrder = EndianOrder.BigEndian,
- PersistSettings = false,
- MaxConnectionAttempts = 1,
- CertificateFile = FilePath.GetAbsolutePath(localCertificate),
- CheckCertificateRevocation = checkCertificateRevocation,
- CertificateChecker = certificateChecker,
- ReceiveBufferSize = bufferSize,
- SendBufferSize = bufferSize,
- NoDelay = true
- };
-
- // Assign command channel client reference and attach to needed events
- ClientCommandChannel = commandChannel;
- }
- }
- else
- {
- if (serverBasedConnection)
- {
- // Create a new TCP server
- TcpServer commandChannel = new()
- {
- ConfigurationString = commandChannelConnectionString,
- PayloadAware = true,
- PayloadMarker = null,
- PayloadEndianOrder = EndianOrder.BigEndian,
- PersistSettings = false,
- MaxClientConnections = 1, // Subscriber can only serve a single publisher
- ReceiveBufferSize = bufferSize,
- SendBufferSize = bufferSize,
- NoDelay = true
- };
-
- // Assign command channel server reference and attach to needed events
- ServerCommandChannel = commandChannel;
- }
- else
- {
- if (UseSimpleTcpClient)
- {
- // Create a new simple TCP client
- TcpSimpleClient commandChannel = new()
- {
- ConnectionString = commandChannelConnectionString,
- PayloadAware = true,
- PayloadMarker = null,
- PayloadEndianOrder = EndianOrder.BigEndian,
- PersistSettings = false,
- MaxConnectionAttempts = 1,
- ReceiveBufferSize = bufferSize,
- SendBufferSize = bufferSize,
- NoDelay = true
- };
-
- // Assign command channel client reference and attach to needed events
- ClientCommandChannel = commandChannel;
- }
- else
- {
- // Create a new TCP client
- TcpClient commandChannel = new()
- {
- ConnectionString = commandChannelConnectionString,
- PayloadAware = true,
- PayloadMarker = null,
- PayloadEndianOrder = EndianOrder.BigEndian,
- PersistSettings = false,
- MaxConnectionAttempts = 1,
- ReceiveBufferSize = bufferSize,
- SendBufferSize = bufferSize,
- NoDelay = true
- };
-
- // Assign command channel client reference and attach to needed events
- ClientCommandChannel = commandChannel;
- }
- }
- }
-
- // Check for simplified compression setup flag
- if (settings.TryGetValue("compression", out setting) && setting.ParseBoolean())
- {
- CompressionModes |= CompressionModes.TSSC | CompressionModes.GZip;
- OperationalModes |= OperationalModes.CompressPayloadData | OperationalModes.CompressMetadata | OperationalModes.CompressSignalIndexCache;
- }
-
- // Get logging path, if any has been defined
- if (settings.TryGetValue(nameof(LoggingPath), out setting))
- {
- setting = FilePath.GetDirectoryName(FilePath.GetAbsolutePath(setting));
-
- if (Directory.Exists(setting))
- m_loggingPath = setting;
- else
- OnStatusMessage(MessageLevel.Info, $"Logging path \"{setting}\" not found, defaulting to \"{FilePath.GetAbsolutePath("")}\"...", flags: MessageFlags.UsageIssue);
- }
-
- // Initialize data gap recovery processing, if requested
- if (settings.TryGetValue("dataGapRecovery", out setting))
- {
- if (m_clientCommandChannel is null)
- {
- m_dataGapRecoveryEnabled = false;
- OnStatusMessage(MessageLevel.Warning, "Cannot use data gap recovery operations with a server-based data subscriber configuration. Data gap recovery will not be enabled.", "Data Subscriber Initialization", MessageFlags.UsageIssue);
- }
- else
- {
- // Make sure setting exists to allow user to by-pass data gap recovery at a configuration level
- ConfigurationFile configFile = ConfigurationFile.Current;
- CategorizedSettingsElementCollection systemSettings = configFile.Settings["systemSettings"];
- CategorizedSettingsElement dataGapRecoveryEnabledSetting = systemSettings["DataGapRecoveryEnabled"];
-
- // See if this node should process phasor source validation
- if (dataGapRecoveryEnabledSetting is null || dataGapRecoveryEnabledSetting.ValueAsBoolean())
- {
- // Example connection string for data gap recovery:
- // dataGapRecovery={enabled=true; recoveryStartDelay=10.0; minimumRecoverySpan=0.0; maximumRecoverySpan=3600.0}
- Dictionary dataGapSettings = setting.ParseKeyValuePairs();
-
- if (dataGapSettings.TryGetValue("enabled", out setting) && setting.ParseBoolean())
- {
- // Remove dataGapRecovery connection setting from command channel connection string, if defined there.
- // This will prevent any recursive data gap recovery operations from being established:
- Dictionary connectionSettings = m_clientCommandChannel.ConnectionString.ParseKeyValuePairs();
- connectionSettings.Remove("dataGapRecovery");
- connectionSettings.Remove("synchronizeMetadata");
- connectionSettings.Remove(nameof(AutoConnect));
- connectionSettings.Remove(nameof(AutoSynchronizeMetadata));
- connectionSettings.Remove(nameof(OutputMeasurements));
- connectionSettings.Remove(nameof(BypassStatistics));
- connectionSettings.Remove(nameof(LoggingPath));
-
- if (dataGapSettings.ContainsKey("server"))
- connectionSettings.Remove("server");
-
- // Note that the data gap recoverer will connect on the same command channel port as
- // the real-time subscriber (TCP only)
- m_dataGapRecoveryEnabled = true;
-
- m_dataGapRecoverer = new DataGapRecoverer
- {
- SourceConnectionName = Name,
- DataSource = DataSource,
- ConnectionString = string.Join("; ", $"autoConnect=false; synchronizeMetadata=false{(string.IsNullOrWhiteSpace(m_loggingPath) ? "" : "; loggingPath=" + m_loggingPath)}", dataGapSettings.JoinKeyValuePairs(), connectionSettings.JoinKeyValuePairs()),
- FilterExpression = this.OutputMeasurementKeys().Select(key => key.SignalID.ToString()).ToDelimitedString(';')
- };
-
- m_dataGapRecoverer.RecoveredMeasurements += DataGapRecoverer_RecoveredMeasurements;
- m_dataGapRecoverer.StatusMessage += DataGapRecoverer_StatusMessage;
- m_dataGapRecoverer.ProcessException += DataGapRecoverer_ProcessException;
- m_dataGapRecoverer.Initialize();
- }
- else
- {
- m_dataGapRecoveryEnabled = false;
- }
- }
- }
- }
- else
- {
- m_dataGapRecoveryEnabled = false;
- }
-
- if (settings.TryGetValue(nameof(BypassStatistics), out setting) && setting.ParseBoolean())
- {
- BypassStatistics = true;
- }
- else
- {
- void statisticsCalculated(object sender, EventArgs args)
- {
- ResetMeasurementsPerSecondCounters();
- }
-
- StatisticsEngine.Register(this, "Subscriber", "SUB");
- StatisticsEngine.Calculated += statisticsCalculated;
- Disposed += (_, _) => StatisticsEngine.Calculated -= statisticsCalculated;
- }
-
- if (PersistConnectionForMetadata)
- {
- m_clientCommandChannel?.ConnectAsync();
- m_serverCommandChannel?.Start();
- }
-
- Initialized = true;
- }
-
- // Initialize (or reinitialize) the output measurements associated with the data subscriber.
- // Returns true if output measurements were updated, otherwise false if they remain the same.
- private bool UpdateOutputMeasurements(bool initialCall = false)
- {
- IMeasurement[] originalOutputMeasurements = OutputMeasurements;
-
- // Reapply output measurements if reinitializing - this way filter expressions and/or sourceIDs
- // will be reapplied. This can be important after a meta-data refresh which may have added new
- // measurements that could now be applicable as desired output measurements.
- if (!initialCall)
- {
- if (Settings.TryGetValue("outputMeasurements", out string setting))
- OutputMeasurements = ParseOutputMeasurements(DataSource, true, setting);
-
- OutputSourceIDs = OutputSourceIDs;
- }
-
- // If active measurements are defined, attempt to defined desired subscription points from there
- if ((SecurityMode == SecurityMode.TLS || FilterOutputMeasurements) && DataSource is not null && DataSource.Tables.Contains("ActiveMeasurements"))
- {
- try
- {
- // Filter to points associated with this subscriber that have been requested for subscription, are enabled and not owned locally
- DataRow[] filteredRows = DataSource.Tables["ActiveMeasurements"].Select("Subscribed <> 0");
- List subscribedMeasurements = [];
-
- foreach (DataRow row in filteredRows)
- {
- // Create a new measurement for the provided field level information
- Measurement measurement = new();
-
- // Parse primary measurement identifier
- Guid signalID = row["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType();
-
- // Set measurement key if defined
- MeasurementKey key = MeasurementKey.LookUpOrCreate(signalID, row["ID"].ToString());
- measurement.Metadata = key.Metadata;
- subscribedMeasurements.Add(measurement);
- }
-
- // Combine subscribed output measurement with any existing output measurement and return unique set
- if (subscribedMeasurements.Count > 0)
- OutputMeasurements = OutputMeasurements is null ?
- subscribedMeasurements.ToArray() :
- subscribedMeasurements.Concat(OutputMeasurements).Distinct().ToArray();
- }
- catch (Exception ex)
- {
- // Errors here may not be catastrophic, this simply limits the auto-assignment of input measurement keys desired for subscription
- OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed to apply subscribed measurements to subscription filter: {ex.Message}", ex));
- }
- }
-
- // Ensure that we are not attempting to subscribe to
- // measurements that we know cannot be published
- TryFilterOutputMeasurements();
-
- // Determine if output measurements have changed
- return originalOutputMeasurements.CompareTo(OutputMeasurements, false) != 0;
- }
-
- // When synchronizing meta-data, the publisher sends meta-data for all possible signals we can subscribe to.
- // Here we check each signal defined in OutputMeasurements to determine whether that signal was defined in
- // the published meta-data rather than blindly attempting to subscribe to all signals.
- private void TryFilterOutputMeasurements()
- {
- if (!FilterOutputMeasurements)
- return;
-
- try
- {
- if (OutputMeasurements is null || DataSource is null || !DataSource.Tables.Contains("ActiveMeasurements"))
- return;
-
- Guid signalID = Guid.Empty;
-
- // Have to use a Convert expression for DeviceID column in Select function
- // here since SQLite doesn't report data types for COALESCE based columns
- IEnumerable measurementIDs = DataSource.Tables["ActiveMeasurements"]
- .Select($"Convert(DeviceID, 'System.String') = '{ID}'")
- .Where(row => Guid.TryParse(row["SignalID"].ToNonNullString(), out signalID))
- .Select(_ => signalID);
-
- HashSet measurementIDSet = [..measurementIDs];
-
- OutputMeasurements = OutputMeasurements.Where(measurement => measurementIDSet.Contains(measurement.ID)).ToArray();
- }
- catch (Exception ex)
- {
- OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Error when filtering output measurements by device ID: {ex.Message}", ex));
- }
- }
-
- ///
- /// Subscribes (or re-subscribes) to a data publisher for a set of data points.
- ///
- /// Configuration object that defines the subscription.
- /// true if subscribe transmission was successful; otherwise false.
- public bool Subscribe(SubscriptionInfo info)
- {
- StringBuilder connectionString = new();
- AssemblyInfo assemblyInfo = new(typeof(DataSubscriber).Assembly);
-
- connectionString.Append($"throttled={info.Throttled};");
- connectionString.Append($"publishInterval={info.PublishInterval};");
- connectionString.Append($"includeTime={info.IncludeTime};");
- connectionString.Append($"lagTime={info.LagTime};");
- connectionString.Append($"leadTime={info.LeadTime};");
- connectionString.Append($"useLocalClockAsRealTime={info.UseLocalClockAsRealTime};");
- connectionString.Append($"processingInterval={info.ProcessingInterval};");
- connectionString.Append($"useMillisecondResolution={info.UseMillisecondResolution};");
- connectionString.Append($"requestNaNValueFilter={info.RequestNaNValueFilter};");
- connectionString.Append($"assemblyInfo={{source=STTP GSF Library ({assemblyInfo.Name}.dll); version={assemblyInfo.Version.Major}.{assemblyInfo.Version.Minor}.{assemblyInfo.Version.Build}; updatedOn={assemblyInfo.BuildDate:yyyy-MM-dd HH:mm:ss} }};");
-
- if (!string.IsNullOrWhiteSpace(info.FilterExpression))
- connectionString.Append($"filterExpression={{{info.FilterExpression}}};");
-
- if (info.UdpDataChannel)
- connectionString.Append($"dataChannel={{localport={info.DataChannelLocalPort}}};");
-
- if (!string.IsNullOrWhiteSpace(info.StartTime))
- connectionString.Append($"startTimeConstraint={info.StartTime};");
-
- if (!string.IsNullOrWhiteSpace(info.StopTime))
- connectionString.Append($"stopTimeConstraint={info.StopTime};");
-
- if (!string.IsNullOrWhiteSpace(info.ConstraintParameters))
- connectionString.Append($"timeConstraintParameters={info.ConstraintParameters};");
-
- if (!string.IsNullOrWhiteSpace(info.ExtraConnectionStringParameters))
- connectionString.Append($"{info.ExtraConnectionStringParameters};");
-
- // Make sure not to monitor for data loss any faster than down-sample time on throttled connections - additionally
- // you will want to make sure data stream monitor is twice lag-time to allow time for initial points to arrive.
- if (info.Throttled && m_dataStreamMonitor is not null && m_dataStreamMonitor.Interval / 1000.0D < info.LagTime)
- m_dataStreamMonitor.Interval = (int)(2.0D * info.LagTime * 1000.0D);
-
- // Set millisecond resolution member variable for compact measurement parsing
- #pragma warning disable 618
- UseMillisecondResolution = info.UseMillisecondResolution;
- #pragma warning restore 618
-
- return Subscribe(info.UseCompactMeasurementFormat, connectionString.ToString());
- }
-
- ///
- /// Subscribes (or re-subscribes) to a data publisher for an unsynchronized set of data points.
- ///
- /// Boolean value that determines if the compact measurement format should be used. Set to false for full fidelity measurement serialization; otherwise set to true for bandwidth conservation.
- /// Boolean value that determines if data should be throttled at a set transmission interval or sent on change.
- /// Filtering expression that defines the measurements that are being subscribed.
- /// Desired UDP return data channel connection string to use for data packet transmission. Set to null to use TCP channel for data transmission.
- /// Boolean value that determines if time is a necessary component in streaming data.
- /// When is true, defines the data transmission speed in seconds (can be sub-second).
- /// When is true, defines the allowed time deviation tolerance to real-time in seconds (can be sub-second).
- /// When is true, defines boolean value that determines whether to use the local clock time as real-time. Set to false to use latest received measurement timestamp as real-time.
- /// Defines a relative or exact start time for the temporal constraint to use for historical playback.
- /// Defines a relative or exact stop time for the temporal constraint to use for historical playback.
- /// Defines any temporal parameters related to the constraint to use for historical playback.
- /// Defines the desired processing interval milliseconds, i.e., historical play back speed, to use when temporal constraints are defined.
- /// Defines the interval, in seconds, at which data should be published when using a throttled subscription.
- /// true if subscribe transmission was successful; otherwise false.
- ///
- ///
- /// When the or temporal processing constraints are defined (i.e., not null), this
- /// specifies the start and stop time over which the subscriber session will process data. Passing in null for the
- /// and specifies the subscriber session will process data in standard, i.e., real-time, operation.
- ///
- ///
- /// Except for the values of -1 and 0, the value specifies the desired historical playback data
- /// processing interval in milliseconds. This is basically a delay, or timer interval, over which to process data. Setting this value
- /// to -1 means to use the default processing interval while setting the value to 0 means to process data as fast as possible.
- ///
- ///
- /// The and parameters can be specified in one of the
- /// following formats:
- ///
- ///
- /// Time Format
- /// Format Description
- ///
- /// -
- /// 12-30-2000 23:59:59.033
- /// Absolute date and time.
- ///
- /// -
- /// *
- /// Evaluates to .
- ///
- /// -
- /// *-20s
- /// Evaluates to 20 seconds before .
- ///
- /// -
- /// *-10m
- /// Evaluates to 10 minutes before .
- ///
- /// -
- /// *-1h
- /// Evaluates to 1 hour before .
- ///
- /// -
- /// *-1d
- /// Evaluates to 1 day before .
- ///
- ///
- ///
- ///
- [Obsolete("Preferred method uses SubscriptionInfo object to subscribe.", false)]
- public virtual bool Subscribe(bool compactFormat, bool throttled, string filterExpression, string dataChannel = null, bool includeTime = true, double lagTime = 10.0D, double leadTime = 5.0D, bool useLocalClockAsRealTime = false, string startTime = null, string stopTime = null, string constraintParameters = null, int processingInterval = -1, double publishInterval = -1.0D)
- {
- int port = 0;
-
- if (!string.IsNullOrWhiteSpace(dataChannel))
- {
- Dictionary settings = dataChannel.ParseKeyValuePairs();
-
- if (settings.TryGetValue("port", out string setting) || settings.TryGetValue("localPort", out setting))
- int.TryParse(setting, out port);
- }
-
- return Subscribe(new SubscriptionInfo
- {
- UseCompactMeasurementFormat = compactFormat,
- Throttled = throttled,
- PublishInterval = publishInterval,
- UdpDataChannel = port > 0,
- DataChannelLocalPort = port,
- FilterExpression = filterExpression,
- IncludeTime = includeTime,
- LagTime = lagTime,
- LeadTime = leadTime,
- UseLocalClockAsRealTime = useLocalClockAsRealTime,
- StartTime = startTime,
- StopTime = stopTime,
- ConstraintParameters = constraintParameters,
- ProcessingInterval = processingInterval,
- UseMillisecondResolution = UseMillisecondResolution // When used as adapter, use configured option
- });
- }
-
- ///
- /// Subscribes (or re-subscribes) to a data publisher for a set of data points.
- ///
- /// Boolean value that determines if the compact measurement format should be used. Set to false for full fidelity measurement serialization; otherwise set to true for bandwidth conservation.
- /// Connection string that defines required and optional parameters for the subscription.
- /// true if subscribe transmission was successful; otherwise false.
- public virtual bool Subscribe(bool compactFormat, string connectionString)
- {
- bool success = false;
-
- if (!string.IsNullOrWhiteSpace(connectionString))
- {
- try
- {
- // Parse connection string to see if it contains a data channel definition
- Dictionary settings = connectionString.ParseKeyValuePairs();
- UdpClient dataChannel = null;
-
- // Track specified time inclusion for later deserialization
- m_includeTime = !settings.TryGetValue("includeTime", out string setting) || setting.ParseBoolean();
-
- settings.TryGetValue("dataChannel", out setting);
-
- if (!string.IsNullOrWhiteSpace(setting))
- {
- if ((CompressionModes & CompressionModes.TSSC) > 0)
- {
- // TSSC is a stateful compression algorithm which will not reliably support UDP
- OnStatusMessage(MessageLevel.Warning, "Cannot use TSSC compression mode with UDP - special compression mode disabled");
-
- // Disable TSSC compression processing
- CompressionModes &= ~CompressionModes.TSSC;
- }
-
- dataChannel = new UdpClient(setting)
- {
- ReceiveBufferSize = ushort.MaxValue,
- MaxConnectionAttempts = -1
- };
-
- dataChannel.ConnectAsync();
- }
-
- // Assign data channel client reference and attach to needed events
- DataChannel = dataChannel;
-
- // Setup subscription packet
- using BlockAllocatedMemoryStream buffer = new();
- DataPacketFlags flags = DataPacketFlags.NoFlags;
-
- if (compactFormat)
- flags |= DataPacketFlags.Compact;
-
- // Write data packet flags into buffer
- buffer.WriteByte((byte)flags);
-
- // Get encoded bytes of connection string
- byte[] bytes = Encoding.GetBytes(connectionString);
-
- // Write encoded connection string length into buffer
- buffer.Write(BigEndian.GetBytes(bytes.Length), 0, 4);
-
- // Encode connection string into buffer
- buffer.Write(bytes, 0, bytes.Length);
-
- // Send subscribe server command with associated command buffer
- success = SendServerCommand(ServerCommand.Subscribe, buffer.ToArray());
- }
- catch (Exception ex)
- {
- OnProcessException(MessageLevel.Error, new InvalidOperationException($"Exception occurred while trying to make publisher subscription: {ex.Message}", ex));
- }
- }
- else
- {
- OnProcessException(MessageLevel.Error, new InvalidOperationException("Cannot make publisher subscription without a connection string."));
- }
-
- return success;
- }
-
- ///
- /// Subscribes to a data publisher based on currently configured adapter settings.
- ///
- /// true if subscribe command was sent successfully; otherwise false.
- [AdapterCommand("Subscribes to data publisher.", "Administrator", "Editor")]
- [EditorBrowsable(EditorBrowsableState.Advanced)] // Method exists for remote console execution
- public virtual bool Subscribe()
- {
- return SubscribeToOutputMeasurements(!m_metadataRefreshPending);
- }
-
- ///
- /// Unsubscribes from a data publisher.
- ///
- /// true if unsubscribe command was sent successfully; otherwise false.
- [AdapterCommand("Unsubscribes from data publisher.", "Administrator", "Editor")]
- public virtual bool Unsubscribe()
- {
- return SendServerCommand(ServerCommand.Unsubscribe);
- }
-
- ///
- /// Returns the measurements signal IDs that were authorized after the last successful subscription request.
- ///
- [AdapterCommand("Gets authorized signal IDs from last subscription request.", "Administrator", "Editor", "Viewer")]
- public virtual Guid[] GetAuthorizedSignalIDs()
- {
- lock (m_signalIndexCacheLock)
- return m_signalIndexCache?[m_cacheIndex] is null ? [] : m_signalIndexCache[m_cacheIndex].AuthorizedSignalIDs;
- }
-
- ///
- /// Returns the measurements signal IDs that were unauthorized after the last successful subscription request.
- ///
- [AdapterCommand("Gets unauthorized signal IDs from last subscription request.", "Administrator", "Editor", "Viewer")]
- public virtual Guid[] GetUnauthorizedSignalIDs()
- {
- lock (m_signalIndexCacheLock)
- return m_signalIndexCache?[m_cacheIndex] is null ? [] : m_signalIndexCache[m_cacheIndex].UnauthorizedSignalIDs;
- }
-
- ///
- /// Resets the counters for the lifetime statistics without interrupting the adapter's operations.
- ///
- [AdapterCommand("Resets the counters for the lifetime statistics without interrupting the adapter's operations.", "Administrator", "Editor")]
- public virtual void ResetLifetimeCounters()
- {
- LifetimeMeasurements = 0L;
- TotalBytesReceived = 0L;
- m_lifetimeTotalLatency = 0L;
- m_lifetimeMinimumLatency = 0L;
- m_lifetimeMaximumLatency = 0L;
- m_lifetimeLatencyMeasurements = 0L;
- }
-
- ///
- /// Initiate a meta-data refresh.
- ///
- [AdapterCommand("Initiates a meta-data refresh.", "Administrator", "Editor")]
- public virtual void RefreshMetadata()
- {
- SendServerCommand(ServerCommand.MetaDataRefresh, MetadataFilters);
- }
-
- ///
- /// Log a data gap for data gap recovery.
- ///
- /// The string representing the data gap.
- [AdapterCommand("Logs a data gap for data gap recovery.", "Administrator", "Editor")]
- public virtual void LogDataGap(string timeString)
- {
- DateTimeOffset end = default;
- string[] split = timeString.Split(';');
-
- if (!m_dataGapRecoveryEnabled)
- throw new InvalidOperationException("Data gap recovery is not enabled.");
-
- if (split.Length != 2)
- throw new FormatException("Invalid format for time string - ex: 2014-03-27 02:10:47.566;2014-03-27 02:10:59.733");
-
- string startTime = split[0];
- string endTime = split[1];
-
- bool parserSuccessful =
- DateTimeOffset.TryParse(startTime, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowInnerWhite, out DateTimeOffset start) &&
- DateTimeOffset.TryParse(endTime, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowInnerWhite, out end);
-
- if (!parserSuccessful)
- throw new FormatException("Invalid format for time string - ex: 2014-03-27 02:10:47.566;2014-03-27 02:10:59.733");
-
- m_dataGapRecoverer?.LogDataGap(start, end, true);
- }
-
- ///
- /// Remove a data gap from data gap recovery.
- ///
- /// The string representing the data gap.
- [AdapterCommand("Removes a data gap from data gap recovery.", "Administrator", "Editor")]
- public virtual string RemoveDataGap(string timeString)
- {
- DateTimeOffset end = default;
- string[] split = timeString.Split(';');
-
- if (!m_dataGapRecoveryEnabled)
- throw new InvalidOperationException("Data gap recovery is not enabled.");
-
- if (split.Length != 2)
- throw new FormatException("Invalid format for time string - ex: 2014-03-27 02:10:47.566;2014-03-27 02:10:59.733");
-
- string startTime = split[0];
- string endTime = split[1];
-
- bool parserSuccessful =
- DateTimeOffset.TryParse(startTime, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowInnerWhite, out DateTimeOffset start) &&
- DateTimeOffset.TryParse(endTime, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowInnerWhite, out end);
-
- if (!parserSuccessful)
- throw new FormatException("Invalid format for time string - ex: 2014-03-27 02:10:47.566;2014-03-27 02:10:59.733");
-
- if (m_dataGapRecoverer?.RemoveDataGap(start, end) ?? false)
- return "Data gap successfully removed.";
-
- return "Data gap not found.";
- }
-
- ///
- /// Displays the contents of the outage log.
- ///
- /// The contents of the outage log.
- [AdapterCommand("Displays data gaps queued for data gap recovery.", "Administrator", "Editor", "Viewer")]
- public virtual string DumpOutageLog()
- {
- if (m_dataGapRecoveryEnabled && m_dataGapRecoverer is not null)
- return Environment.NewLine + m_dataGapRecoverer.DumpOutageLog();
-
- throw new InvalidOperationException("Data gap recovery not enabled");
- }
-
- ///
- /// Gets the status of the temporal used by the data gap recovery module.
- ///
- /// Status of the temporal used by the data gap recovery module.
- [AdapterCommand("Gets the status of the temporal subscription used by the data gap recovery module.", "Administrator", "Editor", "Viewer")]
- public virtual string GetDataGapRecoverySubscriptionStatus()
- {
- if (m_dataGapRecoveryEnabled && m_dataGapRecoverer is not null)
- return m_dataGapRecoverer.TemporalSubscriptionStatus;
-
- return "Data gap recovery not enabled";
- }
-
- ///
- /// Spawn meta-data synchronization.
- ///
- /// to use for synchronization.
- ///
- /// This method makes sure only one meta-data synchronization happens at a time.
- ///
- public void SynchronizeMetadata(DataSet metadata)
- {
- try
- {
- m_receivedMetadata = metadata;
- m_synchronizeMetadataOperation.RunOnceAsync();
- }
- catch (Exception ex)
- {
- // Process exception for logging
- OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed to queue meta-data synchronization: {ex.Message}", ex));
- }
- }
-
- ///
- /// Sends a server command to the publisher connection with associated data.
- ///
- /// to send.
- /// String based command data to send to server.
- /// true if transmission was successful; otherwise false.
- public virtual bool SendServerCommand(ServerCommand commandCode, string message)
- {
- if (!string.IsNullOrWhiteSpace(message))
- {
- using BlockAllocatedMemoryStream buffer = new();
- byte[] bytes = Encoding.GetBytes(message);
-
- buffer.Write(BigEndian.GetBytes(bytes.Length), 0, 4);
- buffer.Write(bytes, 0, bytes.Length);
-
- return SendServerCommand(commandCode, buffer.ToArray());
- }
-
- return SendServerCommand(commandCode);
- }
-
- ///
- /// Sends a server command to the publisher connection.
- ///
- /// to send.
- /// Optional command data to send.
- /// true if transmission was successful; otherwise false.
- public virtual bool SendServerCommand(ServerCommand commandCode, byte[] data = null)
- {
- if (m_clientCommandChannel?.CurrentState == ClientState.Connected || m_serverCommandChannel?.CurrentState == ServerState.Running && m_activeClientID != Guid.Empty)
- {
- try
- {
- using BlockAllocatedMemoryStream commandPacket = new();
- // Write command code into command packet
- commandPacket.WriteByte((byte)commandCode);
-
- // Write command buffer into command packet
- if (data is not null && data.Length > 0)
- commandPacket.Write(data, 0, data.Length);
-
- // Send command packet to publisher
- m_clientCommandChannel?.SendAsync(commandPacket.ToArray(), 0, (int)commandPacket.Length);
- m_serverCommandChannel?.SendToAsync(m_activeClientID, commandPacket.ToArray(), 0, (int)commandPacket.Length);
- m_metadataRefreshPending = commandCode == ServerCommand.MetaDataRefresh;
-
- return true;
- }
- catch (Exception ex)
- {
- OnProcessException(MessageLevel.Error, new InvalidOperationException($"Exception occurred while trying to send server command \"{commandCode}\" to publisher: {ex.Message}", ex));
- }
- }
- else
- OnProcessException(MessageLevel.Error, new InvalidOperationException($"Subscriber is currently unconnected. Cannot send server command \"{commandCode}\" to publisher."));
-
- return false;
- }
-
- ///
- /// Attempts to connect to this .
- ///
- protected override void AttemptConnection()
- {
- if (!this.TemporalConstraintIsDefined() && !SupportsRealTimeProcessing)
- return;
-
- long now = UseLocalClockAsRealTime ? DateTime.UtcNow.Ticks : 0L;
- List> statisticsHelpers = m_statisticsHelpers;
-
- m_registerStatisticsOperation.RunOnceAsync();
- m_expectedBufferBlockSequenceNumber = 0u;
- m_commandChannelConnectionAttempts = 0;
- m_dataChannelConnectionAttempts = 0;
-
- m_subscribed = false;
- m_keyIVs = null;
- TotalBytesReceived = 0L;
- m_monitoredBytesReceived = 0L;
- m_lastBytesReceived = 0;
- m_lastReceivedAt = DateTime.MinValue;
-
- if (!PersistConnectionForMetadata)
- {
- m_clientCommandChannel?.ConnectAsync();
- m_serverCommandChannel?.Start();
- }
- else
- {
- OnConnected();
- }
-
- if (PersistConnectionForMetadata && CommandChannelConnected)
- SubscribeToOutputMeasurements(true);
-
- if (UseLocalClockAsRealTime && m_subscribedDevicesTimer is null)
- {
- m_subscribedDevicesTimer = Common.TimerScheduler.CreateTimer(1000);
- m_subscribedDevicesTimer.Elapsed += SubscribedDevicesTimer_Elapsed;
- }
-
- if (statisticsHelpers is not null)
- {
- m_realTime = 0L;
- m_lastStatisticsHelperUpdate = 0L;
-
- foreach (DeviceStatisticsHelper statisticsHelper in statisticsHelpers)
- statisticsHelper.Reset(now);
- }
-
- if (UseLocalClockAsRealTime)
- m_subscribedDevicesTimer.Start();
- }
-
- ///
- /// Attempts to disconnect from this .
- ///
- protected override void AttemptDisconnection()
- {
- // Unregister device statistics
- m_registerStatisticsOperation.RunOnceAsync();
-
- // Stop data stream monitor
- if (m_dataStreamMonitor is not null)
- m_dataStreamMonitor.Enabled = false;
-
- // Disconnect command channel
- if (!PersistConnectionForMetadata)
- {
- m_clientCommandChannel?.Disconnect();
- m_serverCommandChannel?.Stop();
- }
-
- m_activeClientID = Guid.Empty;
- m_subscribedDevicesTimer?.Stop();
- m_metadataRefreshPending = false;
- }
-
- ///
- /// Gets a short one-line status of this .
- ///
- /// Maximum length of the status message.
- /// Text of the status message.
- public override string GetShortStatus(int maxLength)
- {
- if (m_clientCommandChannel?.CurrentState == ClientState.Connected)
- return $"Subscriber connection has received {ProcessedMeasurements:N0} data points".CenterText(maxLength);
-
- if (m_serverCommandChannel?.CurrentState == ServerState.Running && m_activeClientID != Guid.Empty)
- return $"Subscriber server-based connection has received {ProcessedMeasurements:N0} points".CenterText(maxLength);
-
- return "Subscriber is not connected.".CenterText(maxLength);
- }
-
- ///
- /// Get message from string based response.
- ///
- /// Response buffer.
- /// Start index of response message.
- /// Length of response message.
- /// Decoded response string.
- protected string InterpretResponseMessage(byte[] buffer, int startIndex, int length)
- {
- return Encoding.GetString(buffer, startIndex, length);
- }
-
- // Restarts the subscriber.
- private void Restart()
- {
- try
- {
- base.Start();
- }
- catch (Exception ex)
- {
- OnProcessException(MessageLevel.Warning, ex);
- }
- }
-
- private void ProcessServerResponse(byte[] buffer, int length)
- {
- // Currently this work is done on the async socket completion thread, make sure work to be done is timely and if the response processing
- // is coming in via the command channel and needs to send a command back to the server, it should be done on a separate thread...
- if (buffer is null || length <= 0)
- return;
-
- int startIndex = 0;
-
- while (startIndex < length)
- {
- try
- {
- Dictionary> subscribedDevicesLookup;
- DeviceStatisticsHelper statisticsHelper;
-
- ServerResponse responseCode = (ServerResponse)buffer[startIndex];
- ServerCommand commandCode = (ServerCommand)buffer[startIndex + 1];
- int responseLength = BigEndian.ToInt32(buffer, startIndex + 2);
- int responseIndex = startIndex + DataPublisher.ClientResponseHeaderSize;
- byte[][][] keyIVs;
-
- startIndex = responseIndex + responseLength;
-
- // Disconnect any established UDP data channel upon successful unsubscribe
- if (commandCode == ServerCommand.Unsubscribe && responseCode == ServerResponse.Succeeded)
- DataChannel = null;
-
- if (!IsUserCommand(commandCode))
- OnReceivedServerResponse(responseCode, commandCode);
- else
- OnReceivedUserCommandResponse(commandCode, responseCode, buffer, responseIndex, responseLength);
-
- switch (responseCode)
- {
- case ServerResponse.Succeeded:
- switch (commandCode)
- {
- case ServerCommand.Subscribe:
- OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}");
- m_subscribed = true;
- break;
- case ServerCommand.Unsubscribe:
- OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}");
- m_subscribed = false;
- if (m_dataStreamMonitor is not null)
- m_dataStreamMonitor.Enabled = false;
- break;
- case ServerCommand.RotateCipherKeys:
- OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}");
- break;
- case ServerCommand.MetaDataRefresh:
- OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": latest meta-data received.");
- OnMetaDataReceived(DeserializeMetadata(buffer.BlockCopy(responseIndex, responseLength)));
- m_metadataRefreshPending = false;
- break;
- }
- break;
- case ServerResponse.Failed:
- OnStatusMessage(MessageLevel.Info, $"Failure code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}");
-
- if (commandCode == ServerCommand.MetaDataRefresh)
- m_metadataRefreshPending = false;
- break;
- case ServerResponse.DataPacket:
- {
- long now = DateTime.UtcNow.Ticks;
-
- // Deserialize data packet
- List measurements = [];
- Ticks timestamp = default;
-
- if (TotalBytesReceived == 0)
- {
- // At the point when data is being received, data monitor should be enabled
- if (!(m_dataStreamMonitor?.Enabled ?? false))
- m_dataStreamMonitor.Enabled = true;
-
- // Establish run-time log for subscriber
- if (AutoConnect || m_dataGapRecoveryEnabled)
- {
- if (m_runTimeLog is null)
- {
- m_runTimeLog = new RunTimeLog { FileName = GetLoggingPath($"{Name}_RunTimeLog.txt") };
- m_runTimeLog.ProcessException += RunTimeLog_ProcessException;
- m_runTimeLog.Initialize();
- }
- else
- {
- // Mark the start of any data transmissions
- m_runTimeLog.StartTime = DateTimeOffset.UtcNow;
- m_runTimeLog.Enabled = true;
- }
- }
-
- // The duration between last disconnection and start of data transmissions
- // represents a gap in data - if data gap recovery is enabled, we log
- // this as a gap for recovery:
- if (m_dataGapRecoveryEnabled)
- m_dataGapRecoverer?.LogDataGap(m_runTimeLog.StopTime, DateTimeOffset.UtcNow);
- }
-
- // Track total data packet bytes received from any channel
- TotalBytesReceived += m_lastBytesReceived;
- m_monitoredBytesReceived += m_lastBytesReceived;
-
- // Get data packet flags
- DataPacketFlags flags = (DataPacketFlags)buffer[responseIndex];
- responseIndex++;
-
- SignalIndexCache signalIndexCache;
- bool compactMeasurementFormat = (byte)(flags & DataPacketFlags.Compact) > 0;
- bool compressedPayload = (byte)(flags & DataPacketFlags.Compressed) > 0;
- int cipherIndex = (flags & DataPacketFlags.CipherIndex) > 0 ? 1 : 0;
- int cacheIndex = (flags & DataPacketFlags.CacheIndex) > 0 ? 1 : 0;
- byte[] packet = buffer;
- int packetLength = responseLength - 1;
-
- lock (m_signalIndexCacheLock)
- signalIndexCache = m_signalIndexCache?[cacheIndex];
-
- // Decrypt data packet payload if keys are available
- if (m_keyIVs is not null)
- {
- // Get a local copy of volatile keyIVs reference since this can change at any time
- keyIVs = m_keyIVs;
-
- // Decrypt payload portion of data packet
- packet = Common.SymmetricAlgorithm.Decrypt(packet, responseIndex, packetLength, keyIVs[cipherIndex][0], keyIVs[cipherIndex][1]);
- responseIndex = 0;
- packetLength = packet.Length;
- }
-
- // Deserialize number of measurements that follow
- int count = BigEndian.ToInt32(packet, responseIndex);
- responseIndex += 4;
- packetLength -= 4;
-
- if (compressedPayload)
- {
- if (CompressionModes.HasFlag(CompressionModes.TSSC))
- {
- if (signalIndexCache is not null)
- {
- try
- {
- // Decompress TSSC serialized measurements from payload
- ParseTSSCMeasurements(packet, packetLength, signalIndexCache, ref responseIndex, measurements);
- }
- catch (Exception ex)
- {
- OnProcessException(MessageLevel.Error, new InvalidOperationException($"Decompression failure: (Decoded {measurements.Count} of {count} measurements) - {ex.Message}", ex));
- }
- }
- }
- else
- {
- OnProcessException(MessageLevel.Error, new InvalidOperationException("Decompression failure: Unexpected compression type in use - STTP currently only supports TSSC payload compression"));
- }
- }
- else
- {
- // Deserialize measurements
- for (int i = 0; i < count; i++)
- {
- if (!compactMeasurementFormat)
- {
- // Deserialize full measurement format
- SerializableMeasurement measurement = new(Encoding);
- responseIndex += measurement.ParseBinaryImage(packet, responseIndex, length - responseIndex);
- measurements.Add(measurement);
- }
- else if (signalIndexCache is not null)
- {
- #pragma warning disable 618
- bool useMillisecondResolution = UseMillisecondResolution;
- #pragma warning restore 618
-
- // Deserialize compact measurement format
- CompactMeasurement measurement = new(signalIndexCache, m_includeTime, m_baseTimeOffsets, m_timeIndex, useMillisecondResolution);
- responseIndex += measurement.ParseBinaryImage(packet, responseIndex, length - responseIndex);
-
- // Apply timestamp from frame if not included in transmission
- if (!measurement.IncludeTime)
- measurement.Timestamp = timestamp;
-
- measurements.Add(measurement);
- }
- else if (m_lastMissingCacheWarning + MissingCacheWarningInterval < now)
- {
- // Warning message for missing signal index cache
- if (m_lastMissingCacheWarning != 0L)
- OnStatusMessage(MessageLevel.Error, "Signal index cache has not arrived. No compact measurements can be parsed.");
-
- m_lastMissingCacheWarning = now;
- }
- }
- }
-
- // Calculate statistics
- subscribedDevicesLookup = m_subscribedDevicesLookup;
- statisticsHelper = null;
-
- if (subscribedDevicesLookup is not null)
- {
- IEnumerable, IMeasurement>> deviceGroups = measurements
- .Where(measurement => subscribedDevicesLookup.TryGetValue(measurement.ID, out statisticsHelper))
- .Select(measurement => Tuple.Create(statisticsHelper, measurement))
- .ToList()
- .GroupBy(tuple => tuple.Item1, tuple => tuple.Item2);
-
- foreach (IGrouping, IMeasurement> deviceGroup in deviceGroups)
- {
- statisticsHelper = deviceGroup.Key;
-
- foreach (IGrouping frame in deviceGroup.GroupBy(measurement => measurement.Timestamp))
- {
- // Determine the number of measurements received with valid values
- const MeasurementStateFlags ErrorFlags = MeasurementStateFlags.BadData | MeasurementStateFlags.BadTime | MeasurementStateFlags.SystemError;
-
- static bool hasError(MeasurementStateFlags stateFlags)
- {
- return (stateFlags & ErrorFlags) != MeasurementStateFlags.Normal;
- }
-
- int measurementsReceived = frame.Count(measurement => !double.IsNaN(measurement.Value));
- int measurementsWithError = frame.Count(measurement => !double.IsNaN(measurement.Value) && hasError(measurement.StateFlags));
-
- IMeasurement statusFlags = null;
- IMeasurement frequency = null;
- IMeasurement deltaFrequency = null;
-
- // Attempt to update real-time
- if (!UseLocalClockAsRealTime && frame.Key > m_realTime)
- m_realTime = frame.Key;
-
- // Search the frame for status flags, frequency, and delta frequency
- foreach (IMeasurement measurement in frame)
- {
- if (measurement.ID == statisticsHelper.Device.StatusFlagsID)
- statusFlags = measurement;
- else if (measurement.ID == statisticsHelper.Device.FrequencyID)
- frequency = measurement;
- else if (measurement.ID == statisticsHelper.Device.DeltaFrequencyID)
- deltaFrequency = measurement;
- }
-
- // If we are receiving status flags for this device,
- // count the data quality, time quality, and device errors
- if (statusFlags is not null)
- {
- uint commonStatusFlags = (uint)statusFlags.Value;
-
- if ((commonStatusFlags & (uint)Bits.Bit19) > 0)
- statisticsHelper.Device.DataQualityErrors++;
-
- if ((commonStatusFlags & (uint)Bits.Bit18) > 0)
- statisticsHelper.Device.TimeQualityErrors++;
-
- if ((commonStatusFlags & (uint)Bits.Bit16) > 0)
- statisticsHelper.Device.DeviceErrors++;
-
- measurementsReceived--;
-
- if (hasError(statusFlags.StateFlags))
- measurementsWithError--;
- }
-
- // Zero is not a valid value for frequency.
- // If frequency is zero, invalidate both frequency and delta frequency
- if (frequency is not null)
- {
- if (!this.TemporalConstraintIsDefined())
- statisticsHelper.MarkDeviceTimestamp(frequency.Timestamp);
-
- if (frequency.Value == 0.0D)
- {
- if (deltaFrequency is null || double.IsNaN(deltaFrequency.Value))
- measurementsReceived--;
- else
- measurementsReceived -= 2;
-
- if (hasError(frequency.StateFlags))
- {
- if (deltaFrequency is null || double.IsNaN(deltaFrequency.Value))
- measurementsWithError--;
- else
- measurementsWithError -= 2;
- }
- }
- }
-
- // Track the number of measurements received
- statisticsHelper.AddToMeasurementsReceived(measurementsReceived);
- statisticsHelper.AddToMeasurementsWithError(measurementsWithError);
- }
- }
- }
-
- OnNewMeasurements(measurements);
-
- // Gather statistics on received data
- DateTime timeReceived = RealTime;
-
- if (!UseLocalClockAsRealTime && timeReceived.Ticks - m_lastStatisticsHelperUpdate > Ticks.PerSecond)
- {
- UpdateStatisticsHelpers();
- m_lastStatisticsHelperUpdate = m_realTime;
- }
-
- LifetimeMeasurements += measurements.Count;
- UpdateMeasurementsPerSecond(timeReceived, measurements.Count);
-
- foreach (IMeasurement measurement in measurements)
- {
- long latency = timeReceived.Ticks - (long)measurement.Timestamp;
-
- // Throw out latencies that exceed one hour as invalid
- if (Math.Abs(latency) > Time.SecondsPerHour * Ticks.PerSecond)
- continue;
-
- if (m_lifetimeMinimumLatency > latency || m_lifetimeMinimumLatency == 0)
- m_lifetimeMinimumLatency = latency;
-
- if (m_lifetimeMaximumLatency < latency || m_lifetimeMaximumLatency == 0)
- m_lifetimeMaximumLatency = latency;
-
- m_lifetimeTotalLatency += latency;
- m_lifetimeLatencyMeasurements++;
- }
- break;
- }
- case ServerResponse.BufferBlock:
- {
- // Buffer block received - wrap as a buffer block measurement and expose back to consumer
- uint sequenceNumber = BigEndian.ToUInt32(buffer, responseIndex);
- int bufferCacheIndex = (int)(sequenceNumber - m_expectedBufferBlockSequenceNumber);
- int signalCacheIndex = Version > 1 ? buffer[responseIndex + 4] : 0;
-
- // Check if this buffer block has already been processed (e.g., mistaken retransmission due to timeout)
- if (bufferCacheIndex >= 0 && (bufferCacheIndex >= m_bufferBlockCache.Count || m_bufferBlockCache[bufferCacheIndex] is null))
- {
- // Send confirmation that buffer block is received
- SendServerCommand(ServerCommand.ConfirmBufferBlock, buffer.BlockCopy(responseIndex, 4));
-
- if (Version > 1)
- responseIndex += 1;
-
- // Get measurement key from signal index cache
- int signalIndex = BigEndian.ToInt32(buffer, responseIndex + 4);
-
- SignalIndexCache signalIndexCache;
-
- lock (m_signalIndexCacheLock)
- signalIndexCache = m_signalIndexCache?[signalCacheIndex];
-
- if (signalIndexCache is null || !signalIndexCache.Reference.TryGetValue(signalIndex, out MeasurementKey measurementKey))
- throw new InvalidOperationException($"Failed to find associated signal identification for runtime ID {signalIndex}");
-
- // Skip the sequence number and signal index when creating the buffer block measurement
- BufferBlockMeasurement bufferBlockMeasurement = new(buffer, responseIndex + 8, responseLength - 8)
- {
- Metadata = measurementKey.Metadata
- };
-
- // Determine if this is the next buffer block in the sequence
- if (sequenceNumber == m_expectedBufferBlockSequenceNumber)
- {
- List bufferBlockMeasurements = [];
- int i;
-
- // Add the buffer block measurement to the list of measurements to be published
- bufferBlockMeasurements.Add(bufferBlockMeasurement);
- m_expectedBufferBlockSequenceNumber++;
-
- // Add cached buffer block measurements to the list of measurements to be published
- for (i = 1; i < m_bufferBlockCache.Count; i++)
- {
- if (m_bufferBlockCache[i] is null)
- break;
-
- bufferBlockMeasurements.Add(m_bufferBlockCache[i]);
- m_expectedBufferBlockSequenceNumber++;
- }
-
- // Remove published measurements from the buffer block queue
- if (m_bufferBlockCache.Count > 0)
- m_bufferBlockCache.RemoveRange(0, i);
-
- // Publish measurements
- OnNewMeasurements(bufferBlockMeasurements);
- }
- else
- {
- // Ensure that the list has at least as many
- // elements as it needs to cache this measurement
- for (int i = m_bufferBlockCache.Count; i <= bufferCacheIndex; i++)
- m_bufferBlockCache.Add(null);
-
- // Insert this buffer block into the proper location in the list
- m_bufferBlockCache[bufferCacheIndex] = bufferBlockMeasurement;
- }
- }
-
- LifetimeMeasurements += 1;
- UpdateMeasurementsPerSecond(DateTime.UtcNow, 1);
- break;
- }
- case ServerResponse.DataStartTime:
- // Raise data start time event
- OnDataStartTime(BigEndian.ToInt64(buffer, responseIndex));
- break;
- case ServerResponse.ProcessingComplete:
- // Raise input processing completed event
- OnProcessingComplete(InterpretResponseMessage(buffer, responseIndex, responseLength));
- break;
- case ServerResponse.UpdateSignalIndexCache:
- {
- int version = Version;
- int cacheIndex = 0;
-
- // Get active cache index
- if (version > 1)
- cacheIndex = buffer[responseIndex++];
-
- // Deserialize new signal index cache
- SignalIndexCache remoteSignalIndexCache = DeserializeSignalIndexCache(buffer.BlockCopy(responseIndex, responseLength));
- SignalIndexCache signalIndexCache = new(DataSource, remoteSignalIndexCache);
-
- lock (m_signalIndexCacheLock)
- {
- Interlocked.CompareExchange(ref m_signalIndexCache, new SignalIndexCache[version > 1 ? 2 : 1], null);
-
- m_signalIndexCache[cacheIndex] = signalIndexCache;
- m_remoteSignalIndexCache = remoteSignalIndexCache;
- m_cacheIndex = cacheIndex;
- }
-
- if (version > 1)
- SendServerCommand(ServerCommand.ConfirmSignalIndexCache);
-
- FixExpectedMeasurementCounts();
- break;
- }
- case ServerResponse.UpdateBaseTimes:
- // Get active time index
- m_timeIndex = BigEndian.ToInt32(buffer, responseIndex);
- responseIndex += 4;
-
- // Deserialize new base time offsets
- m_baseTimeOffsets = [BigEndian.ToInt64(buffer, responseIndex), BigEndian.ToInt64(buffer, responseIndex + 8)];
- break;
- case ServerResponse.UpdateCipherKeys:
- // Move past active cipher index (not currently used anywhere else)
- responseIndex++;
-
- // Extract remaining response
- byte[] bytes = buffer.BlockCopy(responseIndex, responseLength - 1);
-
- // Deserialize new cipher keys
- keyIVs = new byte[2][][];
- keyIVs[EvenKey] = new byte[2][];
- keyIVs[OddKey] = new byte[2][];
-
- int index = 0;
-
- // Read even key size
- int bufferLen = BigEndian.ToInt32(bytes, index);
- index = 4;
-
- // Read even key
- keyIVs[EvenKey][KeyIndex] = new byte[bufferLen];
- Buffer.BlockCopy(bytes, index, keyIVs[EvenKey][KeyIndex], 0, bufferLen);
- index += bufferLen;
-
- // Read even initialization vector size
- bufferLen = BigEndian.ToInt32(bytes, index);
- index += 4;
-
- // Read even initialization vector
- keyIVs[EvenKey][IVIndex] = new byte[bufferLen];
- Buffer.BlockCopy(bytes, index, keyIVs[EvenKey][IVIndex], 0, bufferLen);
- index += bufferLen;
-
- // Read odd key size
- bufferLen = BigEndian.ToInt32(bytes, index);
- index += 4;
-
- // Read odd key
- keyIVs[OddKey][KeyIndex] = new byte[bufferLen];
- Buffer.BlockCopy(bytes, index, keyIVs[OddKey][KeyIndex], 0, bufferLen);
- index += bufferLen;
-
- // Read odd initialization vector size
- bufferLen = BigEndian.ToInt32(bytes, index);
- index += 4;
-
- // Read odd initialization vector
- keyIVs[OddKey][IVIndex] = new byte[bufferLen];
- Buffer.BlockCopy(bytes, index, keyIVs[OddKey][IVIndex], 0, bufferLen);
- //index += bufferLen;
-
- // Exchange keys
- m_keyIVs = keyIVs;
-
- OnStatusMessage(MessageLevel.Info, "Successfully established new cipher keys for data packet transmissions.");
- break;
- case ServerResponse.Notify:
- // Skip the 4-byte hash
- string message = Encoding.GetString(buffer, responseIndex + 4, responseLength - 4);
-
- // Display notification
- OnStatusMessage(MessageLevel.Info, $"NOTIFICATION: {message}");
- OnNotificationReceived(message);
-
- // Send confirmation of receipt of the notification
- SendServerCommand(ServerCommand.ConfirmNotification, buffer.BlockCopy(responseIndex, 4));
- break;
- case ServerResponse.ConfigurationChanged:
- OnStatusMessage(MessageLevel.Info, "Received notification from publisher that configuration has changed.");
- OnServerConfigurationChanged();
-
- // Initiate meta-data refresh when publisher configuration has changed - we only do this
- // for automatic connections since API style connections have to manually initiate a
- // meta-data refresh. API style connection should attach to server configuration changed
- // event and request meta-data refresh to complete automated cycle.
- if (AutoConnect && AutoSynchronizeMetadata)
- SendServerCommand(ServerCommand.MetaDataRefresh, MetadataFilters);
- break;
- }
- }
- catch (Exception ex)
- {
- OnProcessException(MessageLevel.Error, new InvalidOperationException($"Failed to process publisher response packet due to exception: {ex.Message}", ex));
- }
- }
- }
-
- private void ParseTSSCMeasurements(byte[] buffer, int packetLength, SignalIndexCache signalIndexCache, ref int responseIndex, List measurements)
- {
- TsscDecoder decoder = signalIndexCache.TsscDecoder;
- bool newDecoder = false;
-
- // Use TSSC compression to decompress measurements
- if (decoder is null)
- {
- decoder = signalIndexCache.TsscDecoder = new TsscDecoder();
- decoder.SequenceNumber = 0;
- newDecoder = true;
- }
-
- if (buffer[responseIndex] != 85)
- throw new Exception($"TSSC version not recognized: {buffer[responseIndex]}");
-
- responseIndex++;
-
- int sequenceNumber = BigEndian.ToUInt16(buffer, responseIndex);
- responseIndex += 2;
-
- if (sequenceNumber == 0)
- {
- if (!newDecoder)
- {
- if (decoder.SequenceNumber > 0)
- OnStatusMessage(MessageLevel.Info, $"TSSC algorithm reset before sequence number: {decoder.SequenceNumber}", "TSSC");
-
- decoder = signalIndexCache.TsscDecoder = new TsscDecoder();
- decoder.SequenceNumber = 0;
- }
- }
-
- if (decoder.SequenceNumber != sequenceNumber)
- {
- OnProcessException(MessageLevel.Warning, new InvalidDataException($"TSSC is out of sequence. Expecting: {decoder.SequenceNumber}, Received: {sequenceNumber} -- resetting connection."));
- Start();
- return;
- }
-
- decoder.SetBuffer(buffer, responseIndex, packetLength - 3);
-
- while (decoder.TryGetMeasurement(out int id, out long time, out uint quality, out float value))
- {
- if (!signalIndexCache.Reference.TryGetValue(id, out MeasurementKey key) || key is null)
- continue;
-
- Measurement measurement = new()
- {
- Metadata = key.Metadata,
- Timestamp = time,
- StateFlags = (MeasurementStateFlags)quality,
- Value = value
- };
-
- measurements.Add(measurement);
- }
-
- decoder.SequenceNumber++;
-
- // Do not increment to 0 on roll-over
- if (decoder.SequenceNumber == 0)
- decoder.SequenceNumber = 1;
- }
-
- private static bool IsUserCommand(ServerCommand command)
- {
- ServerCommand[] userCommands =
- [
- ServerCommand.UserCommand00,
- ServerCommand.UserCommand01,
- ServerCommand.UserCommand02,
- ServerCommand.UserCommand03,
- ServerCommand.UserCommand04,
- ServerCommand.UserCommand05,
- ServerCommand.UserCommand06,
- ServerCommand.UserCommand07,
- ServerCommand.UserCommand08,
- ServerCommand.UserCommand09,
- ServerCommand.UserCommand10,
- ServerCommand.UserCommand11,
- ServerCommand.UserCommand12,
- ServerCommand.UserCommand13,
- ServerCommand.UserCommand14,
- ServerCommand.UserCommand15
- ];
-
- return userCommands.Contains(command);
- }
-
- // Handles auto-connection subscription initialization
- private void StartSubscription()
- {
- SubscribeToOutputMeasurements(!AutoSynchronizeMetadata);
-
- // Initiate meta-data refresh
- if (AutoSynchronizeMetadata && !this.TemporalConstraintIsDefined())
- SendServerCommand(ServerCommand.MetaDataRefresh, MetadataFilters);
- }
-
- private bool SubscribeToOutputMeasurements(bool metaDataRefreshCompleted)
- {
- StringBuilder filterExpression = new();
- string dataChannel = null;
- string startTimeConstraint = null;
- string stopTimeConstraint = null;
- int processingInterval = -1;
-
- // If TCP command channel is defined separately, then base connection string defines data channel
- if (Settings.ContainsKey("commandChannel"))
- dataChannel = ConnectionString;
-
- if (this.TemporalConstraintIsDefined())
- {
- startTimeConstraint = StartTimeConstraint.ToString("yyyy-MM-dd HH:mm:ss.fffffff");
- stopTimeConstraint = StopTimeConstraint.ToString("yyyy-MM-dd HH:mm:ss.fffffff");
- processingInterval = ProcessingInterval;
- }
-
- MeasurementKey[] outputMeasurementKeys = AutoStart
- ? this.OutputMeasurementKeys()
- : RequestedOutputMeasurementKeys;
-
- if (outputMeasurementKeys is not null && outputMeasurementKeys.Length > 0)
- {
- // TODO: Handle "continued" subscribe operations so connection string size can be fixed
- foreach (MeasurementKey measurementKey in outputMeasurementKeys)
- {
- if (filterExpression.Length > 0)
- filterExpression.Append(';');
-
- // Subscribe by associated Guid...
- filterExpression.Append(measurementKey.SignalID);
- }
-
- // Start unsynchronized subscription
- #pragma warning disable 618
- return Subscribe(true, Throttled, filterExpression.ToString(), dataChannel, startTime: startTimeConstraint, stopTime: stopTimeConstraint, processingInterval: processingInterval, publishInterval: PublishInterval);
- #pragma warning restore 618
- }
-
- Unsubscribe();
-
- if (AutoStart && metaDataRefreshCompleted)
- OnStatusMessage(MessageLevel.Error, "No measurements are currently defined for subscription.");
-
- return false;
- }
-
- ///
- /// Handles meta-data synchronization to local system.
- ///
- ///
- /// This function should only be initiated from call to to make
- /// sure only one meta-data synchronization happens at once. Users can override this method to customize
- /// process of meta-data synchronization.
- ///
- protected virtual void SynchronizeMetadata()
- {
- bool dataMonitoringEnabled = false;
-
- // TODO: This function is complex and very closely tied to the current time-series data schema - perhaps it should be moved outside this class and referenced
- // TODO: as a delegate that can be assigned and called to allow other schemas as well. DataPublisher is already very flexible in what data it can deliver.
- try
- {
- DataSet metadata = m_receivedMetadata;
-
- // Only perform database synchronization if meta-data has changed since last update
- if (!SynchronizedMetadataChanged(metadata))
- return;
-
- if (metadata is null)
- {
- OnStatusMessage(MessageLevel.Error, "Meta-data synchronization was not performed, deserialized dataset was empty.");
- return;
- }
-
- // Reset data stream monitor while meta-data synchronization is in progress
- if (m_dataStreamMonitor?.Enabled ?? false)
- {
- m_dataStreamMonitor.Enabled = false;
- dataMonitoringEnabled = true;
- }
-
- // Track total meta-data synchronization process time
- Ticks startTime = DateTime.UtcNow.Ticks;
- DateTime latestUpdateTime = DateTime.MinValue;
-
- // Open the configuration database using settings found in the config file
- using (AdoDataConnection database = new("systemSettings"))
- using (IDbCommand command = database.Connection.CreateCommand())
- {
- IDbTransaction transaction = null;
-
- if (UseTransactionForMetadata)
- transaction = database.Connection.BeginTransaction(database.DefaultIsolationLevel);
-
- try
- {
- if (transaction is not null)
- command.Transaction = transaction;
-
- // Query the actual record ID based on the known run-time ID for this subscriber device
- object sourceID = command.ExecuteScalar($"SELECT SourceID FROM Runtime WHERE ID = {ID} AND SourceTable='Device'", MetadataSynchronizationTimeout);
-
- if (sourceID is null || sourceID == DBNull.Value)
- return;
-
- 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);
-
- // Determine the active node ID - we cache this since this value won't change for the lifetime of this class
- if (m_nodeID == Guid.Empty)
- m_nodeID = Guid.Parse(command.ExecuteScalar($"SELECT NodeID FROM IaonInputAdapter WHERE ID = {(int)ID}", MetadataSynchronizationTimeout).ToString());
-
- // Determine the protocol record auto-inc ID value for STTP - this value is also cached since it shouldn't change for the lifetime of this class
- if (m_sttpProtocolID == 0)
- m_sttpProtocolID = int.Parse(command.ExecuteScalar("SELECT ID FROM Protocol WHERE Acronym='STTP'", MetadataSynchronizationTimeout).ToString());
-
- // Ascertain total number of actions required for all meta-data synchronization so some level feed back can be provided on progress
- InitSyncProgress(metadata.Tables.Cast().Select(dataTable => (long)dataTable.Rows.Count).Sum() + 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 ? $"{Name}!" : "";
- Dictionary deviceIDs = new(StringComparer.OrdinalIgnoreCase);
- DateTime updateTime;
- string deviceAcronym;
- int deviceID;
-
- // Check to see if data for the "DeviceDetail" table was included in the meta-data
- if (metadata.Tables.Contains("DeviceDetail"))
- {
- DataTable deviceDetail = metadata.Tables["DeviceDetail"];
- DataRow[] deviceRows;
-
- // 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, OriginalSource, AccessID, Longitude, Latitude, ContactList, ConnectionString, IsConcentrator, Enabled) " +
- "VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, 0, " + (SyncIndependentDevices ? AutoEnableIndependentlySyncedDevices ? "1" : "0" : "1") + ")",
- "nodeID", "parentID", "historianID", "acronym", "name", "protocolID", "framesPerSecond", "originalSource", "accessID", "longitude", "latitude", "contactList", "connectionString");
-
- // Define SQL statement to update device's guid-based unique ID after insert
- string updateDeviceUniqueIDSql = database.ParameterizedQueryString("UPDATE Device SET UniqueID = {0} WHERE Acronym = {1}", "uniqueID", "acronym");
-
- // Define SQL statement to query if a device can be safely updated
- string deviceParentRestriction = SyncIndependentDevices ? "OriginalSource <> {1}" : "(ParentID <> {1} OR ParentID IS NULL)";
- string deviceIsUpdateableSql = database.ParameterizedQueryString("SELECT COUNT(*) FROM Device WHERE UniqueID = {0} AND " + deviceParentRestriction, "uniqueID", "parentID");
-
- // Define SQL statement to update existing device record
- string updateDeviceSql = database.ParameterizedQueryString("UPDATE Device SET Acronym = {0}, Name = {1}, OriginalSource = {2}, ProtocolID = {3}, FramesPerSecond = {4}, HistorianID = {5}, AccessID = {6}, Longitude = {7}, Latitude = {8}, ContactList = {9} WHERE UniqueID = {10}",
- "acronym", "name", "originalSource", "protocolID", "framesPerSecond", "historianID", "accessID", "longitude", "latitude", "contactList", "uniqueID");
-
- string updateDeviceWithConnectionStringSql = database.ParameterizedQueryString("UPDATE Device SET Acronym = {0}, Name = {1}, OriginalSource = {2}, ProtocolID = {3}, FramesPerSecond = {4}, HistorianID = {5}, AccessID = {6}, Longitude = {7}, Latitude = {8}, ContactList = {9}, ConnectionString = {10} WHERE UniqueID = {11}",
- "acronym", "name", "originalSource", "protocolID", "framesPerSecond", "historianID", "accessID", "longitude", "latitude", "contactList", "connectionString", "uniqueID");
-
- // Define SQL statement to retrieve device's auto-inc ID based on its unique guid-based ID
- string queryDeviceIDSql = database.ParameterizedQueryString("SELECT ID FROM Device WHERE UniqueID = {0}", "uniqueID");
-
- // Define SQL statement to retrieve all unique device ID's for the current parent to check for mismatches
- string queryUniqueDeviceIDsSql = database.ParameterizedQueryString($"SELECT UniqueID FROM Device WHERE {(SyncIndependentDevices ? "OriginalSource" : "ParentID")} = {{0}}", "parentID");
-
- // Define SQL statement to remove device records that no longer exist in the meta-data
- string deleteDeviceSql = database.ParameterizedQueryString("DELETE FROM Device WHERE UniqueID = {0}", "uniqueID");
-
- // Determine which device rows should be synchronized based on operational mode flags
- if (ReceiveInternalMetadata && ReceiveExternalMetadata || MutualSubscription)
- deviceRows = deviceDetail.Select();
- else if (ReceiveInternalMetadata)
- deviceRows = deviceDetail.Select("OriginalSource IS NULL");
- else if (ReceiveExternalMetadata)
- deviceRows = deviceDetail.Select("OriginalSource IS NOT NULL");
- else
- deviceRows = [];
-
- // Check existence of optional meta-data fields
- DataColumnCollection deviceDetailColumns = deviceDetail.Columns;
- bool accessIDFieldExists = deviceDetailColumns.Contains("AccessID");
- bool longitudeFieldExists = deviceDetailColumns.Contains("Longitude");
- bool latitudeFieldExists = deviceDetailColumns.Contains("Latitude");
- bool companyAcronymFieldExists = deviceDetailColumns.Contains("CompanyAcronym");
- bool protocolNameFieldExists = deviceDetailColumns.Contains("ProtocolName");
- bool vendorAcronymFieldExists = deviceDetailColumns.Contains("VendorAcronym");
- bool vendorDeviceNameFieldExists = deviceDetailColumns.Contains("VendorDeviceName");
- bool interconnectionNameFieldExists = deviceDetailColumns.Contains("InterconnectionName");
- bool updatedOnFieldExists = deviceDetailColumns.Contains("UpdatedOn");
- bool connectionStringFieldExists = deviceDetailColumns.Contains("ConnectionString");
- object parentIDValue = SyncIndependentDevices ? parentID.ToString() : parentID;
- int accessID = 0;
-
- List uniqueIDs = deviceRows
- .Select(deviceRow => Guid.Parse(deviceRow.Field