//******************************************************************************************************
// SynchronizeLocalClock.cs - Gbtc
//
// Copyright © 2018, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
// file except in compliance with the License. You may obtain a copy of the License at:
//
// http://opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 12/20/2018 - J. Ritchie Carroll
// Generated original version of source code.
//
//******************************************************************************************************
// ReSharper disable InconsistentNaming
// ReSharper disable PossibleMultipleEnumeration
// ReSharper disable NotAccessedField.Local
// ReSharper disable UnusedMember.Global
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;
using System.Timers;
using GSF;
using GSF.Diagnostics;
using GSF.TimeSeries;
using GSF.TimeSeries.Adapters;
namespace TestingAdapters
{
///
/// Represents a class used to synchronize the local system clock time to the time from input measurements.
///
[Description("System Time Sync: Synchronizes local system clock time to the time from input measurements")]
public class SynchronizeLocalClock : FacileActionAdapterBase
{
#region [ Members ]
// Nested Types
#pragma warning disable 169
#pragma warning disable 414
#pragma warning disable 649
private struct SYSTEMTIME
{
public ushort wYear;
public ushort wMonth;
public ushort wDayOfWeek;
public ushort wDay;
public ushort wHour;
public ushort wMinute;
public ushort wSecond;
public ushort wMilliseconds;
}
#pragma warning restore 169
#pragma warning restore 414
#pragma warning restore 649
// Constants
///
/// Default value for the property.
///
public const int DefaultUpdateFrequency = 1000;
///
/// Default value for the property.
///
public const double DefaultUpdateTolerance = 0.001D; // One millisecond
// Fields
private Timer m_updateTimer;
private long m_skippedUpdates;
private long m_failedUpdates;
private long m_successfulUpdates;
private long m_timerEvents;
private long m_updateTolerance;
private bool m_goodSourceTime;
private Ticks m_latestTime;
private bool m_disposed;
#endregion
#region [ Properties ]
///
/// Gets or sets the update frequency, in milliseconds, for setting the local system clock time.
///
[ConnectionStringParameter]
[DefaultValue(DefaultUpdateFrequency)]
[Description("Defines the update frequency, in milliseconds, for setting the local system clock time.")]
public int UpdateFrequency { get; set; } = DefaultUpdateFrequency;
///
/// Gets or sets the minimum update tolerance, in seconds, as deviation to current local time to check before updating the local clock.
///
[ConnectionStringParameter]
[DefaultValue(DefaultUpdateTolerance)]
[Description("Defines the minimum update tolerance, in seconds, as deviation to current local time to check before updating the local clock.")]
public double UpdateTolerance
{
get => new Ticks(m_updateTolerance).ToSeconds();
set => m_updateTolerance = Ticks.FromSeconds(value);
}
///
/// Gets or sets flag that determines whether or not to use the local clock time as real time.
///
///
/// Use your local system clock as real time only if the time is locally GPS-synchronized,
/// or if the measurement values being sorted were not measured relative to a GPS-synchronized clock.
/// Turn this off if the class is intended to process historical data.
///
[EditorBrowsable(EditorBrowsableState.Never)] // Redeclared to hide property - value should not be changed by user
public override bool UseLocalClockAsRealTime
{
get => base.UseLocalClockAsRealTime;
set => base.UseLocalClockAsRealTime = value;
}
///
/// Gest or sets flag that determines whether to fall back on local clock time as real time when time is unreasonable.
///
///
/// This property is only applicable when is false.
///
[EditorBrowsable(EditorBrowsableState.Never)] // Redeclared to hide property - value should not be changed by user
public override bool FallBackOnLocalClock
{
get => base.FallBackOnLocalClock;
set => base.FallBackOnLocalClock = value;
}
///
/// Gets or sets flag to start tracking the absolute latest received measurement values.
///
///
/// Latest received measurement value will be available via the property.
///
[EditorBrowsable(EditorBrowsableState.Never)] // Redeclared to hide property - value should not be changed by user
public override bool TrackLatestMeasurements
{
get => base.TrackLatestMeasurements;
set => base.TrackLatestMeasurements = value;
}
///
/// Gets the flag indicating if this adapter supports temporal processing.
///
public override bool SupportsTemporalProcessing => false;
///
/// Returns the detailed status of the data input source.
///
///
/// Derived classes should extend status with implementation specific information.
///
public override string Status
{
get
{
StringBuilder status = new StringBuilder();
status.Append(base.Status);
status.AppendLine($"Absolute latest time value: {(m_latestTime.Value > 0L ? $"{m_latestTime:yyyy-MM-dd HH:mm:ss.fff}" : "No time value has been received")}");
status.AppendLine($" Update tolerance: {UpdateTolerance:N3} seconds ({TimeSpan.FromSeconds(UpdateTolerance).TotalMilliseconds:N3} milliseconds)");
status.AppendLine($" Skipped clock updates: {m_skippedUpdates:N0} were within update tolerance");
status.AppendLine($" Failed clock updates: {m_failedUpdates:N0}");
status.AppendLine($" Successful clock updates: {m_successfulUpdates:N0}");
return status.ToString();
}
}
#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;
if (!(m_updateTimer is null))
{
m_updateTimer.Stop();
m_updateTimer.Elapsed -= UpdateTimer_Elapsed;
m_updateTimer.Dispose();
}
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
base.Dispose(disposing); // Call base class Dispose().
}
}
///
/// Initializes .
///
public override void Initialize()
{
Dictionary settings = Settings;
// Force facile adapter to use incoming timestamps as time source, i.e., "RealTime"
settings[nameof(UseLocalClockAsRealTime)] = false.ToString();
settings[nameof(FallBackOnLocalClock)] = true.ToString();
settings[nameof(TrackLatestMeasurements)] = true.ToString();
base.Initialize();
if (settings.TryGetValue(nameof(UpdateFrequency), out string setting))
{
if (int.TryParse(setting, out int updateFrequency) && updateFrequency > 0)
UpdateFrequency = updateFrequency;
}
if (settings.TryGetValue(nameof(UpdateTolerance), out setting) && double.TryParse(setting, out double tolerance))
UpdateTolerance = tolerance;
else
UpdateTolerance = DefaultUpdateTolerance;
m_updateTimer = new Timer(UpdateFrequency);
m_updateTimer.Elapsed += UpdateTimer_Elapsed;
m_updateTimer.Start();
}
///
/// Stops the .
///
public override void Stop()
{
base.Stop();
m_updateTimer.Stop();
OnStatusMessage(MessageLevel.Info, "Clock synchronizations paused...");
}
///
/// Starts the .
///
public override void Start()
{
base.Start();
m_updateTimer.Start();
OnStatusMessage(MessageLevel.Info, "Clock synchronizations resumed...");
}
///
/// Manually synchronizes local to specified date/time.
///
[AdapterCommand("Manually synchronizes local to specified date/time.", "Administrator")]
public void ManualSync(string dateTime)
{
if (!DateTime.TryParse(dateTime, out DateTime targetTime))
{
OnStatusMessage(MessageLevel.Warning, $"Failed to parse \"{dateTime}\" as a valid DateTime value.");
return;
}
try
{
SetSystemTime(targetTime);
m_successfulUpdates++;
OnStatusMessage(MessageLevel.Info, $"Manually updated local clock to {targetTime:yyyy-MM-dd HH:mm:ss.fff}");
}
catch (Exception ex)
{
m_failedUpdates++;
OnStatusMessage(MessageLevel.Error, $"Failed to manually update local clock to {targetTime:yyyy-MM-dd HH:mm:ss.fff}: {ex.Message}");
}
}
///
/// Forces local clock synchronization to latest time value without reasonability considerations.
///
[AdapterCommand("Forces local clock synchronization to absolute latest received time value without reasonability considerations.", "Administrator")]
public void ForceSync()
{
Ticks targetTime = m_latestTime;
if (targetTime.Value > 0L)
{
try
{
SetSystemTime(targetTime);
m_successfulUpdates++;
OnStatusMessage(MessageLevel.Info, $"Forced local clock to {targetTime:yyyy-MM-dd HH:mm:ss.fff}");
}
catch (Exception ex)
{
m_failedUpdates++;
OnStatusMessage(MessageLevel.Error, $"Failed to force local clock to {targetTime:yyyy-MM-dd HH:mm:ss.fff}: {ex.Message}");
}
}
else
{
int count = InputMeasurementKeys.Length;
if (count > 0)
OnStatusMessage(MessageLevel.Warning, $"Cannot force clock update - no time has been received from any of the {count:N0} defined input sources.");
else
OnStatusMessage(MessageLevel.Warning, "Cannot force clock update - no input sources have been defined.");
}
}
///
/// Gets a short one-line status of this .
///
/// Maximum number of available characters for display.
/// A short one-line summary of the current status of this .
public override string GetShortStatus(int maxLength) => $"Updated clock {m_successfulUpdates:N0} times out of {m_timerEvents:N0} checks so far...".CenterText(maxLength);
///
/// Queues a collection of measurements for processing.
///
/// Measurements to queue for processing.
public override void QueueMeasurementsForProcessing(IEnumerable measurements)
{
List measurementsWithGoodTime = new List();
foreach (IMeasurement measurement in measurements)
{
if (measurement.TimestampQualityIsGood())
measurementsWithGoodTime.Add(measurement);
// Track absolute latest time, regardless of resonability or quality
if (measurement.Timestamp.Value > m_latestTime.Value)
m_latestTime = measurement.Timestamp;
}
if (measurementsWithGoodTime.Count == 0)
{
base.QueueMeasurementsForProcessing(measurements);
m_goodSourceTime = false;
}
else
{
base.QueueMeasurementsForProcessing(measurementsWithGoodTime);
m_goodSourceTime = true;
}
}
private void UpdateTimer_Elapsed(object sender, ElapsedEventArgs e)
{
try
{
m_timerEvents++;
Ticks newSystemTime = RealTime;
if (newSystemTime <= 0L)
return;
if (Math.Abs(newSystemTime.Value - DateTime.UtcNow.Ticks) < m_updateTolerance)
{
m_skippedUpdates++;
return;
}
SetSystemTime(RealTime);
m_successfulUpdates++;
if (!m_goodSourceTime)
OnStatusMessage(MessageLevel.Warning, "WARNING: Clock set with measurement that has bad quality -- increase measurement sources.");
}
catch (Exception ex)
{
m_failedUpdates++;
OnProcessException(MessageLevel.Error, ex);
}
}
#endregion
#region [ Static ]
// Static Methods
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetSystemTime(ref SYSTEMTIME lpSystemTime);
private static void SetSystemTime(DateTime newSystemTime)
{
SYSTEMTIME systime = new SYSTEMTIME
{
wYear = (ushort)newSystemTime.Year,
wMonth = (ushort)newSystemTime.Month,
wDay = (ushort)newSystemTime.Day,
wHour = (ushort)newSystemTime.Hour,
wMinute = (ushort)newSystemTime.Minute,
wSecond = (ushort)newSystemTime.Second,
wMilliseconds = (ushort)newSystemTime.Millisecond
};
if (!SetSystemTime(ref systime))
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to set local system time, verify service account has SE_SYSTEMTIME_NAME privilege.");
}
#endregion
}
}