//****************************************************************************************************** // ScheduleManager.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/01/2006 - Pinal C. Patel // Original version of source code generated. // 04/23/2007 - Pinal C. Patel // Made the schedules dictionary case-insensitive. // 04/24/2007 - Pinal C. Patel // Implemented the IPersistSettings and ISupportInitialize interfaces. // 05/02/2007 - Pinal C. Patel // Converted schedules to a list instead of a dictionary. // 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. // Modified the Stop() method to avoid a null reference exception that was most likely causing // the IDE of the component's consumer to crash in design-mode. // 09/19/2008 - J. Ritchie Carroll // Convert to C#. // 11/04/2008 - Pinal C. Patel // Edited code comments. // 06/18/2009 - Pinal C. Patel // Fixed the implementation of Enabled property. // 09/14/2009 - Stephen C. Wills // Added new header and license agreement. // 06/21/2010 - Stephen C. Wills // Modified code to avoid calls to abort the m_startTimerThread. // 07/02/2010 - J. Ritchie Carroll // Fixed an issue related to accessing a disposed timer. // 08/04/2010 - Pinal C. Patel // Fixed an issue with NullReferenceException being encountered when ScheduleManager object is // disposed before it successfully starts up (StartTimer() method). // 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.Text; using System.Threading; using System.Timers; using GSF.Collections; using GSF.Configuration; using Timer = System.Timers.Timer; namespace GSF.Scheduling { /// /// Monitors multiple at an interval of one minute to check if they are due. /// /// /// This example shows how to use the component: /// /// using System; /// using GSF; /// using GSF.Scheduling; /// /// class Program /// { /// static void Main(string[] args) /// { /// ScheduleManager scheduler = new ScheduleManager(); /// scheduler.Initialize(); /// // Add event handlers. /// scheduler.Starting += scheduler_Starting; /// scheduler.Started += scheduler_Started; /// scheduler.ScheduleDue += scheduler_ScheduleDue; /// // Add test schedules. /// scheduler.AddSchedule("Run.Notepad", "* * * * *"); /// scheduler.AddSchedule("Run.Explorer", "* * * * *"); /// // Start the scheduler. /// scheduler.Start(); /// /// Console.ReadLine(); /// } /// /// static void scheduler_Started(object sender, EventArgs e) /// { /// Console.WriteLine("Scheduler has started successfully."); /// } /// /// static void scheduler_Starting(object sender, EventArgs e) /// { /// Console.WriteLine("Scheduler is waiting to be started."); /// } /// /// static void scheduler_ScheduleDue(object sender, EventArgsSchedule]]> e) /// { /// Console.WriteLine(string.Format("{0} schedule is due for processing.", e.Argument.Name)); /// } /// } /// /// /// [ToolboxBitmap(typeof(ScheduleManager))] public class ScheduleManager : Component, ISupportLifecycle, ISupportInitialize, IProvideStatus, IPersistSettings { #region [ Members ] // Constants /// /// Specifies the default value for the property. /// public const bool DefaultPersistSettings = false; /// /// Specifies the default value for the property. /// public const string DefaultSettingsCategory = "ScheduleManager"; /// /// Number of milliseconds between timer ticks. /// private const int TimerInterval = 60000; // Events /// /// Occurs while the is waiting to start at the top of the minute. /// [Category("State"), Description("Occurs while the ScheduleManager is waiting to start at the top of the minute.")] public event EventHandler Starting; /// /// Occurs when the has started at the top of the minute. /// [Category("State"), Description("Occurs when the ScheduleManager has started at the top of the minute.")] public event EventHandler Started; /// /// Occurs asynchronously when a is due according to the rule specified for it. /// /// /// is the that is due. /// [Category("Schedules"), Description("Occurs asynchronously when a Schedule is due according to the rule specified for it.")] public event EventHandler> ScheduleDue; /// /// Occurs when the a particular is being checked to see if it is due. /// /// /// is the that is being checked to see if it is due. /// [Category("Schedules"), Description("Occurs when the a particular Schedule is being checked to see if it is due.")] public event EventHandler> ScheduleDueCheck; // Fields private bool m_persistSettings; private string m_settingsCategory; private readonly List m_schedules; private Timer m_timer; #if ThreadTracking private ManagedThread m_startTimerThread; #else private Thread m_startTimerThread; #endif private bool m_disposed; private bool m_initialized; #endregion #region [ Constructors ] /// /// Initializes a new instance of the class. /// public ScheduleManager() { m_persistSettings = DefaultPersistSettings; m_settingsCategory = DefaultSettingsCategory; m_schedules = new List(); } /// /// Initializes a new instance of the class. /// /// object that contains the . public ScheduleManager(IContainer container) : this() { if ((object)container != null) container.Add(this); } #endregion #region [ Properties ] /// /// Gets or sets a boolean value that indicates whether the settings of object are /// to be saved to the config file. /// [Category("Persistence"), DefaultValue(DefaultPersistSettings), Description("Indicates whether the settings of ScheduleManager 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("Persistence"), DefaultValue(DefaultSettingsCategory), Description("Category under which the settings of ScheduleManager 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(nameof(value)); m_settingsCategory = value; } } /// /// Gets a list of all monitored by the object. /// /// /// Thread-safety Warning: Due to the asynchronous nature of , a lock must be /// obtained on before accessing it. /// [Category("Settings"), DesignerSerializationVisibility(DesignerSerializationVisibility.Content), Description("Gets a list of all Schedule monitored by the ScheduleManager object.")] public List Schedules { get { return m_schedules; } } /// /// Gets a boolean value that indicates whether the is running. /// [Browsable(false)] public bool IsRunning { get { if ((object)m_timer != null) return m_timer.Enabled; return false; } } /// /// Gets or sets a boolean value that indicates whether the object is currently enabled. /// /// /// property is not be set by user-code directly. /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool Enabled { get { return IsRunning; } set { if (value && !Enabled) Start(); else if (!value && Enabled) Stop(); } } /// /// Gets a flag that indicates whether the object has been disposed. /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool IsDisposed { get { return m_disposed; } } /// /// 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(" Number of schedules: "); status.Append(m_schedules.Count); status.AppendLine(); lock (m_schedules) { foreach (Schedule schedule in m_schedules) { status.AppendLine(); status.Append(schedule.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.Clear(); lock (m_schedules) { foreach (Schedule schedule in m_schedules) { if (schedule.UseLocalTime) settings[schedule.Name, true].Update(string.Format("rule={0}; useLocalTime={1}", schedule.Rule, schedule.UseLocalTime), schedule.Description); else settings[schedule.Name, true].Update(schedule.Rule, schedule.Description); } } 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]; foreach (CategorizedSettingsElement setting in settings) { if ((object)setting != null && !string.IsNullOrWhiteSpace(setting.Name)) { string value = setting.Value.ToNonNullNorWhiteSpace("* * * * *"); Dictionary lookup = value.ParseKeyValuePairs(); // Add the schedule if it doesn't exist or update it otherwise with data from the config file. Schedule existingSchedule = FindSchedule(setting.Name); if ((object)existingSchedule == null) { // Schedule doesn't exist, so we'll add it. lock (m_schedules) { m_schedules.Add(new Schedule(setting.Name, lookup.GetOrDefault("rule", key => value), setting.Description.ToNonNullString(), lookup.GetOrDefault("useLocalTime").ParseBoolean())); } } else { // Schedule exists, so we'll update it. existingSchedule.Name = setting.Name; existingSchedule.Rule = lookup.GetOrDefault("rule", key => value); existingSchedule.Description = setting.Description.ToNonNullString(); existingSchedule.UseLocalTime = lookup.GetOrDefault("useLocalTime").ParseBoolean(); } } } } } /// /// Starts the asynchronously if not running. /// public void Start() { if (!IsRunning && (object)m_startTimerThread == null) { // Initialize if uninitialized. Initialize(); // Initialize timer that checks schedules. m_timer = new Timer(TimerInterval); m_timer.Elapsed += m_timer_Elapsed; // Spawn new thread to start timer at top of the minute. #if ThreadTracking m_startTimerThread = new ManagedThread(StartTimer); m_startTimerThread.Name = "GSF.Scheduling.ScheduleManager.StartTimer()"; #else m_startTimerThread = new Thread(StartTimer); m_startTimerThread.IsBackground = true; #endif m_startTimerThread.Start(); } } /// /// Stops the if running. /// public void Stop() { if ((object)m_timer != null) { m_timer.Elapsed -= m_timer_Elapsed; m_timer.Dispose(); } m_timer = null; m_startTimerThread = null; } /// /// Checks all of the to determine if they are due. /// public void CheckAllSchedules() { lock (m_schedules) { foreach (Schedule schedule in m_schedules) { OnScheduleDueCheck(new EventArgs(schedule)); // Schedule is due so raise the event. if (schedule.IsDue()) OnScheduleDue(schedule); } } } /// /// Attempts to add a new . /// /// Name of the new . /// Rule of the new . /// true if a new was added or an existing one was updated; otherwise false. public bool AddSchedule(string scheduleName, string scheduleRule) { return AddSchedule(scheduleName, scheduleRule, false); } /// /// Attempts to add a new . /// /// Name of the new . /// Rule of the new . /// true to update existing with the specified ; otherwise false. /// true if a new was added or an existing one was updated; otherwise false. public bool AddSchedule(string scheduleName, string scheduleRule, bool updateExisting) { return AddSchedule(scheduleName, scheduleRule, string.Empty, updateExisting); } /// /// Attempts to add a new . /// /// Name of the new . /// Rule of the new . /// Description of the new . /// true if a new was added; otherwise false. public bool AddSchedule(string scheduleName, string scheduleRule, string scheduleDescription) { return AddSchedule(scheduleName, scheduleRule, scheduleDescription, false); } /// /// Attempts to add a new . /// /// Name of the new . /// Rule of the new . /// Description of the new . /// true to update existing with the specified ; otherwise false. /// true if a new was added or an existing one was updated; otherwise false. public bool AddSchedule(string scheduleName, string scheduleRule, string scheduleDescription, bool updateExisting) { Schedule existingSchedule = FindSchedule(scheduleName); if ((object)existingSchedule == null) { // Schedule doesn't exist, so we'll add it. lock (m_schedules) { m_schedules.Add(new Schedule(scheduleName, scheduleRule, scheduleDescription)); } return true; } // Schedule exists, we'll update if specified. if (updateExisting) { // Update existing schedule. existingSchedule.Name = scheduleName; existingSchedule.Rule = scheduleRule; existingSchedule.Description = scheduleDescription; return true; } // Leave existing schedule alone. return false; } /// /// Attempts to remove a with the specified name if one exists. /// /// Name of the to be removed. /// true if the was removed; otherwise false. public bool RemoveSchedule(string scheduleName) { Schedule scheduleToRemove = FindSchedule(scheduleName); if ((object)scheduleToRemove != null) { // Schedule exists, so remove it. lock (m_schedules) { m_schedules.Remove(scheduleToRemove); } return true; } // Can't remove schedule, since it doesn't exist. return false; } /// /// Searches for the with the specified name. /// /// Name of the to be obtained. /// object if a match is found; otherwise null. public Schedule FindSchedule(string scheduleName) { Schedule match = null; lock (m_schedules) { foreach (Schedule schedule in m_schedules) { if (string.Compare(schedule.Name, scheduleName, StringComparison.OrdinalIgnoreCase) == 0) { match = schedule; break; } } } return match; } /// /// Raises the event. /// protected virtual void OnStarting() { if ((object)Starting != null) Starting(this, EventArgs.Empty); } /// /// Raises the event. /// protected virtual void OnStarted() { if ((object)Started != null) Started(this, EventArgs.Empty); } /// /// Raises the event. /// /// to send to event. protected virtual void OnScheduleDue(Schedule schedule) { if ((object)ScheduleDue != null) { EventArgs args = new EventArgs(schedule); foreach (Delegate invocationFunction in ScheduleDue.GetInvocationList()) { EventHandler> handler = invocationFunction as EventHandler>; // Asynchronously invoke handlers... if ((object)handler != null) handler.BeginInvoke(this, args, null, null); } } } /// /// Raises the event. /// /// Event data. protected virtual void OnScheduleDueCheck(EventArgs e) { if ((object)ScheduleDueCheck != null) ScheduleDueCheck(this, e); } /// /// 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(). Stop(); SaveSettings(); } } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } } private void StartTimer() { while (!IsRunning && (object)m_timer != null) { OnStarting(); if (DateTime.UtcNow.Second == 0) { // We'll start the timer that will check the schedules at top of the minute. m_timer.Start(); OnStarted(); CheckAllSchedules(); break; } Thread.Sleep(1000); } m_startTimerThread = null; } private void m_timer_Elapsed(object sender, ElapsedEventArgs e) { CheckAllSchedules(); } #endregion } }