diff ServerMonitor/Objects/ServerMonitor.cs @ 17:68d7834dc28e

More comments.
author Brad Greco <brad@bgreco.net>
date Sat, 25 May 2019 15:14:26 -0400
parents 052aa62cb42a
children 781d8b980be1
line wrap: on
line diff
--- a/ServerMonitor/Objects/ServerMonitor.cs	Tue Apr 30 20:40:58 2019 -0400
+++ b/ServerMonitor/Objects/ServerMonitor.cs	Sat May 25 15:14:26 2019 -0400
@@ -4,11 +4,9 @@
 using ServerMonitorApp.Properties;
 using System;
 using System.Collections.Generic;
-using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Net.NetworkInformation;
-using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Windows.Forms;
@@ -16,51 +14,74 @@
 
 namespace ServerMonitorApp
 {
+    /// <summary>Central class for scheduling and executing checks against remote servers.</summary>
     public class ServerMonitor
     {
         private readonly string configFileDir;
         private readonly Logger logger;
+        // Cancellation tokens for executing checks, keyed by check ID.
         private readonly Dictionary<int, CancellationTokenSource> tokens = new Dictionary<int, CancellationTokenSource>();
+        // SSH private keys, keyed by the path to the private key file.
+        // A value of NULL indicates that the private key is inaccessible or encrypted.
         private readonly Dictionary<string, PrivateKeyFile> privateKeys = new Dictionary<string, PrivateKeyFile>();
+        // IDs of all checks that have been paused due to network unavailability,
+        // or due to the system being suspended.
+        // Not to be confused with checks that have been disabled by the user.
         private readonly List<int> pausedChecks = new List<int>();
         private bool running, networkAvailable, suspend;
+        // List of check execution tasks that have been started.
+        // A check task begins by sleeping until the next scheduled execution time,
+        // then executes.
         private Dictionary<Task<CheckResult>, int> tasks;
         private ServerSummaryForm mainForm;
 
-        //private List<Task<CheckResult>> tasks;
-
+        /// <summary>Fires when the status of a check changes.</summary>
         public event EventHandler<CheckStatusChangedEventArgs> CheckStatusChanged;
 
+        /// <summary>The collection of registered servers.</summary>
         public List<Server> Servers { get; private set; } = new List<Server>();
 
+        /// <summary>A collection of all checks belonging to all registerd servers.</summary>
         public IEnumerable<Check> Checks => Servers.SelectMany(s => s.Checks);
 
+        /// <summary>Path to the file that stores server and check configuration.</summary>
         public string ConfigFile { get; private set; }
 
+        /// <summary>Path to the file that stores server and check configuration.</summary>
         public IEnumerable<string> LockedKeys { get { return privateKeys.Where(kvp => kvp.Value == null).Select(kvp => kvp.Key); } }
 
+        /// <summary>ServerMonitor constructor.</summary>
+        /// <param name="mainForm">A reference to the main form.</param>
         public ServerMonitor(ServerSummaryForm mainForm)
         {
             this.mainForm = mainForm;
+            // Store configuration in %appdata%\ServerMonitor
             configFileDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ServerMonitor");
             ConfigFile = Path.Combine(configFileDir, "servers.xml");
             logger = new Logger(Path.Combine(configFileDir, "monitor.log"));
         }
 
+        /// <summary>Registers a new server with the server monitor.</summary>
+        /// <param name="server">The server to be added.</param>
         public void AddServer(Server server)
         {
             Servers.Add(server);
             SaveServers();
         }
 
+        /// <summary>Deletes a server from the server monitor.</summary>
+        /// <param name="server">The server to be deleted.</param>
         public void DeleteServer(Server server)
         {
             Servers.Remove(server);
             SaveServers();
         }
 
+        /// <summary>Loads all servers and checks from the config file.</summary>
         public void LoadServers()
         {
+            bool triedBackup = false;
+
             Read:
             TextReader reader = null;
             try
@@ -69,16 +90,23 @@
                 XmlSerializer serializer = CreateXmlSerializer();
                 Servers.Clear();
                 Servers.AddRange((List<Server>)serializer.Deserialize(reader));
-                // Update the Checks so they know what Server they belong to.
-                // Would rather do this in the Server object on deserialization, but
-                // that doesn't work when using the XML serializer for some reason.
+                // Do some more set-up now that the servers and checks have been loaded.
                 foreach (Server server in Servers)
                 {
+                    // Read private keys into memory if they are accessible and not encrypted.
+                    // If PrivateKeyFile != null, it means same the key has already been loaded for
+                    // a different server and nothing more needs to be done.
                     if (server.LoginType == LoginType.PrivateKey && server.PrivateKeyFile == null)
                         OpenPrivateKey(server.KeyFile);
                     foreach (Check check in server.Checks)
                     {
+                        // Update the checks so they know what server they belong to.
+                        // Would rather do this in the Server object on deserialization, but
+                        // that doesn't work when using the XML serializer for some reason.
                         check.Server = server;
+                        // If the program last exited while the check was running, change its status
+                        // to the result of its last execution (since, at this point, the check is
+                        // not running).
                         if (check.Status == CheckStatus.Running)
                             check.Status = check.LastRunStatus;
                     }
@@ -92,9 +120,23 @@
             catch (InvalidOperationException)
             {
                 reader?.Close();
-                File.Copy(ConfigFile, ConfigFile + ".error", true);
-                File.Copy(ConfigFile + ".bak", ConfigFile, true);
-                goto Read;
+                // If there was an error parsing the XML, try again with the backup config file.
+                if (!triedBackup)
+                {
+                    File.Copy(ConfigFile, ConfigFile + ".error", true);
+                    string backupConfig = ConfigFile + ".bak";
+                    if (File.Exists(backupConfig))
+                    {
+                        File.Copy(backupConfig, ConfigFile, true);
+                    }
+                    triedBackup = true;
+                    goto Read;
+                }
+                else
+                {
+                    // If there was an error reading the backup file too, give up.
+                    throw;
+                }
             }
             finally
             {
@@ -103,10 +145,14 @@
             Application.ApplicationExit += Application_ApplicationExit;
             NetworkChange.NetworkAddressChanged += NetworkChange_NetworkAddressChanged;
             SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
+
+            // Remove old entries from the log file according to user preferences.
             logger.TrimLog();
+
             Run();
         }
 
+        /// <summary>Saves all servers and checks to the config file.</summary>
         public void SaveServers()
         {
             GenerateIds();
@@ -114,6 +160,7 @@
             XmlSerializer serializer = null;
             try
             {
+                // Make a backup first in case something goes wrong in the middle of writing.
                 File.Copy(ConfigFile, ConfigFile + ".bak", true);
             }
             catch { }
@@ -125,6 +172,7 @@
             }
             catch (DirectoryNotFoundException)
             {
+                // If the directory does not exist, create it and try again.
                 Directory.CreateDirectory(configFileDir);
                 writer = new StreamWriter(ConfigFile);
                 serializer = CreateXmlSerializer();
@@ -136,11 +184,17 @@
             }
         }
 
+        /// <summary>Main server monitor loop. Schedules and executes checks.</summary>
         private async void Run()
         {
+            // Do not run again if already running or if the system is suspending or resuming.
             if (running || suspend)
                 return;
+
             running = true;
+
+            // If the network is available, immediately execute checks that were supposed to run
+            // earlier but could not due to network unavailability or the system being suspended.
             networkAvailable = Helpers.IsNetworkAvailable();
             if (networkAvailable)
             {
@@ -150,41 +204,110 @@
                 }
                 pausedChecks.Clear();
             }
+
+            // Schedule all checks to run according to their schedules.
+            // Each check will sleep until it is scheduled to run, then execute.
             tasks = Checks.ToDictionary(c => ScheduleExecuteCheckAsync(c), c => c.Id);
             while (tasks.Count > 0)
             {
+                // When any check is done sleeping and executing, remove the completed
+                // task and queue a new task to schedule it again.
                 Task<CheckResult> task = await Task.WhenAny(tasks.Keys);
                 tasks.Remove(task);
                 try
                 {
                     CheckResult result = await task;
-                    // Result will be null if a scheduled check was disabled
+                    // Do not schedule the task again if it is now disabled.
+                    // Result will be null if a scheduled check was disabled.
                     if (result != null && result.CheckStatus != CheckStatus.Disabled)
                         tasks.Add(ScheduleExecuteCheckAsync(result.Check), result.Check.Id);
                 }
                 catch (OperationCanceledException)
                 {
-
+                    // When a server's state changes to Disabled, any checks that are executing
+                    // are immediately cancelled. Silently catch these expected exceptions.
                 }
             }
+            // If there are no enabled checks scheduled, exit the main loop.
+            // It will be restarted when a check or server is enabled.
             running = false;
         }
 
+        /// <summary>Schedules a check to be run on its schedule.</summary>
+        /// <param name="check">The check to execute.</param>
+        /// <returns>The async check result.</returns>
+        private async Task<CheckResult> ScheduleExecuteCheckAsync(Check check)
+        {
+            // Do not schedule or execute the check if it or its server is disabled.
+            if (!check.Enabled || !check.Server.Enabled)
+                return await Task.FromResult(new CheckResult(check, CheckStatus.Disabled, null));
+
+            // Create a cancellation token that will be used to cancel the check if it or
+            // its server is disabled while it is executing.
+            CancellationTokenSource cts = new CancellationTokenSource();
+            tokens[check.Id] = cts;
+
+            // Sleep until next time the check is supposed to be executed.
+            // Use the LastScheduledRunTime so manual executions by the user do not
+            // interfere with the schedule.
+            check.NextRunTime = check.Schedule.GetNextTime(check.LastScheduledRunTime);
+            int delay = Math.Max(0, (int)(check.NextRunTime - DateTime.Now).TotalMilliseconds);
+            await Task.Delay(delay, cts.Token);
+            check.LastScheduledRunTime = check.NextRunTime;
+
+            // Execute the check if not cancelled.
+            if (!cts.IsCancellationRequested)
+            {
+                // If the network is available, execute the check.
+                // Otherwise, add it to the list of paused checks to be executed
+                // when the network becomes available again.
+                if (networkAvailable)
+                {
+                    return await ExecuteCheckAsync(check, cts.Token);
+                }
+                else
+                {
+                    if (!pausedChecks.Contains(check.Id))
+                        pausedChecks.Add(check.Id);
+                }
+            }
+            return await Task.FromResult(new CheckResult(check, CheckStatus.Disabled, null));
+        }
+
+        /// <summary>Executes a check asynchronously.</summary>
+        /// <param name="check">The check to execute.</param>
+        /// <param name="token">A chancellation token that may be used to cancel the check execution.</param>
+        /// <returns>The async check result.</returns>
         public async Task<CheckResult> ExecuteCheckAsync(Check check, CancellationToken token = default(CancellationToken))
         {
+            // Update the status.
             check.Status = CheckStatus.Running;
             OnCheckStatusChanged(check);
+
+            // Execute the check.
             CheckResult result = await check.ExecuteAsync(token);
+
+            // Increment the consecutive failure counter on failue, or reset
+            // the counter on success.
             if (result.Failed)
                 check.ConsecutiveFailures++;
+            else
+                check.ConsecutiveFailures = 0;
+
             OnCheckStatusChanged(check, result);
             HandleResultAsync(result);
             return result;
         }
 
+        /// <summary>Handles the result of a check execution.</summary>
+        /// <param name="result">The result.</param>
         private void HandleResultAsync(CheckResult result)
         {
+            // Log the result.
             logger.Log(result);
+
+            // Notify the user of failure according to user preferences.
+            // If the check succeeded, result.FailAction will be None.
             if (result.Check.ConsecutiveFailures >= result.Check.MaxConsecutiveFailures)
             {
                 if (result.FailAction == FailAction.FlashTaskbar)
@@ -194,31 +317,41 @@
             }
         }
 
+        /// <summary>Reads all check results from the log for a server.</summary>
+        /// <param name="server">The server whose check results should be read.</param>
+        /// <returns>A list of all check results found in the log file for the given server.</returns>
         public IList<CheckResult> GetLog(Server server)
         {
             return logger.Read(server);
         }
 
+        /// <summary>Saves the check settings and notifies event subscribers when the status of a check changes.</summary>
+        /// <param name="check">The check whose status has changed.</param>
+        /// <param name="result">The check result that caused the status to change, if any.</param>
         private void OnCheckStatusChanged(Check check, CheckResult result = null)
         {
             SaveServers();
             CheckStatusChanged?.Invoke(check, new CheckStatusChangedEventArgs(check, result));
         }
 
+        /// <summary>Handles user modifications to a check's settings.</summary>
+        /// <param name="sender">The check that was modified.</param>
         private void Server_CheckModified(object sender, EventArgs e)
         {
             Check check = (Check)sender;
-            Task<CheckResult> task = tasks.FirstOrDefault(kvp => kvp.Value == check.Id).Key;
+
+            // No need to mess with the task queue if not currently running.
             if (running)
             {
+                Task<CheckResult> task = tasks.FirstOrDefault(kvp => kvp.Value == check.Id).Key;
                 if (task == null)
                 {
-                    // No tasks associated with the check, so schedule a new one
+                    // No tasks associated with the check, so schedule a new one.
                     tasks.Add(ScheduleExecuteCheckAsync(check), check.Id);
                 }
                 else
                 {
-                    // Check was modified or deleted, so remove any waiting tasks
+                    // Check was modified or deleted, so remove any waiting tasks.
                     CancelCheck(check);
                     if (check.Server != null)
                     {
@@ -230,19 +363,26 @@
                     }
                 }
             }
-            // Run again in case removing a task above caused it to stop
+            // Run again in case removing a task above caused it to stop.
             Run();
         }
 
+        /// <summary>Handles the enabled state of a server changing.</summary>
+        /// <param name="sender">The server that was enabled or disabled.</param>
         private void Server_EnabledChanged(object sender, EventArgs e)
         {
             Server server = (Server)sender;
+
+            // Make sure the monitor is running. If no servers were enabled before this
+            // one was enabled, it is not running.
             if (server.Enabled)
             {
                 Run();
             }
             else
             {
+                // Cancel all queued and executing checks belonging to a
+                // server that was disabled.
                 foreach (Check check in server.Checks)
                 {
                     CancelCheck(check);
@@ -250,50 +390,42 @@
             }
         }
 
+        /// <summary>Cancels a check that may be executing.</summary>
+        /// <param name="check">The check to cancel.</param>
         private void CancelCheck(Check check)
         {
             if (tasks == null)
                 return;
+
+            // Find the waiting or executing task for the check and remove it.
             Task<CheckResult> task = tasks.FirstOrDefault(kvp => kvp.Value == check.Id).Key;
             if (task != null)
                 tasks.Remove(task);
+
+            // Remove it from the list of paused checks so it doesn't get restarted later.
             pausedChecks.RemoveAll(id => id == check.Id);
+
+            // Cancel the current execution.
             if (tokens.TryGetValue(check.Id, out CancellationTokenSource cts))
                 cts.Cancel();
         }
 
-        private async Task<CheckResult> ScheduleExecuteCheckAsync(Check check)
-        {
-            if (!check.Enabled || !check.Server.Enabled)
-                return await Task.FromResult(new CheckResult(check, CheckStatus.Disabled, null));
-
-            CancellationTokenSource cts = new CancellationTokenSource();
-            tokens[check.Id] = cts;
-            check.NextRunTime = check.Schedule.GetNextTime(check.LastScheduledRunTime);
-            int delay = Math.Max(0, (int)(check.NextRunTime - DateTime.Now).TotalMilliseconds);
-            await Task.Delay(delay, cts.Token);
-            check.LastScheduledRunTime = check.NextRunTime;
-            if (networkAvailable && !cts.IsCancellationRequested)
-            {
-                return await ExecuteCheckAsync(check, cts.Token);
-            }
-            else
-            {
-                if (!pausedChecks.Contains(check.Id))
-                    pausedChecks.Add(check.Id);
-                return await Task.FromResult(new CheckResult(check, CheckStatus.Disabled, null));
-            }
-        }
-
+        /// <summary>Handles network state changing.</summary>
         private void NetworkChange_NetworkAddressChanged(object sender, EventArgs e)
         {
             networkAvailable = Helpers.IsNetworkAvailable();
+            // If the network is available, it might not have been before.
+            // This method is not called from the correct thread, so special
+            // handling is needed to start it on the UI thread again.
             if (networkAvailable)
                 mainForm.Invoke((MethodInvoker)(() => Run()));
         }
 
+        /// <summary>Handles system power mode changes.</summary>
         private async void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
         {
+            // If the system is being suspended, cancel all waiting and executing checks.
+            // Once all the checks are removed, the main loop will exit.
             if (e.Mode == PowerModes.Suspend)
             {
                 foreach (Check check in Checks)
@@ -304,32 +436,45 @@
             }
             else if (e.Mode == PowerModes.Resume)
             {
+                // When resuming from suspend, examine each check to find out if it was
+                // scheduled to be executed during the time period when the systems was
+                // suspended. Add them to the paused checks list, to be executed almost
+                // immediately.
+                // Make sure the list is empty to start.
                 pausedChecks.Clear();
                 foreach (Check check in Checks)
                 {
-                    //CancelCheck(check);
                     if (check.Enabled && check.Server.Enabled && check.NextRunTime < DateTime.Now)
                     {
                         pausedChecks.Add(check.Id);
                     }
                 }
+                // Wait 10 seconds to give things time to quiet down after resuming.
                 await Task.Delay(10000);
                 suspend = false;
                 Run();
             }
         }
 
+        /// <summary>Unregister system events when exiting.</summary>
         private void Application_ApplicationExit(object sender, EventArgs e)
         {
             NetworkChange.NetworkAddressChanged -= NetworkChange_NetworkAddressChanged;
             SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged;
         }
 
+        /// <summary>Attempts to read a private file.</summary>
+        /// <param name="path">The path to the private key file.</param>
+        /// <param name="password">The password used to encrypt the key.</param>
+        /// <returns>A status indicating the result of the attempt.</returns>
         public KeyStatus OpenPrivateKey(string path, string password = null)
         {
             KeyStatus keyStatus;
+
             if (path == null)
                 keyStatus = KeyStatus.NotAccessible;
+
+            // Check if the key has already been open and read.
             if (privateKeys.TryGetValue(path, out PrivateKeyFile key) && key != null)
                 keyStatus = KeyStatus.Open;
             else
@@ -339,15 +484,20 @@
                     key = new PrivateKeyFile(path, password);
                     keyStatus = KeyStatus.Open;
                 }
+                // If the key is encrypted and the password is empty or incorrect,
+                // return the NeedPassword status.
                 catch (Exception e) when (e is SshPassPhraseNullOrEmptyException || e is InvalidOperationException)
                 {
                     keyStatus = KeyStatus.NeedPassword;
                 }
+                // For any other failure reason, return the NotAccessible status.
                 catch (Exception)
                 {
                     keyStatus = KeyStatus.NotAccessible;
                 }
             }
+            // A single private key may be used by multiple servers. Update all servers
+            // that use this private key with the results of the above operations.
             foreach (Server server in Servers)
             {
                 if (server.KeyFile == path)
@@ -356,14 +506,21 @@
                     server.KeyStatus = keyStatus;
                 }
             }
+            // Keep a reference to this private key so we don't have to re-open
+            // it later if the same key is used on a different server.
             privateKeys[path] = key;
+
             return keyStatus;
         }
 
