//******************************************************************************************************
// FileClient.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:
// ----------------------------------------------------------------------------------------------------
// 07/24/2006 - Pinal C. Patel
// Original version of source code generated.
// 09/06/2006 - J. Ritchie Carroll
// Added bypass optimizations for high-speed file data access.
// 09/29/2008 - J. Ritchie Carroll
// Converted to C#.
// 07/08/2009 - J. Ritchie Carroll
// Added WaitHandle return value from asynchronous connection.
// 09/14/2009 - Stephen C. Wills
// Added new header and license agreement.
// 06/23/2010 - Stephen C. Wills
// Modified to use the absolute file path.
// 11/29/2010 - Pinal C. Patel
// Corrected the implementation of ConnectAsync() method.
// 12/04/2010 - Pinal C. Patel
// Removed locking around m_fileClient.Provider since it was unnecessary.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Threading;
using GSF.Configuration;
using GSF.Diagnostics;
using GSF.IO;
using GSF.Threading;
namespace GSF.Communication
{
///
/// Represents a communication client based on .
///
///
/// This example shows how to use for writing data to a file:
///
/// using System;
/// using GSF.Communication;
///
/// class Program
/// {
/// static FileClient s_client;
///
/// static void Main(string[] args)
/// {
/// // Initialize the client.
/// s_client = new FileClient(@"File=c:\File.txt");
/// s_client.Initialize();
/// // Register event handlers.
/// s_client.ConnectionAttempt += s_client_ConnectionAttempt;
/// s_client.ConnectionEstablished += s_client_ConnectionEstablished;
/// s_client.ConnectionTerminated += s_client_ConnectionTerminated;
/// s_client.SendDataComplete += s_client_SendDataComplete;
/// // Connect the client.
/// s_client.Connect();
///
/// // Write user input to the file.
/// string input;
/// while (string.Compare(input = Console.ReadLine(), "Exit", true) != 0)
/// {
/// s_client.Send(input + "\r\n");
/// }
///
/// // Disconnect the client on shutdown.
/// s_client.Dispose();
/// }
///
/// static void s_client_ConnectionAttempt(object sender, EventArgs e)
/// {
/// Console.WriteLine("Client is connecting to file.");
/// }
///
/// static void s_client_ConnectionEstablished(object sender, EventArgs e)
/// {
/// Console.WriteLine("Client connected to file.");
/// }
///
/// static void s_client_ConnectionTerminated(object sender, EventArgs e)
/// {
/// Console.WriteLine("Client disconnected from file.");
/// }
///
/// static void s_client_SendDataComplete(object sender, EventArgs e)
/// {
/// Console.WriteLine(string.Format("Sent data - {0}", s_client.TextEncoding.GetString(s_client.Client.SendBuffer)));
/// }
/// }
///
/// This example shows how to use for reading data to a file:
///
/// using System;
/// using GSF;
/// using GSF.Communication;
///
/// class Program
/// {
/// static FileClient s_client;
///
/// static void Main(string[] args)
/// {
/// // Initialize the client.
/// s_client = new FileClient(@"File=c:\File.txt");
/// s_client.Initialize();
/// // Register event handlers.
/// s_client.ConnectionAttempt += s_client_ConnectionAttempt;
/// s_client.ConnectionEstablished += s_client_ConnectionEstablished;
/// s_client.ConnectionTerminated += s_client_ConnectionTerminated;
/// s_client.ReceiveDataComplete += s_client_ReceiveDataComplete;
/// // Connect the client.
/// s_client.Connect();
///
/// // Wait for client to read data.
/// Console.ReadLine();
///
/// // Disconnect the client on shutdown.
/// s_client.Dispose();
/// }
///
/// static void s_client_ConnectionAttempt(object sender, EventArgs e)
/// {
/// Console.WriteLine("Client is connecting to file.");
/// }
///
/// static void s_client_ConnectionEstablished(object sender, EventArgs e)
/// {
/// Console.WriteLine("Client connected to file.");
/// }
///
/// static void s_client_ConnectionTerminated(object sender, EventArgs e)
/// {
/// Console.WriteLine("Client disconnected from file.");
/// }
///
/// static void s_client_ReceiveDataComplete(object sender, EventArgs<byte[], int> e)
/// {
/// Console.WriteLine(string.Format("Received data - {0}", s_client.TextEncoding.GetString(e.Argument1, 0, e.Argument2)));
/// }
/// }
///
///
public class FileClient : ClientBase
{
#region [ Members ]
// Constants
///
/// Specifies the default value for the property.
///
public const bool DefaultAutoRepeat = false;
///
/// Specifies the default value for the property.
///
public const bool DefaultReceiveOnDemand = false;
///
/// Specifies the default value for the property.
///
public const int DefaultReceiveInterval = -1;
///
/// Specifies the default value for the property.
///
public const long DefaultStartingOffset = 0;
///
/// Specifies the default value for the property.
///
public const FileMode DefaultFileOpenMode = FileMode.OpenOrCreate;
///
/// Specifies the default value for the property.
///
public const FileShare DefaultFileShareMode = FileShare.ReadWrite;
///
/// Specifies the default value for the property.
///
public const FileAccess DefaultFileAccessMode = FileAccess.ReadWrite;
///
/// Specifies the default value for the property.
///
public const bool DefaultDisconnectAtEOF = false;
///
/// Specifies the default value for the property.
///
public const string DefaultConnectionString = "File=DataFile.txt";
// Events
///
/// Occurs when the end of a file has been reached.
///
[Category("Data")]
[Description("Occurs when the end of a file has been reached.")]
public event EventHandler EndOfFile;
// Fields
private bool m_autoRepeat;
private bool m_receiveOnDemand;
private int m_receiveInterval;
private long m_startingOffset;
private FileAccess m_fileAccessMode;
private readonly TransportProvider m_fileClient;
private Dictionary m_connectData;
private readonly SharedTimer m_receiveDataTimer;
private ManualResetEvent m_connectionHandle;
private Thread m_connectionThread;
private bool m_disposed;
#endregion
#region [ Constructors ]
///
/// Initializes a new instance of the class.
///
public FileClient() : this(DefaultConnectionString)
{
}
///
/// Initializes a new instance of the class.
///
/// Connect string of the . See for format.
public FileClient(string connectString) : base(TransportProtocol.File, connectString)
{
m_autoRepeat = DefaultAutoRepeat;
m_receiveOnDemand = DefaultReceiveOnDemand;
m_receiveInterval = DefaultReceiveInterval;
m_startingOffset = DefaultStartingOffset;
FileOpenMode = DefaultFileOpenMode;
FileShareMode = DefaultFileShareMode;
m_fileAccessMode = DefaultFileAccessMode;
m_fileClient = new TransportProvider();
m_receiveDataTimer = s_timerScheduler.CreateTimer();
m_receiveDataTimer.Elapsed += m_receiveDataTimer_Elapsed;
}
///
/// Initializes a new instance of the class.
///
/// object that contains the .
public FileClient(IContainer container) : this() => container?.Add(this);
#endregion
#region [ Properties ]
///
/// Gets or sets a boolean value that indicates whether receiving (reading) of data is to be repeated endlessly.
///
/// is enabled when is
[Category("Data")]
[DefaultValue(DefaultAutoRepeat)]
[Description("Indicates whether receiving (reading) of data is to be repeated endlessly.")]
public bool AutoRepeat
{
get => m_autoRepeat;
set
{
if (value && m_fileAccessMode == FileAccess.ReadWrite)
throw new InvalidOperationException("AutoRepeat cannot be enabled when FileAccessMode is FileAccess.ReadWrite");
m_autoRepeat = value;
}
}
///
/// Gets or sets a boolean value that indicates whether receiving (reading) of data will be initiated manually by calling .
///
///
/// will be set to -1 when is enabled.
///
[Category("Data")]
[DefaultValue(DefaultReceiveOnDemand)]
[Description("Indicates whether receiving (reading) of data will be initiated manually by calling ReceiveData().")]
public bool ReceiveOnDemand
{
get => m_receiveOnDemand;
set
{
m_receiveOnDemand = value;
// We'll disable receiving data at a set interval if user wants to receive data on demand.
if (m_receiveOnDemand)
m_receiveInterval = -1;
}
}
///
/// Gets or sets the number of milliseconds to pause before receiving (reading) the next available set of data.
///
///
/// Set = -1 to receive (read) data continuously without pausing.
///
[Category("Data")]
[DefaultValue(DefaultReceiveInterval)]
[Description("The number of milliseconds to pause before receiving (reading) the next available set of data. Set ReceiveInterval = -1 to receive data continuously without pausing.")]
public double ReceiveInterval
{
get => m_receiveInterval;
set => m_receiveInterval = value < 1.0D ? - 1 : (int)value;
}
///
/// Gets or sets the starting point relative to the beginning of the file from where the data is to be received (read).
///
/// The value being assigned is not a positive number.
[Category("File")]
[DefaultValue(DefaultStartingOffset)]
[Description("The starting point relative to the beginning of the file from where the data is to be received (read).")]
public long StartingOffset
{
get => m_startingOffset;
set
{
if (value < 0)
throw new ArgumentException("Value must be positive");
m_startingOffset = value;
}
}
///
/// Gets or sets the value to be used when opening the file.
///
[Category("File")]
[DefaultValue(DefaultFileOpenMode)]
[Description("The System.IO.FileMode value to be used when opening the file.")]
public FileMode FileOpenMode { get; set; }
///
/// Gets or set the value to be used when opening the file.
///
[Category("File")]
[DefaultValue(DefaultFileShareMode)]
[Description("The System.IO.FileShare value to be used when opening the file.")]
public FileShare FileShareMode { get; set; }
///
/// Gets or sets the value to be used when opening the file.
///
/// is set to when is enabled.
[Category("File")]
[DefaultValue(DefaultFileAccessMode)]
[Description("The System.IO.FileAccess value to be used when opening the file.")]
public FileAccess FileAccessMode
{
get => m_fileAccessMode;
set
{
if (value == FileAccess.ReadWrite && m_autoRepeat)
throw new InvalidOperationException("FileAccessMode cannot be set to FileAccess.ReadWrite when AutoRepeat is enabled");
m_fileAccessMode = value;
}
}
///
/// Gets or sets flag that determines if client should disconnect when end of file has been reached.
///
[Category("File")]
[DefaultValue(DefaultDisconnectAtEOF)]
[Description("The flag that determines if client should disconnect when end of file has been reached.")]
public bool DisconnectAtEOF { get; set; } = DefaultDisconnectAtEOF;
///
/// Gets the object for the .
///
[Browsable(false)]
public FileStream Client => m_fileClient.Provider;
///
/// Gets the server URI of the .
///
[Browsable(false)]
public override string ServerUri => $"{TransportProtocol}://{m_connectData["file"]}".ToLower();
#endregion
#region [ Methods ]
///
/// Reads a number of bytes from the current received data buffer and writes those bytes into a byte array at the specified offset.
///
/// Destination buffer used to hold copied bytes.
/// 0-based starting index into destination to begin writing data.
/// The number of bytes to read from current received data buffer and write into .
/// The number of bytes read.
///
/// This function should only be called from within the event handler. Calling this method outside
/// this event will have unexpected results.
///
/// No received data buffer has been defined to read.
/// is null.
///
/// or is less than 0 -or-
/// and will exceed length.
///
public override int Read(byte[] buffer, int startIndex, int length)
{
buffer.ValidateParameters(startIndex, length);
if (m_fileClient.ReceiveBuffer == null)
throw new InvalidOperationException("No received data buffer has been defined to read.");
int sourceLength = m_fileClient.BytesReceived - ReadIndex;
int readBytes = length > sourceLength ? sourceLength : length;
Buffer.BlockCopy(m_fileClient.ReceiveBuffer, ReadIndex, buffer, startIndex, readBytes);
// Update read index for next call
ReadIndex += readBytes;
if (ReadIndex >= m_fileClient.BytesReceived)
ReadIndex = 0;
return readBytes;
}
///
/// Reads next data buffer from the .
///
/// is called when is not connected.
/// is called when is disabled.
public void ReadNextBuffer()
{
if (m_receiveOnDemand)
{
if (CurrentState == ClientState.Connected)
ReadData();
else
throw new InvalidOperationException("Client is currently not connected");
}
else
{
throw new InvalidOperationException("ReadNextBuffer() cannot be used when ReceiveOnDemand is disabled");
}
}
///
/// Disconnects the from the .
///
public override void Disconnect()
{
if (CurrentState == ClientState.Disconnected)
return;
m_fileClient.Reset();
m_receiveDataTimer.Stop();
m_connectionThread?.Abort();
OnConnectionTerminated();
}
///
/// Connects the to the asynchronously.
///
/// Attempt is made to connect the when it is not disconnected.
/// for the asynchronous operation.
public override WaitHandle ConnectAsync()
{
m_connectionHandle = (ManualResetEvent)base.ConnectAsync();
m_fileClient.SetReceiveBuffer(ReceiveBufferSize);
m_connectionThread = new Thread(OpenFile)
{
IsBackground = true
};
m_connectionThread.Start();
return m_connectionHandle;
}
///
/// Saves settings to the config file if the property is set to true.
///
public override void SaveSettings()
{
base.SaveSettings();
if (!PersistSettings)
return;
// Save settings under the specified category.
ConfigurationFile config = ConfigurationFile.Current;
CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory];
settings[nameof(AutoRepeat), true].Update(m_autoRepeat);
settings[nameof(ReceiveOnDemand), true].Update(m_receiveOnDemand);
settings[nameof(ReceiveInterval), true].Update(m_receiveInterval);
settings[nameof(StartingOffset), true].Update(m_startingOffset);
settings[nameof(FileOpenMode), true].Update(FileOpenMode);
settings[nameof(FileShareMode), true].Update(FileShareMode);
settings[nameof(FileAccessMode), true].Update(m_fileAccessMode);
settings[nameof(DisconnectAtEOF), true].Update(DisconnectAtEOF);
config.Save();
}
///
/// Loads saved settings from the config file if the property is set to true.
///
public override void LoadSettings()
{
base.LoadSettings();
if (!PersistSettings)
return;
// Load settings from the specified category.
ConfigurationFile config = ConfigurationFile.Current;
CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory];
settings.Add(nameof(AutoRepeat), m_autoRepeat, "True if receiving (reading) of data is to be repeated endlessly, otherwise False.");
settings.Add(nameof(ReceiveOnDemand), m_receiveOnDemand, "True if receiving (reading) of data will be initiated manually, otherwise False.");
settings.Add(nameof(ReceiveInterval), m_receiveInterval, "Number of milliseconds to pause before receiving (reading) the next available set of data.");
settings.Add(nameof(StartingOffset), m_startingOffset, "Starting point relative to the beginning of the file from where the data is to be received (read).");
settings.Add(nameof(FileOpenMode), FileOpenMode, "Open mode (CreateNew; Create; Open; OpenOrCreate; Truncate; Append) to be used when opening the file.");
settings.Add(nameof(FileShareMode), FileShareMode, "Share mode (None; Read; Write; ReadWrite; Delete; Inheritable) to be used when opening the file.");
settings.Add(nameof(FileAccessMode), m_fileAccessMode, "Access mode (Read; Write; ReadWrite) to be used when opening the file.");
settings.Add(nameof(DisconnectAtEOF), DisconnectAtEOF, "True if client should disconnect when end of file has been reached, otherwise False.");
AutoRepeat = settings[nameof(AutoRepeat)].ValueAs(m_autoRepeat);
ReceiveOnDemand = settings[nameof(ReceiveOnDemand)].ValueAs(m_receiveOnDemand);
ReceiveInterval = settings[nameof(ReceiveInterval)].ValueAs(m_receiveInterval);
StartingOffset = settings[nameof(StartingOffset)].ValueAs(m_startingOffset);
FileOpenMode = settings[nameof(FileOpenMode)].ValueAs(FileOpenMode);
FileShareMode = settings[nameof(FileShareMode)].ValueAs(FileShareMode);
FileAccessMode = settings[nameof(FileAccessMode)].ValueAs(m_fileAccessMode);
DisconnectAtEOF = settings[nameof(DisconnectAtEOF)].ValueAs(DisconnectAtEOF);
}
///
/// Releases the unmanaged resources used by the 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
{
// This will be done regardless of whether the object is finalized or disposed.
if (!disposing)
return;
// This will be done only when the object is disposed by calling Dispose().
m_connectionHandle?.Dispose();
if (m_receiveDataTimer != null)
{
m_receiveDataTimer.Elapsed -= m_receiveDataTimer_Elapsed;
m_receiveDataTimer.Dispose();
}
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
base.Dispose(disposing); // Call base class Dispose().
}
}
///
/// Validates the specified .
///
/// Connection string to be validated.
/// File property is missing.
protected override void ValidateConnectionString(string connectionString)
{
m_connectData = connectionString.ParseKeyValuePairs();
if (!m_connectData.ContainsKey("file"))
throw new ArgumentException($"File property is missing (Example: {DefaultConnectionString})");
}
///
/// Sends (writes) data to the file asynchronously.
///
/// The buffer that contains the binary data to be sent (written).
/// The zero-based position in the at which to begin sending (writing) data.
/// The number of bytes to be sent (written) from starting at the .
/// for the asynchronous operation.
protected override WaitHandle SendDataAsync(byte[] data, int offset, int length)
{
// Send data to the file asynchronously.
WaitHandle handle = m_fileClient.Provider.BeginWrite(data, offset, length, SendDataAsyncCallback, null).AsyncWaitHandle;
// Notify that the send operation has started.
m_fileClient.Statistics.UpdateBytesSent(length);
OnSendDataStart();
// Return the async handle that can be used to wait for the async operation to complete.
return handle;
}
///
/// Callback method for asynchronous send operation.
///
private void SendDataAsyncCallback(IAsyncResult asyncResult)
{
try
{
// Send operation is complete.
m_fileClient.Provider.EndWrite(asyncResult);
OnSendDataComplete();
}
catch (Exception ex)
{
// Send operation failed to complete.
OnSendDataException(ex);
}
}
///
/// Connects to the .
///
private void OpenFile()
{
int connectionAttempts = 0;
while (MaxConnectionAttempts == -1 || connectionAttempts < MaxConnectionAttempts)
{
try
{
OnConnectionAttempt();
// Open the file.
m_fileClient.Provider = new FileStream(FilePath.GetAbsolutePath(m_connectData["file"]), FileOpenMode, m_fileAccessMode, FileShareMode);
// Move to the specified offset.
m_fileClient.Provider.Seek(m_startingOffset, SeekOrigin.Begin);
m_connectionHandle.Set();
OnConnectionEstablished();
if (!m_receiveOnDemand)
{
if (m_receiveInterval > 0)
{
// Start receiving data at interval.
m_receiveDataTimer.Interval = m_receiveInterval;
m_receiveDataTimer.Start();
}
else
{
// Start receiving data continuously.
while (true)
{
ReadData(); // Read all available data.
Thread.Sleep(1000); // Wait for more data to be available.
}
}
}
break; // We're done here.
}
catch (Exception ex)
{
// Keep retrying connecting to the file.
Thread.Sleep(1000);
connectionAttempts++;
OnConnectionException(ex);
}
}
}
///
/// Receive (reads) data from the .
///
private void ReadData()
{
try
{
// Process the entire file content
while (m_fileClient.Provider.Position < m_fileClient.Provider.Length)
{
// Retrieve data from the file.
m_fileClient.BytesReceived = m_fileClient.Provider.Read(m_fileClient.ReceiveBuffer, 0, m_fileClient.ReceiveBufferSize);
m_fileClient.Statistics.UpdateBytesReceived(m_fileClient.BytesReceived);
// Notify of the retrieved data.
OnReceiveDataComplete(m_fileClient.ReceiveBuffer, m_fileClient.BytesReceived);
// Handle end of file operations
if (m_fileClient.Provider.Position == m_fileClient.Provider.Length)
{
// Re-read the file if the user wants to repeat when done reading the file.
if (m_autoRepeat)
m_fileClient.Provider.Seek(m_startingOffset, SeekOrigin.Begin);
else if (DisconnectAtEOF)
Disconnect();
OnEndOfFile();
}
// Stop processing the file if user has either opted to receive data on demand or receive data at a predefined interval.
if (m_receiveOnDemand || m_receiveInterval > 0)
break;
}
}
catch (Exception ex)
{
// Notify of the exception.
if (!(ex is NullReferenceException))
OnReceiveDataException(ex);
}
}
private void m_receiveDataTimer_Elapsed(object sender, EventArgs e) => ReadData();
///
/// Raises the event.
///
/// Exception to send to event.
protected override void OnConnectionException(Exception ex)
{
base.Disconnect();
base.OnConnectionException(ex);
}
///
/// Raises the event.
///
protected virtual void OnEndOfFile()
{
try
{
EndOfFile?.Invoke(this, EventArgs.Empty);
}
catch (Exception ex)
{
OnUnhandledUserException(ex);
}
}
#endregion
#region [ Static ]
// Static Fields
// Common use static timer for FileClient instances
private static readonly SharedTimerScheduler s_timerScheduler;
//Static Constructor
static FileClient()
{
using (Logger.AppendStackMessages("Owner", "FileClient"))
s_timerScheduler = new SharedTimerScheduler();
}
#endregion
}
}