//****************************************************************************************************** // LogFile.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 Eclipse Public License -v 1.0 (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/eclipse-1.0.php // // 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: // ---------------------------------------------------------------------------------------------------- // 04/09/2007 - Pinal C. Patel // Original version of source code generated. // 11/30/2007 - Pinal C. Patel // Modified the "design time" check in EndInit() method to use LicenseManager.UsageMode property // instead of DesignMode property as the former is more accurate than the latter. // 09/19/2008 - J. Ritchie Carroll // Converted to C#. // 10/21/2008 - Pinal C. Patel // Edited code comments. // 06/18/2009 - Pinal C. Patel // Fixed the implementation of Enabled property. // 07/02/2009 - Pinal C. Patel // Modified state alterning properties to reopen the file when changed. // 09/14/2009 - Stephen C. Wills // Added new header and license agreement. // 07/30/2012 - Vijay Sukhavasi // Added user option to control time duration of rollover. // 12/14/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.ComponentModel; using System.Configuration; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading; using GSF.Collections; using GSF.Configuration; namespace GSF.IO { #region [ Enumerations ] /// /// Specifies the operation to be performed on the when it is full. /// public enum LogFileFullOperation { /// /// Truncates the existing entries in the to make space for new entries. /// Truncate, /// /// Rolls over to a new , and keeps the full for reference. /// Rollover } #endregion /// /// Represents a file that can be used for logging messages in real-time. /// /// /// This example shows how to use for logging messages: /// /// using System; /// using GSF.IO; /// /// class Program /// { /// static void Main(string[] args) /// { /// LogFile log = new LogFile(); /// log.Initialize(); // Initialize the log file. /// log.Open(); // Open the log file. /// log.WriteTimestampedLine("Test entry"); // Write message to the log file. /// log.Flush(); // Flush message to the log file. /// log.Close(); // Close the log file. /// /// Console.ReadLine(); /// } /// } /// /// [ToolboxBitmap(typeof(LogFile))] public class LogFile : Component, ISupportLifecycle, ISupportInitialize, IProvideStatus, IPersistSettings { #region [ Members ] // Constants /// /// Specifies the minimum size for a . /// public const int MinFileSize = 1; /// /// Specifies the maximum size for a . /// public const int MaxFileSize = 10; /// /// Specifies the default value for the property. /// public const string DefaultFileName = "LogFile.txt"; /// /// Specifies the default value for the property. /// public const int DefaultFileSize = 3; /// /// Specifies the default value for the property. /// public const LogFileFullOperation DefaultFileFullOperation = LogFileFullOperation.Truncate; /// /// Specifies the default value for the property. /// public const bool DefaultPersistSettings = false; /// /// Specifies the default value for the property. /// public const double DefaultLogFilesDuration = 0.0; /// /// Specifies the default value for the property. /// public const string DefaultSettingsCategory = "LogFile"; // Events /// /// Occurs when the is full. /// [Description("Occurs when the LogFile is full.")] public event EventHandler FileFull; /// /// Occurs when an is encountered while writing entries to the . /// /// /// is the encountered during writing of log entries. /// [Description("Occurs when an Exception is encountered while writing entries to the LogFile.")] public event EventHandler> LogException; // Fields private string m_fileName; private int m_fileSize; private LogFileFullOperation m_fileFullOperation; private bool m_persistSettings; private string m_settingsCategory; private FileStream m_fileStream; private readonly ManualResetEvent m_operationWaitHandle; private readonly ProcessList m_logEntryQueue; private Encoding m_textEncoding; private bool m_disposed; private bool m_initialized; private double m_logFilesDuration; private readonly Dictionary m_savedFilesWithTime; #endregion #region [ Constructors ] /// /// Initializes a new instance of the class. /// public LogFile() { m_fileName = DefaultFileName; m_fileSize = DefaultFileSize; m_fileFullOperation = DefaultFileFullOperation; m_logFilesDuration = DefaultLogFilesDuration; m_persistSettings = DefaultPersistSettings; m_settingsCategory = DefaultSettingsCategory; m_textEncoding = Encoding.Default; m_operationWaitHandle = new ManualResetEvent(true); m_savedFilesWithTime = new Dictionary(); m_logEntryQueue = ProcessList.CreateRealTimeQueue(WriteLogEntries); this.FileFull += LogFile_FileFull; m_logEntryQueue.ProcessException += ProcessExceptionHandler; } /// /// Initializes a new instance of the class. /// /// object that contains the . public LogFile(IContainer container) : this() { if ((object)container != null) container.Add(this); } #endregion #region [ Properties ] /// /// Gets or sets the name of the , including the file extension. /// /// The value being assigned is null or empty string. [Category("Settings"), DefaultValue(DefaultFileName), Description("Name of the LogFile, including the file extension.")] public string FileName { get { return m_fileName; } set { if (string.IsNullOrEmpty(value)) throw new ArgumentNullException("value"); m_fileName = value; ReOpen(); } } /// /// Gets or sets the size of the in MB. /// ///The value being assigned outside the and range. [Category("Settings"), DefaultValue(DefaultFileSize), Description("Size of the LogFile in MB.")] public int FileSize { get { return m_fileSize; } set { if (value < MinFileSize || value > MaxFileSize) throw new ArgumentOutOfRangeException("value", string.Format("Value must be between {0} and {1}", MinFileSize, MaxFileSize)); m_fileSize = value; } } /// /// Gets or sets the type of operation to be performed when the is full. /// [Category("Behavior"), DefaultValue(DefaultFileFullOperation), Description("Type of operation to be performed when the LogFile is full.")] public LogFileFullOperation FileFullOperation { get { return m_fileFullOperation; } set { m_fileFullOperation = value; } } /// /// Gets or sets the time duration, in hours, to save the . /// [Category("Behavior"), DefaultValue(DefaultLogFilesDuration), Description("Time duration in hours to save the log files.")] public double LogFilesDuration { get { return m_logFilesDuration; } set { m_logFilesDuration = value; } } /// /// Gets or sets a boolean value that indicates whether the settings of object are /// to be saved to the config file. /// [Category("Persistance"), DefaultValue(DefaultPersistSettings), Description("Indicates whether the settings of LogFile object are to be saved to the config file.")] public bool PersistSettings { get { return m_persistSettings; } set { m_persistSettings = value; } } /// /// Gets or sets the category under which the settings of object are to be saved /// to the config file if the property is set to true. /// /// The value being assigned is null or empty string. [Category("Persistance"), DefaultValue(DefaultSettingsCategory), Description("Category under which the settings of LogFile object are to be saved to the config file if the PersistSettings property is set to true.")] public string SettingsCategory { get { return m_settingsCategory; } set { if (string.IsNullOrEmpty(value)) throw new ArgumentNullException("value"); m_settingsCategory = value; } } /// /// Gets or sets the to be used to encode the messages being logged. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public virtual Encoding TextEncoding { get { return m_textEncoding; } set { if ((object)value == null) m_textEncoding = Encoding.Default; else m_textEncoding = value; } } /// /// Gets or sets a boolean value that indicates whether the object is currently enabled. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool Enabled { get { return IsOpen; } set { if (value && !Enabled) Open(); else if (!value && Enabled) Close(); } } /// /// Gets a boolean value that indicates whether the is open. /// [Browsable(false)] public bool IsOpen { get { return ((object)m_fileStream != null); } } /// /// Gets the unique identifier of the object. /// [Browsable(false)] public string Name { get { return m_settingsCategory; } } /// /// Gets the descriptive status of the object. /// [Browsable(false)] public string Status { get { StringBuilder status = new StringBuilder(); status.Append(" Configuration section: "); status.Append(m_settingsCategory); status.AppendLine(); status.Append(" Maximum export size: "); status.Append(m_fileSize.ToString()); status.Append(" MB"); status.AppendLine(); status.Append(" File full operation: "); status.Append(m_fileFullOperation); status.AppendLine(); if ((object)m_logEntryQueue != null) status.Append(m_logEntryQueue.Status); return status.ToString(); } } #endregion #region [ Methods ] /// /// Initializes the object. /// /// /// is to be called by user-code directly only if the /// object is not consumed through the designer surface of the IDE. /// public void Initialize() { if (!m_initialized) { LoadSettings(); // Load settings from the config file. m_initialized = true; // Initialize only once. } } /// /// Performs necessary operations before the object properties are initialized. /// /// /// should never be called by user-code directly. This method exists solely for use /// by the designer if the object is consumed through the designer surface of the IDE. /// [EditorBrowsable(EditorBrowsableState.Never)] public void BeginInit() { if (!DesignMode) { try { // Nothing needs to be done before component is initialized. } catch (Exception) { // Prevent the IDE from crashing when component is in design mode. } } } /// /// Performs necessary operations after the object properties are initialized. /// /// /// should never be called by user-code directly. This method exists solely for use /// by the designer if the object is consumed through the designer surface of the IDE. /// [EditorBrowsable(EditorBrowsableState.Never)] public void EndInit() { if (!DesignMode) { try { Initialize(); } catch (Exception) { // Prevent the IDE from crashing when component is in design mode. } } } /// /// Saves settings for the object to the config file if the /// property is set to true. /// /// has a value of null or empty string. public void SaveSettings() { if (m_persistSettings) { // Ensure that settings category is specified. if (string.IsNullOrEmpty(m_settingsCategory)) throw new ConfigurationErrorsException("SettingsCategory property has not been set"); // Save settings under the specified category. ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings[m_settingsCategory]; settings["FileName", true].Update(m_fileName); settings["FileSize", true].Update(m_fileSize); settings["FileFullOperation", true].Update(m_fileFullOperation); settings["LogFilesDuration", true].Update(m_logFilesDuration); config.Save(); } } /// /// Loads saved settings for the object from the config file if the /// property is set to true. /// /// has a value of null or empty string. public void LoadSettings() { if (m_persistSettings) { // Ensure that settings category is specified. if (string.IsNullOrEmpty(m_settingsCategory)) throw new ConfigurationErrorsException("SettingsCategory property has not been set"); // Load settings from the specified category. ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings[m_settingsCategory]; settings.Add("FileName", m_fileName, "Name of the log file including its path."); settings.Add("FileSize", m_fileSize, "Maximum size of the log file in MB."); settings.Add("FileFullOperation", m_fileFullOperation, "Operation (Truncate; Rollover) that is to be performed on the file when it is full."); settings.Add("LogFilesDuration", m_logFilesDuration, "Time duration in hours to save the log files,files older than this duration are purged automatically"); FileName = settings["FileName"].ValueAs(m_fileName); FileSize = settings["FileSize"].ValueAs(m_fileSize); FileFullOperation = settings["FileFullOperation"].ValueAs(m_fileFullOperation); LogFilesDuration = settings["LogFilesDuration"].ValueAs(m_logFilesDuration); } } /// /// Opens the for use if it is closed. /// public void Open() { if (!IsOpen) { // Initialize if uninitialized. Initialize(); // Get the absolute file path if a relative path is specified. m_fileName = FilePath.GetAbsolutePath(m_fileName); // Create the folder in which the log file will reside it, if it does not exist. if (!Directory.Exists(FilePath.GetDirectoryName(m_fileName))) Directory.CreateDirectory(FilePath.GetDirectoryName(m_fileName)); // Open the log file (if it exists) or creates it (if it does not exist). m_fileStream = new FileStream(m_fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); // Scrolls to the end of the file so that existing data is not overwritten. m_fileStream.Seek(0, SeekOrigin.End); // If this is a new log file, set its creation date to current date. This is done to prevent historic // log files (when FileFullOperation = Rollover) from having the same start time in their filename. if (m_fileStream.Length == 0) (new FileInfo(m_fileName)).CreationTime = DateTime.Now; // Start the queue to which log entries are going to be added. m_logEntryQueue.Start(); } } /// /// Closes the if it is open. /// /// /// Forces queued log entries to be flushed to the . /// public void Close() { Close(true); } /// /// Closes the if it is open. /// /// true, if queued log entries are to be written to the ; otherwise, false. public void Close(bool flushQueuedEntries) { if (IsOpen) { if (flushQueuedEntries) // Writes all queued log entries to the file. Flush(); else // Stops processing the queued log entries. m_logEntryQueue.Stop(); if ((object)m_fileStream != null) { // Closes the log file. m_fileStream.Close(); m_fileStream = null; } } } /// /// Forces queued log entries to be written to the . /// public void Flush() { if (IsOpen) m_logEntryQueue.Flush(); } /// /// Queues the text for writing to the . /// /// The text to be written to the . public void Write(string text) { // Yield to the "file full operation" to complete, if in progress. m_operationWaitHandle.WaitOne(); if (IsOpen) // Queue the text for writing to the log file. m_logEntryQueue.Add(text); else throw new InvalidOperationException(string.Format("{0} \"{1}\" is not open", this.GetType().Name, m_fileName)); } /// /// Queues the text for writing to the . /// /// The text to be written to the log file. /// /// In addition to the specified text, a "newline" character will be appended to the text. /// public void WriteLine(string text) { Write(text + "\r\n"); } /// /// Queues the text for writing to the log file. /// /// The text to be written to the log file. /// /// In addition to the specified text, a timestamp will be prepended, and a "newline" character will appended to the text. /// public void WriteTimestampedLine(string text) { Write("[" + DateTime.Now.ToString() + "] " + text + "\r\n"); } /// /// Reads and returns the text from the . /// /// The text read from the . public string ReadText() { // Yields to the "file full operation" to complete, if in progress. m_operationWaitHandle.WaitOne(); if (IsOpen) { byte[] buffer = null; lock (m_fileStream) { buffer = new byte[m_fileStream.Length]; m_fileStream.Seek(0, SeekOrigin.Begin); m_fileStream.Read(buffer, 0, buffer.Length); } return m_textEncoding.GetString(buffer); } else { throw new InvalidOperationException(string.Format("{0} \"{1}\" is not open", this.GetType().Name, m_fileName)); } } /// /// Reads text from the and returns a list of lines created by seperating the text by the "newline" /// characters if and where present. /// /// A list of lines from the text read from the . public List ReadLines() { return new List(ReadText().Split(new[] { Environment.NewLine }, StringSplitOptions.None)); } /// /// Raises the event. /// protected virtual void OnFileFull() { if ((object)FileFull != null) FileFull(this, EventArgs.Empty); } /// /// Raises the event. /// /// to send to event. protected virtual void OnLogException(Exception ex) { if ((object)LogException != null) LogException(this, new EventArgs(ex)); } /// /// 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) { try { // This will be done regardless of whether the object is finalized or disposed. if (disposing) { // This will be done only when the object is disposed by calling Dispose(). SaveSettings(); this.FileFull -= LogFile_FileFull; if ((object)m_operationWaitHandle != null) m_operationWaitHandle.Close(); if ((object)m_fileStream != null) m_fileStream.Dispose(); if ((object)m_logEntryQueue != null) { m_logEntryQueue.ProcessException -= ProcessExceptionHandler; m_logEntryQueue.Dispose(); } } } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } } /// /// Re-opens the file if currently open. /// private void ReOpen() { if (IsOpen) { Close(); Open(); } } private void WriteLogEntries(string[] items) { long currentFileSize = 0; long maximumFileSize = (long)(m_fileSize * 1048576); lock (m_fileStream) { currentFileSize = m_fileStream.Length; } for (int i = 0; i < items.Length; i++) { if (!string.IsNullOrEmpty(items[i])) { // Write entries with text. byte[] buffer = m_textEncoding.GetBytes(items[i]); if (currentFileSize + buffer.Length <= maximumFileSize) { // Writes the entry. lock (m_fileStream) { m_fileStream.Write(buffer, 0, buffer.Length); } currentFileSize += buffer.Length; } else { // Either truncates the file or rolls over to a new file because the current file is full. // Prior to acting, it requeues the entries that have not been written to the file. for (int j = items.Length - 1; j >= i; j--) { m_logEntryQueue.Insert(0, items[j]); } // Truncates file or roll over to new file. OnFileFull(); return; } } } } private void LogFile_FileFull(object sender, EventArgs e) { // Signals that the "file full operation" is in progress. m_operationWaitHandle.Reset(); switch (m_fileFullOperation) { case LogFileFullOperation.Truncate: // Deletes the existing log entries, and makes way from new ones. try { Close(false); File.Delete(m_fileName); } finally { Open(); } break; case LogFileFullOperation.Rollover: // Rolls over to a new log file, and keeps the current file for history. try { string directoryName = FilePath.GetDirectoryName(m_fileName); string rootFileName = FilePath.GetFileNameWithoutExtension(m_fileName); // We want this to run only once, when system starts. if (m_savedFilesWithTime.Count < 1) { string expression = string.Format("{0}*", rootFileName); string[] files = Directory.GetFiles(directoryName, expression); foreach (string prestartLogFile in files) { if (!m_savedFilesWithTime.ContainsKey(File.GetLastWriteTime(prestartLogFile))) m_savedFilesWithTime.Add(File.GetLastWriteTime(prestartLogFile), prestartLogFile); } } Close(false); DateTime creationTime = File.GetCreationTime(m_fileName); DateTime lastWriteTime = File.GetLastWriteTime(m_fileName); List savedTimes; string destinationfile; bool fileExists = false; // In cases where logs are filling very fast you may encounter situations // where the log file names could overlap. In order to help with this // case dated file names include milliseconds and verify file uniqueness do { // Keep adding milliseconds to last write time until file name is unique if (fileExists) lastWriteTime = lastWriteTime.AddMilliseconds(1.0D); destinationfile = Path.Combine(directoryName, rootFileName) + "_" + creationTime.ToString("yyyy-MM-dd HH!mm!ss!fff") + "_to_" + lastWriteTime.ToString("yyyy-MM-dd HH!mm!ss!fff") + FilePath.GetExtension(m_fileName); fileExists = File.Exists(destinationfile); } while (fileExists); File.Move(m_fileName, destinationfile); m_savedFilesWithTime.Add(lastWriteTime, destinationfile); savedTimes = m_savedFilesWithTime.Keys.ToList(); savedTimes.Sort(); // Save at least one file even if duration is 0 or negative. for (int i = 0; i < savedTimes.Count - 1; i++) { // True only when time difference is reached. if (DateTime.Compare(DateTime.Now.AddHours(-m_logFilesDuration), savedTimes[i]) > 0) { File.Delete(m_savedFilesWithTime[savedTimes[i]]); m_savedFilesWithTime.Remove(savedTimes[i]); } } } finally { Open(); } break; } // Signals that the "file full operation" is complete. m_operationWaitHandle.Set(); } private void ProcessExceptionHandler(object sender, EventArgs e) { OnLogException(e.Argument); } #endregion } }