+        /// <summary>Generates internal IDs for servers and checks.</summary>
         private void GenerateIds()
         {
             if (Servers.Any())
             {
+                // Start at the maximum ID to make sure IDs are not reused
+                // if a server was deleted so old log entries do not get associated
+                // with a new server.
                 int id = Servers.Max(s => s.Id);
                 foreach (Server server in Servers)
                 {
@@ -374,6 +531,8 @@
 
             if (Checks.Any())
             {
+                // Start with the max check ID, same reasons as above.
+                // Is there a reason this is stored in a setting?
                 int id = Math.Max(Settings.Default.MaxCheckId, Checks.Max(c => c.Id));
                 foreach (Check check in Checks)
                 {
@@ -385,16 +544,21 @@
             }
         }
 
+        /// <summary>Creates an XML serializer that can handle servers and all check types.</summary>
+        /// <returns>An XML serializer that can handle servers and all check types.</returns>
         private XmlSerializer CreateXmlSerializer()
         {
             return new XmlSerializer(typeof(List<Server>), Check.CheckTypes);
         }
     }
 
+    /// <summary>Event arguments for when a check status changes.</summary>
     public class CheckStatusChangedEventArgs : EventArgs
     {
+        /// <summary>The check whose status changed.</summary>
         public Check Check { get; private set; }
 
+        /// <summary>The check result that caused the status to change, if any.</summary>
         public CheckResult CheckResult { get; private set; }
 
         public CheckStatusChangedEventArgs(Check check, CheckResult result)
@@ -404,5 +568,15 @@
         }
     }
 
-    public enum FailAction { FlashTaskbar = 0, NotificationBalloon = 1, None = 10 }
+    /// <summary>Possible actions that may be taken when a check fails.</summary>
+    public enum FailAction
+    {
+        /// <summary>Flashes the Server Monitor tasbar program icon.</summary>
+        FlashTaskbar = 0,
+        /// <summary>Shows a balloon in the notification area.</summary>
+        NotificationBalloon = 1,
+        /// <summary>Take no action.</summary>
+        None = 10
+    }
+
 }